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” 모듈을 언급한 부분은 역사적 이유로 그대로 남겨 두었습니다.
초록
이 제안은 호출 가능 객체와 부분적인 인자 목록(위치 인자와 키워드 인자 포함)으로부터 새로운 호출 가능 객체를 구성할 수 있도록 하는 함수 또는 호출 가능한 클래스에 관한 것입니다.
유용한 고차 함수를 담는 “functional”이라는 표준 라이브러리 모듈을 제안하며, 여기에는 partial()의 구현도 포함됩니다.
구현이 SourceForge에 제출되었습니다 [2].
승인
패치 #941881은 2005년에 Py2.5용으로 승인되어 적용되었습니다. 이는 여기에서 설명한 내용과 본질적으로 같으며, 가장 왼쪽의 위치 인자와 모든 키워드를 바인딩하는 partial() 타입 생성자입니다. partial 객체에는 읽기 전용 속성 func, args, keywords가 세 개 있습니다. partial 객체를 호출할 때 객체 자체에 지정된 키워드를 재정의하는 키워드를 지정할 수 있습니다.
동등한 함수의 동작을 더 가깝게 모방하도록 __get__ 메서드를 사용해 partial 구현을 수정할지에 대해서는 별도의 논의가 계속되고 있습니다.
동기
함수형 프로그래밍에서 함수 커링은 여러 인자를 받는 함수를 하나의 인자를 받는 함수로 구현하는 방법입니다. N개의 인자를 받는 함수는 실제로는 하나의 인자를 받아 (N-1)개의 인자를 받는 또 다른 함수를 반환하는 함수입니다. Haskell이나 ML과 같은 언어에서 함수 적용은 함수 호출이:
f x y z
실제로 다음을 의미하도록 동작합니다.:
(((f x) y) z)
실제 프로그래밍에서는 이것이 매우 유용하다는 사실이 드러나므로, 이는 단순히 모호한 이론적 문제로만 볼 수 없습니다. 다른 함수에 인자를 부분적으로 적용하는 방식으로 함수를 표현하면 우아하면서도 강력할 수 있으며, 함수형 언어에서는 이를 많이 사용합니다.
일부 함수형 언어(예: Miranda)에서는 (+1)과 같은 표현식을 Python의 (lambda x: x + 1)과 동등한 의미로 사용할 수 있습니다.
일반적으로 이러한 언어는 강한 타입 시스템을 사용하므로, 컴파일러는 예상되는 인자 수를 항상 알고 있으며 펑터와 예상보다 적은 수의 인자가 주어졌을 때 올바르게 처리할 수 있습니다.
Python은 커링으로 여러 인자를 받는 함수를 구현하지 않으므로, 인자가 부분적으로 적용된 함수를 원한다면 위와 같은 람다를 사용하거나 각 경우에 대해 이름이 있는 함수를 정의할 가능성이 큽니다.
그러나 적어도 말하자면, 람다 문법은 모든 사람의 취향에 맞는 것은 아닙니다. 또한 Python의 위치 인자와 키워드 인자를 모두 사용하는 유연한 매개변수 전달 방식은 부분 적용이라는 개념을 일반화하여 람다로는 할 수 없는 작업을 수행할 기회를 제공합니다.
구현 예
다음은 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()
폐기된 구문 제안
저는 원래 fn@(*args, **kw)라는 구문을 제안했는데, 이는 partial(fn, *args, **kw)와 같은 의미입니다.
@ 기호는 일부 어셈블리 언어에서 레지스터 간접 참조를 나타내는 데 사용되며, 여기서의 용법도 일종의 간접 참조입니다. f@(x)는 f(x)가 아니라, 호출하면 f(x)가 되는 것입니다.
반응이 좋지 않아, 저는 제안의 이 부분을 철회했습니다. 어쨌든, @는 새로운 데코레이터 구문을 위해 사용되게 되었습니다.
comp.lang.python과 python-dev의 피드백
제기된 의견 중에는 다음과 같은 것들이 있었습니다 (요약함):
- 람다로 충분합니다.
- @ 구문은 보기 흉합니다 (만장일치).
- 이것은 사실 클로저가 아니라 커리입니다. ActiveState의 Python Cookbook에 거의 동일한 curry 클래스 구현이 있습니다.
- curry 클래스는 실제로 표준 라이브러리에 유용한 추가가 될 것입니다.
- 이것은 함수 커링이 아니라 부분 적용입니다. 따라서 이제 이름을 partial()로 제안합니다.
- 내장 함수에 포함될 만큼 충분히 유용하지 않을 수도 있습니다.
functional이라는 모듈에 대한 아이디어는 반응이 좋았으며, 거기에 속할 만한 다른 것들도 있습니다 (예를 들어 함수 합성).- 완전성을 위해, 함수 호출 시 제공된 인자 뒤에 부분 인자를 추가하는 또 다른 객체(아마도
rightcurry라고 부를)가 제안되었습니다.
람다가 보통은 충분하지만 항상 그런 것은 아니라는 데 동의합니다. 그리고 저는 유용한 introspection과 서브클래싱의 가능성을 원합니다.
저는 @가 특별히 흉하다는 데 동의하지 않지만, 제가 그저 별난 것일 수도 있습니다. 우리는 딕셔너리, 리스트, 튜플 리터럴을 특수 문장 부호로 깔끔하게 구분하고 있습니다 – 부분 적용된 함수 리터럴을 직접 표현하는 방법이 그리 무리한 것은 아닙니다. 하지만 단 한 사람도 그것이 마음에 든다고 말한 적이 없으므로, 제 입장에서는 이미 끝난 이야기입니다.
저는 클래스를 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.