Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 443 – 단일 디스패치 제너릭 함수

Author:
Łukasz Langa <lukasz at python.org>
Discussions-To:
Python-Dev list
Status:
Final
Type:
Standards Track
Created:
22-May-2013
Python-Version:
3.4
Post-History:
22-May-2013, 25-May-2013, 31-May-2013
Replaces:
245, 246, 3124

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 단일 디스패치 제너릭 함수라고 하는 간단한 형태의 제너릭 프로그래밍을 제공하는 functools 표준 라이브러리 모듈의 새로운 메커니즘을 제안합니다.

제너릭 함수는 서로 다른 타입에 대해 동일한 연산을 구현하는 여러 함수로 구성됩니다. 호출 중 어떤 구현을 사용할지는 디스패치 알고리즘이 결정합니다. 단일 인자의 타입을 기준으로 구현을 선택하는 경우 이를 단일 디스패치라고 합니다.

근거 및 목표

Python은 len(), iter(), pprint.pprint(), copy.copy()operator 모듈에 있는 대부분의 함수와 같은 다양한 내장 제너릭 함수와 표준 라이브러리 제너릭 함수를 항상 제공해 왔습니다. 그러나 현재는:

  1. 개발자가 새로운 제너릭 함수를 만들 수 있는 간단하거나 직관적인 방법이 없습니다.
  2. 기존 제너릭 함수에 메서드를 추가하는 표준적인 방법이 없습니다(즉, 일부는 등록 함수를 사용하여 추가하고, 다른 일부는 __special__ 메서드를 정의해야 하며, 경우에 따라 몽키 패칭을 사용해야 합니다).

또한 Python 코드가 객체에 대해 수행할 작업을 결정하기 위해 전달받은 인자의 타입을 검사하는 것은 현재 흔한 안티 패턴입니다.

예를 들어 코드는 어떤 타입의 객체 하나 또는 해당 타입의 객체 시퀀스를 받아들이려고 할 수 있습니다. 현재 이를 수행하는 “명백한 방법”은 타입을 검사하는 것이지만, 이는 취약하고 확장에 닫혀 있습니다.

추상 베이스 클래스는 현재 동작을 더 쉽게 발견할 수 있게 하지만, 새로운 동작을 추가하는 데는 도움이 되지 않습니다. 이미 작성된 라이브러리를 사용하는 개발자는 그러한 코드가 자신의 객체를 처리하는 방식을 변경하지 못할 수 있으며, 특히 사용 중인 객체가 제3자에 의해 생성된 경우에는 더욱 그렇습니다.

따라서 이 PEP는 데코레이터를 사용하여 동적 오버로딩을 처리하는 통일된 API를 제안합니다.

사용자 API

제너릭 함수를 정의하려면 @singledispatch 데코레이터로 장식하십시오. 디스패치는 첫 번째 인자의 타입을 기준으로 수행된다는 점에 유의하십시오. 이에 맞게 함수를 작성하십시오.:

>>> from functools import singledispatch
>>> @singledispatch
... def fun(arg, verbose=False):
...     if verbose:
...         print("Let me just say,", end=" ")
...     print(arg)

함수에 오버로드된 구현을 추가하려면 제너릭 함수의 register()속성을 사용하십시오. 이는 데코레이터이며, 타입 매개변수를 받아 해당 타입에 대한 연산을 구현하는 함수를 장식합니다.:

>>> @fun.register(int)
... def _(arg, verbose=False):
...     if verbose:
...         print("Strength in numbers, eh?", end=" ")
...     print(arg)
...
>>> @fun.register(list)
... def _(arg, verbose=False):
...     if verbose:
...         print("Enumerate this:")
...     for i, elem in enumerate(arg):
...         print(i, elem)

람다와 기존 함수를 등록하려면 register()속성을 함수형 형식으로 사용할 수 있습니다.:

>>> def nothing(arg, verbose=False):
...     print("Nothing.")
...
>>> fun.register(type(None), nothing)

register()속성은 장식되지 않은 함수를 반환합니다. 이를 통해 데코레이터 중첩, 피클링, 각 변형에 대한 단위 테스트를 독립적으로 만들 수 있습니다.:

>>> @fun.register(float)
... @fun.register(Decimal)
... def fun_num(arg, verbose=False):
...     if verbose:
...         print("Half of your number:", end=" ")
...     print(arg / 2)
...
>>> fun_num is fun
False

호출되면 제너릭 함수는 첫 번째 인자의 타입을 기준으로 디스패치합니다.:

>>> fun("Hello, world.")
Hello, world.
>>> fun("test.", verbose=True)
Let me just say, test.
>>> fun(42, verbose=True)
Strength in numbers, eh? 42
>>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)
Enumerate this:
0 spam
1 spam
2 eggs
3 spam
>>> fun(None)
Nothing.
>>> fun(1.23)
0.615

특정 타입에 등록된 구현이 없으면 해당 타입의 메서드 결정 순서를 사용하여 더 일반적인 구현을 찾습니다. @singledispatch로 장식된 원래 함수는 기본 object 타입에 등록되므로, 더 적합한 구현을 찾지 못한 경우 사용됩니다.

주어진 타입에 대해 제너릭 함수가 어떤 구현을 선택할지 확인하려면 dispatch()속성을 사용하십시오.:

>>> fun.dispatch(float)
<function fun_num at 0x104319058>
>>> fun.dispatch(dict)    # note: default implementation
<function fun at 0x103fe0000>

등록된 모든 구현에 액세스하려면 읽기 전용 registry속성을 사용하십시오.:

>>> fun.registry.keys()
dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>,
          <class 'decimal.Decimal'>, <class 'list'>,
          <class 'float'>])
>>> fun.registry[float]
<function fun_num at 0x1035a2840>
>>> fun.registry[object]
<function fun at 0x103fe0000>

제안된 API는 설명하고 사용하기 쉬우며 functools 모듈의 기존 구성원과 일관성을 유지할 수 있도록 의도적으로 제한적이고 명확한 견해를 따릅니다.

구현 참고 사항

이 PEP에서 설명하는 기능은 이미 pkgutil 표준 라이브러리 모듈에서 simplegeneric으로 구현되어 있습니다. 이 구현은 충분히 성숙했으므로, 거의 있는 그대로 옮기는 것이 목표입니다. 참조 구현은 hg.python.org에서 확인할 수 있습니다 [1].

디스패치 유형은 데코레이터 인자로 지정합니다. 함수 어노테이션을 사용하는 대안 형식도 고려했지만, 포함하지 않기로 했습니다. 2013년 5월 현재 이 사용 패턴은 표준 라이브러리의 범위 밖에 있으며 [2], 어노테이션 사용에 관한 모범 사례는 여전히 논의 중입니다.

현재 pkgutil.simplegeneric 구현을 기반으로 하고 추상 베이스 클래스에 가상 서브클래스를 등록하는 관례를 따르므로, 디스패치 레지스트리는 스레드로부터 안전하지 않습니다.

추상 베이스 클래스

pkgutil.simplegeneric 구현은 여러 형태의 메서드 결정 순서(MRO)에 의존했습니다. @singledispatch는 구식 클래스와 Zope의 ExtensionClasses에 대한 특수 처리를 제거합니다. 더 중요한 점은 추상 베이스 클래스(ABC)에 대한 지원을 도입한다는 것입니다.

ABC에 제너릭 함수 구현이 등록되면 디스패치 알고리즘은 확장된 형태의 C3 선형화로 전환하며, 제공된 인자의 MRO에 관련 ABC를 포함합니다. 알고리즘은 ABC의 기능이 도입되는 위치에 ABC를 삽입합니다. 즉, issubclass(cls, abc)는 클래스 자체에 대해서는 True를 반환하지만 모든 직접 베이스 클래스에 대해서는 False를 반환합니다. 특정 클래스에 대한 암시적 ABC(등록되었거나 __len__()과 같은 특수 메서드의 존재로 추론된 ABC)는 해당 클래스의 MRO에 명시적으로 나열된 마지막 ABC 바로 뒤에 삽입됩니다.

이 선형화는 가장 기본적인 형태에서 주어진 유형의 MRO를 반환합니다.:

>>> _compose_mro(dict, [])
[<class 'dict'>, <class 'object'>]

두 번째 인자에 지정된 유형이 서브클래스인 ABC가 포함되어 있으면, 해당 ABC는 예측 가능한 순서로 삽입됩니다.:

>>> _compose_mro(dict, [Sized, MutableMapping, str,
...                     Sequence, Iterable])
[<class 'dict'>, <class 'collections.abc.MutableMapping'>,
 <class 'collections.abc.Mapping'>, <class 'collections.abc.Sized'>,
 <class 'collections.abc.Iterable'>, <class 'collections.abc.Container'>,
 <class 'object'>]

이 작동 방식은 훨씬 느리지만 모든 디스패치 결정은 캐시됩니다. 제너릭 함수에 새 구현을 등록하거나 사용자 코드가 ABC에 register()를 호출하여 암시적으로 서브클래스로 만들면 캐시가 무효화됩니다. 후자의 경우에는 모호한 디스패치 상황이 발생할 수 있습니다. 예를 들어 다음과 같습니다.:

>>> from collections.abc import Iterable, Container
>>> class P:
...     pass
>>> Iterable.register(P)
<class '__main__.P'>
>>> Container.register(P)
<class '__main__.P'>

모호한 상황에 직면했을 때 @singledispatch는 추측하려는 유혹을 거부합니다.:

>>> @singledispatch
... def g(arg):
...     return "base"
...
>>> g.register(Iterable, lambda arg: "iterable")
<function <lambda> at 0x108b49110>
>>> g.register(Container, lambda arg: "container")
<function <lambda> at 0x108b491c8>
>>> g(P())
Traceback (most recent call last):
...
RuntimeError: Ambiguous dispatch: <class 'collections.abc.Container'>
or <class 'collections.abc.Iterable'>

클래스 정의 중 하나 이상의 ABC가 베이스 클래스로 명시적으로 제공되었다면 이 예외는 발생하지 않는다는 점에 유의하십시오. 이 경우 디스패치는 MRO 순서로 수행됩니다.:

>>> class Ten(Iterable, Container):
...     def __iter__(self):
...         for i in range(10):
...             yield i
...     def __contains__(self, value):
...         return value in range(10)
...
>>> g(Ten())
'iterable'

__len__()또는 __contains__()와 같은 특수 메서드의 존재로 ABC의 서브클래싱이 추론되는 경우에도 유사한 충돌이 발생합니다.:

>>> class Q:
...   def __contains__(self, value):
...     return False
...
>>> issubclass(Q, Container)
True
>>> Iterable.register(Q)
>>> g(Q())
Traceback (most recent call last):
...
RuntimeError: Ambiguous dispatch: <class 'collections.abc.Container'>
or <class 'collections.abc.Iterable'>

PEP의 초기 버전에는 더 단순하지만 놀라운 결과를 낳는 여러 경계 사례를 만든 사용자 지정 방식이 포함되어 있었습니다 [3].

사용 패턴

이 PEP는 제너릭으로 명시적으로 표시된 함수의 동작만 확장할 것을 제안합니다. 베이스 클래스 메서드를 서브클래스가 재정의할 수 있는 것처럼, 함수도 특정 유형에 대한 사용자 지정 기능을 제공하도록 오버로드할 수 있습니다.

보편적 오버로딩이 임의적 오버로딩과 같은 것은 아닙니다. 기존 함수의 동작을 사람들이 예측할 수 없는 방식으로 무작위로 재정의할 것이라고 예상할 필요는 없다는 의미입니다. 오히려 실제 프로그램에서 제너릭 함수의 사용은 매우 예측 가능한 패턴을 따르는 경향이 있으며, 일반적인 경우 등록된 구현을 쉽게 찾을 수 있습니다.

모듈이 새로운 제너릭 연산을 정의하는 경우, 대개 같은 위치에서 기존 유형에 필요한 구현도 정의합니다. 마찬가지로 모듈이 새로운 유형을 정의하는 경우, 해당 모듈이 알고 있거나 중요하게 여기는 제너릭 함수에 대한 구현도 대개 그곳에서 정의합니다. 따라서 등록된 구현의 대다수는 오버로드되는 함수의 인접 위치에서 찾거나, 구현이 지원을 추가하는 새로 정의된 유형의 인접 위치에서 찾을 수 있습니다.

함수나 구현이 추가되는 타입(들) 어느 것도 포함하지 않는 모듈에 구현이 등록되는 경우는 상당히 드뭅니다. 무능력이나 일부러 모호하게 만들려는 의도가 없다면, 관련 타입(들)이나 함수(들) 옆에 등록되지 않은 소수의 구현은 일반적으로 해당 구현이 정의된 범위 밖에서는 이해하거나 알 필요가 없습니다. (“지원 모듈”의 경우는 제외하며, 이 경우에는 그에 맞게 이름을 지정하는 것이 모범 사례입니다.)

앞서 언급했듯이, 단일 디스패치 제너릭은 이미 표준 라이브러리 전반에 널리 사용되고 있습니다. 이를 깔끔하고 표준적인 방식으로 구현하면 그러한 사용자 정의 구현을 공통 구현을 사용하도록 리팩터링할 수 있으며, 동시에 사용자가 확장할 수 있도록 개방하는 방법이 마련됩니다.

대안 접근법

Phillip J.는 PEP 3124에서 Eby는 임의의 규칙 집합에 기반한 오버로딩(기본 구현은 인자 타입을 기준으로 디스패치함)과 인터페이스, 적응 및 메서드 결합을 포함하는 완전한 솔루션을 제안합니다. PEAK-Rules [4]는 PJE의 PEP에 설명된 개념의 참조 구현입니다.

이러한 광범위한 접근법은 본질적으로 복잡하므로 합의에 도달하기가 어렵습니다. 이와 대조적으로 이 PEP는 추론하기 쉬운 단일 기능에 초점을 맞춥니다. 이것이 현재나 향후에 다른 접근법을 사용하는 것을 배제하지 않는다는 점을 유의해야 합니다.

Guido van Rossum은 Artima의 2005년 글 [5]에서 함수의 모든 인자 타입을 기준으로 디스패치하는 제너릭 함수 구현을 제시합니다. Andrey Popp의 PyPI에서 제공되는 generic패키지 [6], 그리고 David Mertz의 gnosis.magic.multimethods [7]에서도 동일한 접근법을 선택했습니다.

처음에는 이것이 바람직해 보이지만, “함수가 실행해야 할 코드를 구분하기 위해 수많은 논리가 필요한 API를 설계한다면, API 설계를 다른 사람에게 맡겨야 합니다”라는 Fredrik Lundh의 의견에 동의합니다. 다시 말해, 이 PEP에서 제안하는 단일 인자 접근법은 구현하기 쉬울 뿐만 아니라 더 복잡한 상태를 기준으로 디스패치하는 것이 안티패턴임을 명확하게 전달합니다. 또한 객체 지향 프로그래밍에서 익숙한 메서드 디스패치 메커니즘에 직접 대응한다는 장점도 있습니다. 유일한 차이점은 사용자 정의 구현이 데이터(객체 지향 메서드)와 알고리즘(단일 디스패치 오버로딩) 중 어느 쪽에 더 밀접하게 연결되는지입니다.

PyPy의 RPython은 클래스를 외부에서 확장할 수 있게 하는 메타클래스인 extendabletype을 제공합니다 [8]. pairtype()pair()팩토리와 결합하면 이는 단일 디스패치 제너릭의 한 형태를 제공합니다.

감사의 말

Phillip J. Eby의 작업을 제외하면, 영향으로는 PEP 3124와 PEAK-Rules에 대한 그의 작업, pkgutil.simplegenericfunctools API의 일부로 공개하자고 제안한 Paul Moore의 최초 이슈 [9], 멀티메서드에 관한 Guido van Rossum의 글 [5], 그리고 일반적인 pprint 재작성에 관해 Raymond Hettinger와 나눈 논의 등이 있습니다. 이 PEP를 작성하도록 격려하고 초기 피드백을 제공해 준 Alyssa Coghlan에게 깊이 감사드립니다.

참고 문헌