PEP 309 – 부분 함수 적용
- Author:
- Peter Harris <scav at blueyonder.co.uk>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 08-Feb-2003
- Python-Version:
- 2.5
- Post-History:
- 10-Feb-2003, 27-Feb-2003, 22-Feb-2004, 28-Apr-2006
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
참고
이 PEP가 수락된 후 python-dev와 comp.lang.python에서 이어진 논의를 통해, 함수 객체를 다루지만 함수형 프로그래밍과는 관련이 없는 여러 도구가 필요하다는 요구가 드러났습니다. 이러한 도구를 위해 새 모듈을 만드는 대신, 새롭게 넓어진 범위를 반영하도록 “functional” 모듈의 이름을 “functools”로 바꾸기로 합의했습니다 [1].
이 PEP에서 “functional” 모듈을 언급한 부분은 역사적인 이유로 그대로 남겨 두었습니다.
초록
이 제안은 호출 가능 객체와 부분 인자 목록(위치 인자와 키워드 인자 포함)으로부터 새로운 호출 가능 객체를 구성할 수 있게 하는 함수 또는 호출 가능 클래스에 관한 것입니다.
partial()의 구현을 비롯하여 유용한 고차 함수를 담을 “functional”이라는 표준 라이브러리 모듈을 제안합니다.
구현은 SourceForge에 제출되었습니다 [2].
수락
패치 #941881은 2005년에 Py2.5를 위해 수락되어 적용되었습니다. 이 패치는 본질적으로 여기에서 설명한 것과 같으며, 가장 왼쪽의 위치 인자와 임의의 키워드를 바인딩하는 partial() 타입 생성자입니다. partial 객체에는 읽기 전용 속성 func, args, keywords(키워드) 세 가지가 있습니다. partial 객체를 호출할 때 객체 자체에 있는 키워드를 재정의하는 키워드를 지정할 수 있습니다.
동등한 함수의 동작을 더 가깝게 흉내 내도록 partial 구현에 __get__ 메서드를 추가할지에 대해서는 별도의 논의가 계속되고 있습니다.
동기
함수형 프로그래밍에서 함수 커링은 인자가 여러 개인 함수를 인자가 하나인 함수로 구현하는 방법입니다. 인자가 N개인 함수는 실제로는 (N-1)개의 인자를 받는 또 다른 함수를 반환하는, 인자가 1개인 함수입니다. Haskell이나 ML 같은 언어에서 함수 적용은 다음과 같은 함수 호출이:
f x y z
실제로는 다음을 의미하도록 동작합니다:
(((f x) y) z)
실제 프로그래밍에서 매우 유용하다는 점이 드러나지 않았다면 이는 모호한 이론적 문제에 불과했을 것입니다. 다른 함수에 인자를 부분 적용하는 방식으로 함수를 표현하면 우아하면서도 강력할 수 있으며, 함수형 언어에서는 이 방식이 매우 많이 사용됩니다.
일부 함수형 언어(예: Miranda)에서는 (+1)과 같은 표현식을 사용하여 Python의 (lambda x: x + 1)과 동등한 것을 나타낼 수 있습니다.
일반적으로 그러한 언어는 강한 타입 언어이므로, 컴파일러는 기대하는 인자의 개수를 항상 알고 있으며 함자(functor)와 기대보다 적은 인자가 주어졌을 때 올바르게 처리할 수 있습니다.
Python은 커링으로 다중 인자 함수를 구현하지 않으므로, 부분 적용된 인자를 가진 함수가 필요하다면 아마 위와 같이 lambda를 사용하거나 각 경우마다 이름 있는 함수를 정의할 것입니다.
그러나 좋게 말해도 람다 문법이 모든 사람의 취향에 맞는 것은 아닙니다. 게다가 위치 인자와 키워드 인자를 모두 사용하는 Python의 유연한 매개변수 전달 방식은 부분 적용이라는 개념을 일반화하여 lambda로는 할 수 없는 일을 할 수 있는 기회를 제공합니다.
구현 예
다음은 Python에서 부분 적용된 인자를 가진 호출 가능 객체를 만드는 한 가지 방법입니다. 아래 구현은 Scott David Daniels가 제공한 개선 사항을 바탕으로 합니다:
class partial(object):
def __init__(*args, **kw):
self = args[0]
self.fn, self.args, self.kw = (args[1], args[2:], kw)
def __call__(self, *args, **kw):
if kw and self.kw:
d = self.kw.copy()
d.update(kw)
else:
d = kw or self.kw
return self.fn(*(self.args + args), **d)
(이와 비슷한 레시피가 한동안 Python Cookbook에 실려 있었습니다 [3].)
객체를 함수처럼 호출하면 위치 인자는 생성자에 제공된 위치 인자 뒤에 덧붙여지고, 키워드 인자는 생성자에 제공된 키워드 인자를 재정의하고 보강한다는 점에 유의하십시오.
객체를 생성할 때와 호출할 때 모두 위치 인자, 키워드 인자 또는 둘 다를 전달할 수 있습니다.
사용 예
따라서 partial(operator.add, 1)은 (lambda x: 1 + x)와 약간 비슷합니다. 물론 이점이 드러나는 예는 아닙니다.
또한 클래스 자체가 객체를 만들어 내는 호출 가능 객체 팩토리이므로, 클래스도 같은 방식으로 감쌀 수 있다는 점에 유의하십시오. 따라서 어떤 경우에는 서브클래스를 정의하는 대신, 생성자에 인자를 부분 적용하여 클래스를 특수화할 수 있습니다.
예를 들어, partial(Tkinter.Label, fg='blue')는 기본적으로 전경색이 파란색인 Tkinter Label을 만듭니다.
다음은 부분 적용을 사용하여 Tkinter 위젯의 콜백을 즉석에서 구성하는 간단한 예입니다:
from Tkinter import Tk, Canvas, Button
import sys
from functional import partial
win = Tk()
c = Canvas(win,width=200,height=50)
c.pack()
for colour in sys.argv[1:]:
b = Button(win, text=colour,
command=partial(c.config, bg=colour))
b.pack(side='left')
win.mainloop()
폐기된 문법 제안
저는 원래 partial(fn, *args, **kw)와 같은 의미를 갖는 fn@(*args, **kw) 문법을 제안했습니다.
@ 기호는 일부 어셈블리 언어에서 레지스터 간접 참조를 나타내는 데 사용되며, 여기에서의 용법 역시 일종의 간접 참조입니다. f@(x)는 f(x)가 아니라, 호출하면 f(x)가 되는 무언가입니다.
반응이 좋지 않았으므로, 제안의 이 부분은 철회했습니다. 어쨌든 @는 이미 새로운 데코레이터 문법에 쓰이게 되었습니다.
comp.lang.python과 python-dev의 피드백
제기된 의견 중에는 다음과 같은 것들이 있었습니다(제가 요약한 것입니다):
- 람다로 충분합니다.
- @ 문법은 보기 흉합니다(만장일치).
- 이것은 사실 클로저라기보다는 커리입니다. ActiveState의 Python Cookbook에 거의 동일한 커리 클래스 구현이 있습니다.
- 커리 클래스는 표준 라이브러리에 정말 유용한 추가 요소가 될 것입니다.
- 이것은 함수 커링이 아니라 부분 적용입니다. 따라서 이제 이름을 partial()로 제안합니다.
- 내장 함수에 들어갈 만큼 유용하지는 않을 수도 있습니다.
functional이라는 모듈을 만들자는 아이디어는 좋은 반응을 얻었으며, 그 모듈에 들어갈 만한 다른 것들(예를 들어 함수 합성)도 있습니다.- 완전성을 위해, 함수 호출 시 전달된 인자 뒤에 부분 인자를 덧붙이는 또 다른 객체(아마도
rightcurry라는 이름)도 제안되었습니다.
람다가 대개는 충분하다는 데 동의하지만, 항상 그런 것은 아닙니다. 그리고 저는 유용한 인트로스펙션과 서브클래싱이 가능하기를 원합니다.
@가 특별히 보기 흉하다는 데는 동의하지 않지만, 어쩌면 제가 그냥 별난 것일 수도 있습니다. 딕셔너리, 리스트, 튜플 리터럴은 특별한 구두점으로 깔끔하게 구별됩니다 – 부분 적용된 함수 리터럴을 직접 표현하는 방법도 그리 억지스러운 것은 아닙니다. 하지만 이 문법이 마음에 든다고 말한 사람이 단 한 명도 없었으므로, 제가 보기에 이 제안은 죽은 앵무새나 마찬가지입니다.
이 클래스를 curry나 closure 대신 partial이라고 부르는 데 동의하므로, 이 PEP의 제안을 그에 맞게 수정했습니다. 다만 전체를 수정하지는 않았습니다. 당시 논의가 그 단계에 있었기 때문에 ‘curry’를 잘못 언급한 부분이 일부 남아 있습니다.
오른쪽부터 인자를 부분 적용하거나 임의의 위치에 인자를 끼워 넣는 방식에는 그 나름의 문제가 있습니다. 하지만 좋은 구현과 혼란스럽지 않은 의미론을 찾을 때까지는 이를 배제해서는 안 된다고 생각합니다.
Carl Banks는 진짜 함수형 클로저로 만든 구현을 올렸습니다:
def curry(fn, *cargs, **ckwargs):
def call_fn(*fargs, **fkwargs):
d = ckwargs.copy()
d.update(fkwargs)
return fn(*(cargs + fargs), **d)
return call_fn
그는 이 구현이 더 효율적이라고 저에게 장담합니다.
또한 C로 작성하면 성능이 얼마나 개선될지 가늠하기 위해 이 클래스를 Pyrex로도 작성해 보았습니다:
cdef class curry:
cdef object fn, args, kw
def __init__(self, fn, *args, **kw):
self.fn=fn
self.args=args
self.kw = kw
def __call__(self, *args, **kw):
if self.kw: # from Python Cookbook version
d = self.kw.copy()
d.update(kw)
else:
d=kw
return self.fn(*(self.args + args), **d)
Pyrex에서 얻는 성능 향상은 중첩 함수 구현에 비해 100% 미만입니다. 완전히 범용적이려면 Python API 호출을 통해 동작해야 하기 때문입니다. 같은 이유로 C 구현도 크게 빠르지는 않을 것이므로, C로 작성한 내장 기능을 두자는 주장은 그다지 설득력이 없습니다.
요약
저는 함수와 그 밖의 호출 가능 객체를 부분 적용하는 수단이 표준 라이브러리에 있기를 바랍니다.
표준 라이브러리 모듈 functional에는 partial의 구현과 커뮤니티가 원하는 그 밖의 고차 함수가 들어가야 합니다. 다만 그 모듈에 들어갈 만한 다른 함수들은 이 PEP의 범위를 벗어납니다.
구현, 문서, 단위 테스트에 대한 패치(각각 SF 패치 931005, 931007, 931010)가 제출되었지만 아직 체크인되지는 않았습니다.
장혜식(Hye-Shik Chang)의 C 구현도 제출되었습니다. 다만 이 구현은 Python 구현이 최적화할 가치가 있을 만큼 유용하다는 것이 입증된 뒤에야 포함될 것으로 예상됩니다.
참고 문헌
Copyright
This document has been placed in the public domain.