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

Python 개선 제안 한국어 번역

PEP 3124 – 오버로딩, 제네릭 함수, 인터페이스, 어댑테이션

Author:
Phillip J. Eby <pje at telecommunity.com>
Discussions-To:
Python-3000 list
Status:
Deferred
Type:
Standards Track
Requires:
3107, 3115, 3119
Created:
28-Apr-2007
Post-History:
30-Apr-2007
Replaces:
245, 246

Table of Contents

번역·라이선스 안내

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

보류됨

https://mail.python.org/pipermail/python-3000/2007-July/008784.html 을 참고하십시오.

초록

이 PEP는 동적 오버로딩(제네릭 함수라고도 합니다), 인터페이스, 어댑테이션, 메서드 결합(CLOS와 AspectJ 방식), 그리고 단순한 형태의 관점 지향 프로그래밍(AOP)을 포함한 제네릭 프로그래밍 기능을 제공하는 새 표준 라이브러리 모듈 overloading을 제안합니다.

제안하는 API는 확장에도 열려 있습니다. 즉, 라이브러리 개발자는 자신만의 특수한 인터페이스 타입, 제네릭 함수 디스패처, 메서드 결합 알고리즘 등을 구현할 수 있으며, 제안하는 API는 이러한 확장을 일급 시민으로 취급합니다.

이 API는 C 없이 순수 Python으로 구현되지만, sys._getframe이나 함수의 func_code 속성과 같은 CPython 고유 기능에 대해 일부 의존성을 가질 수 있습니다. 예를 들어 Jython과 IronPython에는 (아마도 Java나 C#을 사용하여) 비슷한 기능을 구현하는 다른 방법이 있을 것으로 예상합니다.

근거와 목표

Python은 len(), iter(), pprint.pprint(), 그리고 operator 모듈에 있는 함수 대부분과 같은 다양한 내장 및 표준 라이브러리 제네릭 함수를 항상 제공해 왔습니다. 하지만 현재 파이썬에는 다음과 같은 한계가 있습니다.

  1. 개발자가 새 제네릭 함수를 만들 수 있는 간단하거나 직관적인 방법이 없습니다.
  2. 기존 제네릭 함수에 메서드를 추가하는 표준 방법이 없습니다(즉, 어떤 것은 등록 함수를 사용하여 추가하고, 어떤 것은 __special__ 메서드를 정의해야 하며, 경우에 따라서는 몽키패칭이 필요합니다).
  3. 여러 인자 타입에 따른 디스패치를 허용하지 않습니다(단, 산술 연산자의 경우 “오른쪽”(__r*__) 메서드를 사용하여 두 인자 디스패치를 할 수 있는 제한된 형태는 예외입니다.

또한 현재 Python 코드에서는 받은 인자의 타입을 검사하여 객체를 어떻게 처리할지 결정하는 것이 흔한 안티 패턴입니다. 예를 들어, 코드가 어떤 타입의 객체 하나나 그 타입 객체의 시퀀스를 모두 받고 싶어 할 수 있습니다.

현재 이를 위한 “명백한 방법”은 타입 검사이지만, 이 방법은 깨지기 쉽고 확장에 닫혀 있습니다. 이미 작성된 라이브러리를 사용하는 개발자는 그러한 코드가 자신의 객체를 다루는 방식을 바꾸지 못할 수 있으며, 사용하는 객체를 서드파티가 만든 경우에는 특히 그렇습니다.

따라서 이 PEP는 데코레이터와 인자 어노테이션(PEP 3107)을 사용하여 이러한 문제와 관련 문제를 해결하는 표준 라이브러리 모듈을 제안합니다. 제공할 주요 기능은 다음과 같습니다.

  • Java나 C++ 같은 언어의 정적 오버로딩과 비슷하지만, CLOS와 AspectJ에 있는 것과 같은 선택적 메서드 결합 기능을 포함하는 동적 오버로딩 기능.
  • Haskell의 타입 클래스에서 영감을 받은(하지만 더 동적이며 정적 타입 검사는 전혀 없는) 간단한 “인터페이스와 어댑테이션” 라이브러리. PyProtocols와 Zope에 있는 것과 같은 사용자 정의 인터페이스 타입을 등록할 수 있는 확장 API를 함께 제공합니다.
  • 상태를 가진 어댑터를 만들고 그 밖의 상태를 가진 AOP를 쉽게 할 수 있게 해 주는 간단한 “애스펙트” 구현.

이러한 기능은 확장된 구현을 만들어 사용할 수 있는 방식으로 제공되어야 합니다. 예를 들어, 라이브러리가 제네릭 함수를 위한 새로운 디스패치 기준과 새로운 종류의 인터페이스를 정의하고, 이를 미리 정의된 기능 대신 사용할 수 있어야 합니다. 예를 들어, zope.interface 패키지가 자신을 올바르게 등록했다면(또는 서드파티가 등록을 해 주었다면), zope.interface 인터페이스 객체를 사용하여 함수 인자에 원하는 타입을 지정할 수 있어야 합니다.

이런 식으로 제안하는 API는 모든 라이브러리, 프레임워크, 애플리케이션이 사용할 단일 구현을 규정하는 것이 아니라, 그 범위 안의 기능에 접근하는 일관된 방법을 제공할 뿐입니다.

사용자 API

오버로딩 API는 overloading이라는 이름의 단일 모듈로 구현되며, 다음 기능을 제공합니다.

오버로딩/제네릭 함수

@overload 데코레이터를 사용하면 인자 타입에 따라 특수화된 함수의 대체 구현을 정의할 수 있습니다. 같은 이름의 함수가 로컬 네임스페이스에 이미 존재해야 합니다. 데코레이터는 기존 함수를 그 자리에서 수정하여 새 구현을 추가하고, 수정된 함수를 반환합니다. 따라서 다음 코드는:

from overloading import overload
from collections import Iterable

def flatten(ob):
    """Flatten an object to its component iterables"""
    yield ob

@overload
def flatten(ob: Iterable):
    for o in ob:
        for ob in flatten(o):
            yield ob

@overload
def flatten(ob: basestring):
    yield ob

구현이 대략 다음과 같은 단일 flatten() 함수를 만듭니다.:

def flatten(ob):
    if isinstance(ob, basestring) or not isinstance(ob, Iterable):
        yield ob
    else:
        for o in ob:
            for ob in flatten(o):
                yield ob

다만 오버로딩으로 정의된 flatten() 함수는 오버로드를 더 추가하여 계속 확장할 수 있는 반면, 하드코딩된 버전은 확장할 수 없다는 점이 다릅니다.

예를 들어, 누군가 basestring의 서브클래스가 아닌 문자열 유사 타입으로 flatten()을 사용하려 한다면, 두 번째 구현으로는 방법이 없습니다. 그러나 오버로딩된 구현에서는 다음과 같이 작성하거나:

@overload
def flatten(ob: MyString):
    yield ob

(구현을 복사하지 않으려면) 다음과 같이 작성할 수 있습니다.:

from overloading import RuleSet
RuleSet(flatten).copy_rules((basestring,), (MyString,))

(또한 PEP 3119는 Iterable 같은 추상 베이스 클래스가 MyString 같은 클래스가 자신의 서브클래스임을 주장하도록 허용할 수 있어야 한다고 제안하지만, 그러한 주장은 애플리케이션 전체에 걸쳐 전역적이라는 점에 유의하십시오. 반면 특정 오버로드를 추가하거나 규칙을 복사하는 것은 개별 함수에 한정되므로, 원치 않는 부작용을 일으킬 가능성이 더 낮습니다.)

@overload 대 @when

@overload 데코레이터는 더 일반적인 @when 데코레이터를 흔한 경우에 맞게 줄여 쓴 것입니다. 대상 함수가 로컬 네임스페이스에 있어야 한다는 대가를 치르는 대신, 오버로딩하는 함수의 이름을 생략할 수 있게 해 줍니다. 또한 인자 어노테이션으로 지정한 조건 외에 추가 조건을 더하는 것은 지원하지 않습니다. 다음 함수 정의들은 이름 바인딩 부작용(아래에서 설명합니다)을 제외하면 동일한 효과를 냅니다.:

from overloading import when

@overload
def flatten(ob: basestring):
    yield ob

@when(flatten)
def flatten(ob: basestring):
    yield ob

@when(flatten)
def flatten_basestring(ob: basestring):
    yield ob

@when(flatten, (basestring,))
def flatten_basestring(ob):
    yield ob

위의 첫 번째 정의는 flatten을 이전에 바인딩되어 있던 대상에 바인딩합니다. 두 번째 정의도 해당 이름이 이미 when 데코레이터의 첫 번째 인자에 바인딩되어 있었다면 같은 동작을 합니다. flatten이 바인딩되지 않았거나 다른 것에 바인딩되어 있다면, 주어진 함수 정의에 다시 바인딩됩니다. 위의 마지막 두 정의는 항상 flatten_basestring을 주어진 함수 정의에 바인딩합니다.

이 방식을 사용하면 메서드에 설명적인 이름을 붙일 수 있고(트레이스백에서 유용한 경우가 많습니다!) 나중에 그 메서드를 재사용할 수도 있습니다.

달리 명시하지 않는 한, 모든 overloading 데코레이터는 @when과 같은 시그니처와 바인딩 규칙을 가집니다. 이들은 함수와 선택적인 “술어(predicate)” 객체를 받습니다.

기본 술어 구현은 오버로딩된 함수의 인자와 위치로 매칭되는 타입들의 튜플입니다. 그러나 Extension API를 사용하여 다른 종류의 술어를 얼마든지 만들고 등록할 수 있으며, 그러면 @when 및 이 모듈이 만든 다른 데코레이터(@before, @after, @around 등)와 함께 사용할 수 있습니다.

메서드 조합과 오버라이딩

오버로딩된 함수가 호출되면, 호출 인자와 가장 구체적으로 일치하는 시그니처를 가진 구현이 사용됩니다. 일치하는 구현이 없으면 NoApplicableMethods 에러가 발생합니다. 둘 이상의 구현이 일치하지만 어느 시그니처도 다른 시그니처보다 더 구체적이지 않으면 AmbiguousMethods 에러가 발생합니다.

예를 들어 다음 두 구현은 foo() 함수가 두 정수 인자로 호출되는 경우 모호합니다. 두 시그니처가 모두 적용되지만 어느 시그니처도 다른 쪽보다 더 구체적이지 않기 때문입니다(즉, 어느 쪽도 다른 쪽을 함의하지 않습니다).:

def foo(bar:int, baz:object):
    pass

@overload
def foo(bar:object, baz:int):
    pass

반면 다음 두 구현은 한 시그니처가 항상 다른 시그니처를 함의하므로 결코 모호할 수 없습니다. int/int 시그니처가 object/object 시그니처보다 더 구체적입니다.:

def foo(bar:object, baz:object):
    pass

@overload
def foo(bar:int, baz:int):
    pass

S1이 적용되는 모든 경우에 S2도 적용된다면, 시그니처 S1은 다른 시그니처 S2를 함축합니다. S1이 S2를 함축하지만 S2는 S1을 함축하지 않는다면, 시그니처 S1은 다른 시그니처 S2보다 “더 구체적”입니다.

위의 예제들은 모두 구체 타입이나 추상 타입을 인자 어노테이션으로 사용했지만, 어노테이션이 반드시 그러한 타입이어야 한다는 요구 사항은 없습니다. 어노테이션은 사용자 정의 인터페이스 타입을 포함하여 “인터페이스” 객체(Interfaces and Adaptation 절에서 다룹니다)일 수도 있습니다. (또한 Extension API를 통해 타입이 적절히 등록된 다른 객체일 수도 있습니다.)

“다음” 메서드로 진행하기

오버로드된 함수의 첫 번째 매개변수 이름이 __proceed__이면, 그 매개변수에는 그다음으로 가장 구체적인 메서드를 나타내는 호출 가능 객체가 전달됩니다. 예를 들어, 다음 코드는:

def foo(bar:object, baz:object):
    print "got objects!"

@overload
def foo(__proceed__, bar:int, baz:int):
    print "got integers!"
    return __proceed__(bar, baz)

“got integers!”(정수를 받음)를 출력한 다음 “got objects!”(객체를 받음)를 출력합니다.

그다음으로 가장 구체적인 메서드가 없으면, __proceed__는 NoApplicableMethods 인스턴스에 바인딩됩니다. 이 인스턴스를 호출하면, 첫 번째 인스턴스에 전달된 인자를 담은 새 NoApplicableMethods 인스턴스가 발생합니다.

마찬가지로, 그다음으로 가장 구체적인 메서드들 사이의 우선순위가 서로 모호하면 __proceed__는 AmbiguousMethods 인스턴스에 바인딩되며, 이를 호출하면 새 인스턴스가 발생합니다.

따라서 메서드는 __proceed__가 오류 인스턴스인지 검사하거나, 단순히 그것을 호출할 수 있습니다. NoApplicableMethods와 AmbiguousMethods 오류 클래스는 공통의 DispatchError 베이스 클래스를 가지므로, __proceed__를 안전하게 호출할 수 있는지 확인하려면 isinstance(__proceed__, overloading.DispatchError)만으로 충분합니다.

(구현 참고: __proceed__와 같은 마법 인자 이름을 사용하는 방식은 다음 메서드를 얻기 위해 호출하는 마법 함수로 대체될 수도 있습니다. 그러나 마법 함수는 성능을 떨어뜨리며, CPython이 아닌 플랫폼에서는 구현하기가 더 어려울 수 있습니다. 하지만 마법 인자 이름을 통한 메서드 체이닝은 함수로부터 바운드 메서드를 생성할 수 있는 모든 Python 플랫폼에서 효율적으로 구현할 수 있습니다 – 체이닝할 각 함수를, 그다음 함수나 오류를 바운드 메서드의 im_self로 삼아 재귀적으로 바인딩하기만 하면 됩니다.)

“Before” 메서드와 “After” 메서드

위에서 보인 단순한 다음 메서드 체이닝 외에도, 메서드를 조합하는 다른 방법이 유용할 때가 있습니다. 예를 들어, “옵저버 패턴”은 일반 구현의 앞이나 뒤에 실행되는 추가 메서드를 함수에 더하는 방식으로 구현할 수 있는 경우가 있습니다.

이러한 사용 사례를 지원하기 위해 overloading 모듈은 @before, @after, @around 데코레이터를 제공할 것이며, 이들은 Common Lisp 객체 시스템(Common Lisp Object System, CLOS)의 같은 종류의 메서드, 또는 AspectJ의 해당 “advice” 타입에 대략 대응합니다.

@when과 마찬가지로, 이 데코레이터들에는 모두 오버로드할 함수를 전달해야 하며, 선택적으로 술어(predicate)도 받을 수 있습니다:

from overloading import before, after

def begin_transaction(db):
    print "Beginning the actual transaction"

@before(begin_transaction)
def check_single_access(db: SingletonDB):
    if db.inuse:
        raise TransactionError("Database already in use")

@after(begin_transaction)
def start_logging(db: LoggableDB):
    db.set_log_level(VERBOSE)

@before와 @after 메서드는 주 함수 본문의 앞이나 뒤에 호출되며, 결코 모호한 것으로 간주되지 않습니다. 즉, 시그니처가 동일하거나 겹치는 “before” 또는 “after” 메서드가 여러 개 있더라도 아무런 오류가 발생하지 않습니다. 모호함은 메서드가 대상 함수에 추가된 순서를 사용하여 해소됩니다.

“Before” 메서드는 가장 구체적인 메서드부터 호출되며, 모호한 메서드들은 추가된 순서대로 실행됩니다. 모든 “before” 메서드는 함수의 “주(primary)” 메서드(즉, 일반 @overload 메서드)가 하나라도 실행되기 전에 호출됩니다.

“After” 메서드는 함수의 “주” 메서드가 모두 실행된 후에 역순으로 호출됩니다. 즉, 가장 덜 구체적인 메서드부터 실행되며, 모호한 메서드들은 추가된 순서의 역순으로 실행됩니다.

“before” 메서드와 “after” 메서드의 반환값은 모두 무시되며, 어떤 메서드(주 메서드든 다른 메서드든)에서든 잡히지 않은 예외가 발생하면 디스패치 과정이 즉시 종료됩니다. “Before” 메서드와 “after” 메서드는 다른 메서드를 호출할 책임이 없으므로 __proceed__ 인자를 가질 수 없습니다. 이 메서드들은 단지 주 메서드의 앞이나 뒤에 알림으로서 호출될 뿐입니다.

따라서 “before” 메서드와 “after” 메서드는 기존 기능을 중복 구현할 필요 없이 사전 조건을 검사하거나 설정하는 데(예: 조건이 충족되지 않으면 오류를 발생시키는 방식으로), 또는 사후 조건을 보장하는 데 사용할 수 있습니다.

“Around” 메서드

@around 데코레이터는 메서드를 “around” 메서드로 선언합니다. “Around” 메서드는 주 메서드와 매우 비슷하지만, 가장 덜 구체적인 “around” 메서드가 가장 구체적인 “before” 메서드보다 우선순위가 높다는 점이 다릅니다.

그러나 “before” 및 “after” 메서드와 달리, “Around” 메서드는 호출 과정을 계속 진행하기 위해 자신의 __proceed__ 인자를 호출할 책임이 있습니다. “Around” 메서드는 보통 입력 인자나 반환값을 변환하거나, 특정 경우를 특별한 오류 처리나 try/finally 조건으로 감싸는 데 사용합니다. 예를 들면 다음과 같습니다.:

from overloading import around

@around(commit_transaction)
def lock_while_committing(__proceed__, db: SingletonDB):
    with db.global_lock:
        return __proceed__(db)

또한 __proceed__ 함수를 호출하지 않는 방법으로 특정 경우의 일반적인 처리를 대체하는 데에도 사용할 수 있습니다.

“around” 메서드에 주어지는 __proceed__는 다음에 적용 가능한 “around” 메서드이거나, DispatchError 인스턴스이거나, 모든 “before” 메서드를 호출하고 이어서 주 메서드 체인을, 그다음 모든 “after” 메서드를 호출한 뒤 주 메서드 체인의 결과를 반환하는 합성 메서드 객체입니다.

따라서 일반 메서드와 마찬가지로, __proceed__가 DispatchError인지 검사하거나 단순히 호출할 수 있습니다. “around” 메서드는 함수 전체의 반환값을 수정하거나 다른 값으로 대체하려는 경우가 아니라면, __proceed__가 반환한 값을 반환해야 합니다.

사용자 정의 조합

위에서 설명한 데코레이터들(@overload, @when, @before, @after, @around)은 CLOS에서 “표준 메서드 조합(standard method combination)”이라고 부르는 것, 즉 메서드를 조합할 때 가장 흔히 사용되는 패턴을 함께 구현합니다.

그러나 때로는 애플리케이션이나 라이브러리에 더 정교한 유형의 메서드 조합이 필요할 수 있습니다. 예를 들어 주 메서드(들)가 반환한 값에서 뺄 할인율을 반환하는 “discount” 메서드를 만들고 싶다면, 다음과 같이 작성할 수 있습니다.:

from overloading import always_overrides, merge_by_default
from overloading import Around, Before, After, Method, MethodList

class Discount(MethodList):
    """Apply return values as discounts"""

    def __call__(self, *args, **kw):
        retval = self.tail(*args, **kw)
        for sig, body in self.sorted():
            retval -= retval * body(*args, **kw)
        return retval

# merge discounts by priority
merge_by_default(Discount)

# discounts have precedence over before/after/primary methods
always_overrides(Discount, Before)
always_overrides(Discount, After)
always_overrides(Discount, Method)

# but not over "around" methods
always_overrides(Around, Discount)

# Make a decorator called "discount" that works just like the
# standard decorators...
discount = Discount.make_decorator('discount')

# and now let's use it...
def price(product):
    return product.list_price

@discount(price)
def ten_percent_off_shoes(product: Shoe)
    return Decimal('0.1')

비슷한 기법을 사용하여 매우 다양한 CLOS 스타일의 메서드 한정자와 조합 규칙을 구현할 수 있습니다. 사용자 정의 메서드 조합 객체와 그에 대응하는 데코레이터를 만드는 과정은 Extension API 절에서 더 자세히 설명합니다.

참고로, 위에 보인 @discount 데코레이터는 다른 코드가 정의한 새로운 술어(predicate)와도 올바르게 동작한다는 점에 유의하십시오. 예를 들어 zope.interface가 자신의 인터페이스 타입을 인자 어노테이션으로 올바르게 동작하도록 등록한다면, 클래스나 overloading에서 정의한 인터페이스 타입뿐만 아니라 그 인터페이스 타입을 기준으로도 할인을 지정할 수 있을 것입니다.

마찬가지로 RuleDispatch나 PEAK-Rules 같은 라이브러리가 적절한 술어 구현과 디스패치 엔진을 등록한다면, 그 술어들도 할인에 사용할 수 있을 것입니다. 예를 들면 다음과 같습니다.:

from somewhere import Pred  # some predicate implementation

@discount(
    price,
    Pred("isinstance(product,Shoe) and"
         " product.material.name=='Blue Suede'")
)
def forty_off_blue_suede_shoes(product):
    return Decimal('0.4')

사용자 정의 술어 타입과 디스패치 엔진을 정의하는 과정도 Extension API 절에서 더 자세히 설명합니다.

클래스 내부에서의 오버로딩

위의 모든 데코레이터는 클래스 본문 안에서 직접 호출될 때 특별한 추가 동작을 합니다. 즉, 데코레이트된 함수의 첫 번째 매개변수(__proceed__가 있다면 그것을 제외하고)는 그 함수가 정의된 클래스와 같은 어노테이션을 가진 것처럼 취급됩니다.

즉, 다음 코드는:

class And(object):
    # ...
    @when(get_conjuncts)
    def __conjuncts(self):
        return self.conjuncts

다음 코드와 같은 효과를 냅니다(비공개 메서드가 존재한다는 점은 제외합니다).:

class And(object):
    # ...

@when(get_conjuncts)
def get_conjuncts_of_and(ob: And):
    return ob.conjuncts

이 동작은 많은 메서드를 정의할 때 편의를 높여 주는 개선 사항이면서, 동시에 서브클래스에서 여러 인자를 받는 오버로드를 안전하게 구별하기 위한 필수 요건이기도 합니다. 예를 들어 다음 코드를 생각해 보십시오.:

class A(object):
    def foo(self, ob):
        print "got an object"

    @overload
    def foo(__proceed__, self, ob:Iterable):
        print "it's iterable!"
        return __proceed__(self, ob)


class B(A):
    foo = A.foo     # foo must be defined in local namespace

    @overload
    def foo(__proceed__, self, ob:Iterable):
        print "B got an iterable!"
        return __proceed__(self, ob)

암시적 클래스 규칙으로 인해 B().foo([])를 호출하면 “B got an iterable!”(B가 이터러블을 받음)을 출력한 다음 “it’s iterable!”(이터러블임)을 출력하고, 마지막으로 “got an object”(객체를 받음)를 출력하는 반면, A().foo([])는 A에 정의된 메시지만 출력합니다.

반대로 암시적 클래스 규칙이 없다면 두 “Iterable”(이터러블) 메서드는 완전히 같은 적용 조건을 가지게 되므로, A().foo([])나 B().foo([]) 중 어느 것을 호출하더라도 AmbiguousMethods 오류가 발생합니다.

Python 3.0에서 이 규칙을 구현하는 가장 좋은 방법을 정하는 것은 현재 미해결 문제입니다. Python 2.x에서는 클래스 본문이 끝날 때까지 클래스의 메타클래스가 선택되지 않았으므로, 데코레이터가 이런 종류의 처리를 하는 사용자 정의 메타클래스를 삽입할 수 있었습니다. (예를 들어 RuleDispatch가 암묵적 클래스 규칙을 구현하는 방식이 바로 이것입니다.)

그러나 PEP 3115는 클래스 본문이 실행되기 전에 클래스의 메타클래스가 결정되도록 요구하므로, 이 기법을 더 이상 클래스 데코레이션에 사용할 수 없게 되었습니다.

이 글을 쓰는 시점에 이 문제에 대한 논의가 계속 진행 중입니다.

인터페이스와 어댑테이션

overloading 모듈은 인터페이스와 어댑테이션의 간단한 구현을 제공합니다. 다음 예제는 IStack 인터페이스를 정의하고, list 객체가 이를 지원한다고 선언합니다.:

from overloading import abstract, Interface

class IStack(Interface):
    @abstract
    def push(self, ob)
        """Push 'ob' onto the stack"""

    @abstract
    def pop(self):
        """Pop a value and return it"""


when(IStack.push, (list, object))(list.append)
when(IStack.pop, (list,))(list.pop)

mylist = []
mystack = IStack(mylist)
mystack.push(42)
assert mystack.pop()==42

Interface 클래스는 일종의 “범용 어댑터”입니다. 이 클래스는 어댑트할 객체 하나를 유일한 인자로 받습니다. 그런 다음 자신의 모든 메서드를 자기 자신 대신 대상 객체에 바인딩합니다. 따라서 mystack.push(42)를 호출하는 것은 IStack.push(mylist, 42)를 호출하는 것과 같습니다.

@abstract 데코레이터는 함수를 추상 함수, 즉 구현이 없는 함수로 표시합니다. @abstract 함수가 호출되면 NoApplicableMethods를 발생시킵니다. 이 함수를 실행할 수 있게 하려면 앞에서 설명한 기법으로 오버로드된 메서드를 추가해야 합니다. (즉, @when, @before, @after, @around 또는 임의의 사용자 정의 메서드 조합 데코레이터를 사용하여 메서드를 추가할 수 있습니다.)

위 예제에서 list.append 메서드는 인자가 리스트와 임의의 객체일 때 IStack.push()의 메서드로 추가됩니다. 따라서 IStack.push(mylist, 42)는 list.append(mylist, 42)로 변환되어 원하는 연산을 구현합니다.

추상 메서드와 구체 메서드

덧붙여 @abstract 데코레이터는 인터페이스 정의에서만 쓸 수 있는 것이 아니라는 점에 유의하십시오. 처음에 메서드가 없는 “빈” 제네릭 함수를 만들고 싶은 곳이라면 어디에서나 사용할 수 있습니다. 특히 클래스 안에서 사용하지 않아도 됩니다.

또한 인터페이스 메서드가 반드시 추상 메서드일 필요는 없다는 점에 유의하십시오. 예를 들어 다음과 같은 인터페이스를 작성할 수도 있습니다:

class IWriteMapping(Interface):
    @abstract
    def __setitem__(self, key, value):
        """This has to be implemented"""

    def update(self, other:IReadMapping):
        for k, v in IReadMapping(other).items():
            self[k] = v

어떤 타입에 대해 __setitem__이 정의되어 있기만 하면, 위 인터페이스는 사용 가능한 update() 구현을 제공합니다. 하지만 어떤 특정 타입(또는 타입 쌍)에 update() 연산을 더 효율적으로 처리하는 방법이 있다면, 그 경우에 사용할 적절한 오버로드를 여전히 등록할 수 있습니다.

서브클래싱과 재조립

인터페이스는 서브클래싱할 수 있습니다:

class ISizedStack(IStack):
    @abstract
    def __len__(self):
        """Return the number of items on the stack"""

# define __len__ support for ISizedStack
when(ISizedStack.__len__, (list,))(list.__len__)

또는 기존 인터페이스의 함수들을 조합하여 조립할 수도 있습니다:

class Sizable(Interface):
    __len__ = ISizedStack.__len__

# list now implements Sizable as well as ISizedStack, without
# making any new declarations!

어떤 시점에 한 클래스의 인스턴스에 대해 호출했을 때 NoApplicableMethods 에러를 반드시 발생시키는 메서드가 인터페이스에 하나도 정의되어 있지 않다면, 그 시점에 그 클래스는 해당 인터페이스에 “어댑트된다”고 볼 수 있습니다.

하지만 일반적인 사용에서는 “허락을 구하는 것보다 용서를 구하는 것이 더 쉽습니다”. 즉, 객체가 인터페이스에 어댑트될 수 있는지(또는 인터페이스를 직접 구현하는지) 알아내려 하기보다는, 객체를 인터페이스에 어댑트하거나(예: IStack(mylist)) 인터페이스 메서드를 직접 호출하여(예: IStack.push(mylist, 42)) 그냥 객체에 인터페이스를 사용하는 편이 더 쉽습니다.

클래스에서 인터페이스 구현하기

declare_implementation() 함수를 사용하여 클래스가 인터페이스를 직접 구현한다고 선언할 수 있습니다:

from overloading import declare_implementation

class Stack(object):
    def __init__(self):
        self.data = []
    def push(self, ob):
        self.data.append(ob)
    def pop(self):
        return self.data.pop()

declare_implementation(IStack, Stack)

위의 declare_implementation() 호출은 대략 다음 단계와 같습니다:

when(IStack.push, (Stack,object))(lambda self, ob: self.push(ob))
when(IStack.pop, (Stack,))(lambda self, ob: self.pop())

즉, Stack의 어떤 서브클래스 인스턴스에 대해 IStack.push()나 IStack.pop()을 호출하면, 단순히 그 인스턴스의 실제 push() 또는 pop() 메서드에 위임합니다.

효율성을 위해, s가 Stack의 인스턴스일 때 IStack(s)를 호출하면 IStack 어댑터 대신 s를 반환할 수도 있습니다. (x가 이미 IStack 어댑터일 때 IStack(x)를 호출하면 항상 x를 변경 없이 그대로 반환한다는 점에 유의하십시오. 이는 어댑트 대상이 어댑트 없이 인터페이스를 직접 구현한다고 알려진 경우에 허용되는 추가 최적화입니다.)

편의상 클래스 헤더에서 구현을 선언하는 것이 유용할 수 있습니다. 예를 들면 다음과 같습니다.:

class Stack(metaclass=Implementer, implements=IStack):
    ...

이렇게 하면 스위트가 끝난 뒤에 declare_implementation()을 호출하지 않아도 됩니다.

타입 지정자로서의 인터페이스

Interface 서브클래스는 인자 어노테이션으로 사용하여 오버로드가 어떤 타입의 객체를 받을 수 있는지 나타낼 수 있습니다. 예를 들면 다음과 같습니다.:

@overload
def traverse(g: IGraph, s: IStack):
    g = IGraph(g)
    s = IStack(s)
    # etc....

하지만 인터페이스를 타입 지정자로 사용하는 것만으로는 실제 인자가 어떤 식으로든 변경되거나 어댑트되지 않는다는 점에 유의하십시오. 위에서 보인 것처럼 객체를 적절한 인터페이스로 명시적으로 캐스트해야 합니다.

하지만 인터페이스를 다른 방식으로 사용할 수도 있다는 점에 유의하십시오. 예를 들어, 다른 인터페이스 구현은 어댑테이션을 지원하지 않을 수도 있고, 함수 인자가 지정된 인터페이스에 이미 어댑트되어 있을 것을 요구할 수도 있습니다. 따라서 인터페이스를 타입 지정자로 사용할 때의 정확한 의미는 실제로 사용하는 인터페이스 객체에 따라 달라집니다.

하지만 이 PEP에서 정의하는 인터페이스 객체의 경우, 그 의미는 위에서 설명한 것과 같습니다. I1의 상속 계층에 있는 디스크립터 집합이 I2의 상속 계층에 있는 디스크립터 집합의 진상위집합이면, 인터페이스 I1은 다른 인터페이스 I2보다 “더 구체적”인 것으로 간주됩니다.

그러므로 예를 들어, 이 인터페이스들 사이의 상속 관계와 관계없이 ISizedStack은 ISizable과 ISizedStack 모두보다 더 구체적입니다. 이는 순전히 해당 인터페이스들 안에 어떤 연산이 포함되어 있는가의 문제이며, 연산의 이름은 중요하지 않습니다.

인터페이스(적어도 overloading이 제공하는 인터페이스)는 항상 구체 클래스보다 덜 구체적인 것으로 간주됩니다. 다른 인터페이스 구현은 인터페이스와 다른 인터페이스 사이, 그리고 인터페이스와 클래스 사이의 구체성 규칙을 스스로 정할 수 있습니다.

인터페이스의 메서드가 아닌 속성

Interface 구현은 실제로 모든 속성과 메서드(즉, 디스크립터)를 같은 방식으로 다룹니다. 이들의 __get__(그리고 존재한다면 __set__과 __delete__) 메서드는 래핑된(어댑트된) 객체를 “self”로 하여 호출됩니다. 함수의 경우, 이는 제네릭 함수를 래핑된 객체에 연결하는 바운드 메서드를 만드는 효과가 있습니다.

함수가 아닌 속성은 property 내장 함수와 그에 대응하는 fget, fset, fdel 속성을 사용하여 지정하는 것이 가장 쉬울 수 있습니다:

class ILength(Interface):
    @property
    @abstract
    def length(self):
        """Read-only length attribute"""

# ILength(aList).length == list.__len__(aList)
when(ILength.length.fget, (list,))(list.__len__)

또는 _get_foo()와 _set_foo() 같은 메서드를 인터페이스의 일부로 정의하고 그 메서드들을 이용해 프로퍼티를 정의할 수도 있습니다. 하지만 이렇게 하면 인터페이스를 직접 구현하는 클래스를 만들 때 사용자가 프로퍼티나 속성의 이름만이 아니라 개별 메서드 이름을 모두 맞춰야 하므로, 올바르게 구현하기가 조금 더 어렵습니다.

애스펙트

위에서 설명한 어댑테이션 시스템은 어댑터가 “상태가 없다”고 가정합니다. 즉, 어댑터는 어댑트된 객체의 속성이나 상태 외에는 자신만의 속성이나 상태를 갖지 않는다는 뜻입니다. 이는 Haskell의 “타입클래스/인스턴스” 모델과 “순수한”(즉, 추이적으로 합성 가능한) 어댑터라는 개념을 따른 것입니다.

하지만 어떤 인터페이스를 완전하게 구현하기 위해 일종의 추가 상태가 필요한 경우가 가끔 있습니다.

물론 한 가지 방법은 어댑티에 몽키패치로 “비공개” 속성을 붙이는 것입니다. 하지만 이 방법은 이름 충돌이 일어날 수 있고, 초기화 과정을 복잡하게 만듭니다(이 속성들을 사용하는 모든 코드가 속성이 존재하는지 확인하고 필요하면 초기화해야 하기 때문입니다). 또한 __dict__ 속성이 없는 객체에서는 동작하지 않습니다.

그래서 다음 중 하나에 해당하는 객체에 추가 정보를 쉽게 붙일 수 있도록 Aspect 클래스가 제공됩니다.

  1. __dict__ 속성을 가지고 있거나(이 경우 애스펙트 인스턴스를 애스펙트 클래스를 키로 하여 그 안에 저장할 수 있습니다),
  2. 약한 참조를 지원하거나(이 경우 전역적이면서도 스레드 안전한 약한 참조 딕셔너리를 사용해 애스펙트 인스턴스를 관리할 수 있습니다),
  3. overloading.IAspectOwner 인터페이스를 구현하거나 그 인터페이스로 어댑트될 수 있는 객체(엄밀히 말하면 #1이나 #2이면 이 조건도 충족됩니다).

Aspect를 서브클래싱하면 상태가 어댑트된 객체의 수명에 묶이는 어댑터 클래스가 만들어집니다.

예를 들어, Target의 인스턴스에서 특정 메서드가 호출되는 횟수를 모두 세고 싶다고 가정해 봅시다(고전적인 AOP 예제입니다). 다음과 같이 할 수 있습니다:

from overloading import Aspect

class Count(Aspect):
    count = 0

@after(Target.some_method)
def count_after_call(self:Target, *args, **kw):
    Count(self).count += 1

위 코드는 Target의 인스턴스에서 Target.some_method()가 성공적으로 호출된 횟수를 추적합니다(즉, 오류는 더 구체적인 “after” 메서드에서 발생하지 않는 한 세지 않습니다). 그러면 다른 코드에서 Count(someTarget).count를 사용하여 그 횟수에 접근할 수 있습니다.

Aspect 인스턴스는 물론 자료 구조를 초기화하기 위해 __init__ 메서드를 가질 수 있습니다. 저장 공간으로는 __slots__나 딕셔너리 기반 속성 중 하나를 사용할 수 있습니다.

이 기능은 AspectJ처럼 모든 기능을 갖춘 AOP 도구에 비하면 다소 원시적이지만, 포인트컷 라이브러리나 그 밖의 AspectJ 같은 기능을 만들고자 하는 사람은 Aspect 객체와 메서드 결합 데코레이터를 기반으로 더 표현력 있는 AOP 도구를 충분히 만들 수 있습니다.

XXX 키, N 대 1 애스펙트, 수동 등을 포함한 전체 애스펙트 API를 명세할 것
애스펙트 인스턴스의 attach/detach/delete, 그리고 IAspectOwner 인터페이스를 포함하여 전체 애스펙트 API의 명세를 작성해야 합니다.

확장 API

TODO: 이 모든 것이 어떻게 동작하는지 설명해야 합니다.

implies(o1, o2)

declare_implementation(iface, class)

predicate_signatures(ob)

parse_rule(ruleset, body, predicate, actiontype, localdict, globaldict)

combine_actions(a1, a2)

rules_for(f)

Rule 객체

ActionDef 객체

RuleSet 객체

Method 객체

MethodList 객체

IAspectOwner

오버로딩 사용 패턴

Python-3000 메일링 리스트의 논의에서, 임의의 함수를 오버로드할 수 있게 하자는 기능 제안은 다소 논란이 되었으며, 일부 사람들은 이로 인해 프로그램을 이해하기가 더 어려워질 것이라는 우려를 표했습니다.

이 인자의 요지는, 함수가 언제든 프로그램의 어디에서나 변경될 수 있다면 그 함수가 무엇을 하는지 신뢰할 수 없다는 것입니다. 원칙적으로는 몽키패칭이나 코드 치환을 통해 이미 이런 일이 일어날 수 있지만, 그렇게 하는 것은 좋지 않은 관행으로 여겨집니다.

그러나 (이 인자에 따르면) 어떤 함수든 오버로딩할 수 있도록 지원하는 것은 그러한 변경을 허용 가능한 관행으로 암묵적으로 승인하는 것입니다.

이 인자는 이론상으로는 타당해 보이지만, 실제로는 두 가지 이유로 거의 완전히 무의미해집니다.

첫째, 사람들은 대개 한 곳에서는 함수가 어떤 일을 하도록 정의해 놓고 다른 곳에서는 대뜸 그 반대의 일을 하도록 정의할 만큼 비뚤어져 있지 않습니다! 특별히 제네릭으로 만들어지지 않은 함수의 동작을 확장하는 주된 이유는 다음과 같습니다.

  • 추가 타입 지원처럼 원래 함수의 작성자가 고려하지 않은 특수한 경우를 추가하기 위해서입니다.
  • 어떤 동작에 대해 통지를 받아, 원래 작업이 수행되기 전이나 후, 또는 양쪽 모두에 관련 작업이 수행되도록 하기 위해서입니다. 여기에는 애플리케이션 고유의 동작뿐만 아니라 로깅, 시간 측정, 추적 추가와 같은 범용 작업도 포함될 수 있습니다.

그러나 오버로드를 추가하는 이러한 이유 중 어느 것도 기존 함수의 의도된 기본 동작이나 전반적인 동작을 바꾸는 것을 의미하지는 않습니다. 베이스 클래스 메서드가 바로 이 두 가지 이유로 서브클래스에서 오버라이드될 수 있는 것처럼, 함수 역시 그러한 개선을 제공하기 위해 오버로딩될 수 있습니다.

다시 말해, 사람들이 기존 함수의 동작을 비논리적이거나 예측할 수 없는 방식으로 마구잡이로 재정의할 것이라고 예상할 필요가 없다는 의미에서, 보편적 오버로딩은 임의적인 오버로딩과 같지 않습니다. 만약 그렇게 한다면, 그것은 비논리적이거나 예측할 수 없는 코드를 작성하는 다른 어떤 방식 못지않게 나쁜 관행일 것입니다!

그러나 나쁜 관행과 좋은 관행을 구별하려면, 오버로드를 정의하는 좋은 관행이 무엇인지 좀 더 명확히 할 필요가 있을 것입니다. 그리고 이것이 제네릭 함수가 반드시 프로그램을 이해하기 어렵게 만들지는 않는 두 번째 이유로 이어집니다. 실제 프로그램의 오버로딩 패턴은 매우 예측 가능한 패턴을 따르는 경향이 있습니다. (Python에서도, 비제네릭 함수가 없는 언어에서도 마찬가지입니다.)

모듈이 새로운 제네릭 연산을 정의한다면, 대개 기존 타입에 필요한 오버로드도 같은 곳에서 함께 정의합니다. 마찬가지로 모듈이 새로운 타입을 정의한다면, 대개 그 모듈이 알고 있거나 관심을 두는 제네릭 함수에 대한 오버로드를 그곳에서 정의합니다.

결과적으로 대다수의 오버로드는 오버로드되는 함수 옆이나, 오버로드가 지원을 추가하는 새로 정의된 타입 옆에서 찾을 수 있습니다. 따라서 일반적인 경우 오버로드는 매우 쉽게 찾을 수 있습니다. 함수나 타입, 또는 둘 다를 보고 있을 것이기 때문입니다.

함수도, 오버로드가 추가되는 대상 타입도 포함하지 않는 모듈에 오버로드가 있는 경우는 상당히 드뭅니다. 예를 들어 서드파티가 한 라이브러리의 타입과 다른 라이브러리의 제네릭 함수 사이를 연결하는 지원 브리지를 만든 경우가 이에 해당합니다. 그러나 이러한 경우에는 이 사실을 눈에 띄게 알리는 것이 모범 관행이며, 특히 모듈 이름을 통해 알리는 것이 좋습니다.

예를 들어 PyProtocols는 protocols.twisted_support와 protocols.zope_support라는 모듈을 사용하여 Zope 인터페이스 및 레거시 Twisted 인터페이스와 함께 동작하기 위한 이러한 브리지 지원을 정의합니다. (이 브리지들은 제네릭 함수가 아니라 인터페이스 어댑터로 구현되어 있지만, 기본 원리는 같습니다.)

요컨대 대다수의 오버로드는 함수 옆이나 그 함수에 전달되는 타입의 정의 옆에 있을 것이므로, 보편적 오버로딩이 존재한다고 해서 프로그램을 이해하기가 더 어려워질 필요는 없습니다.

그리고 무능하거나 의도적으로 알아보기 어렵게 만들려는 경우가 아니라면, 관련 타입이나 함수 옆에 있지 않은 소수의 오버로드는 일반적으로 해당 오버로드가 정의된 범위 밖에서 이해하거나 알 필요가 없습니다. (단, “지원 모듈”의 경우는 예외이며, 이 경우에는 모범 관행에 따라 그에 맞게 이름을 지어야 합니다.)

구현 참고 사항

이 PEP에서 설명하는 기능의 대부분은 개발 중인 PEAK-Rules 프레임워크 버전에 이미 구현되어 있습니다. 특히 기본적인 오버로딩 및 메서드 조합 프레임워크(@overload 데코레이터 제외)는 이미 그곳에 존재합니다. 이 글을 쓰는 시점에서 peak.rules.core에 있는 이 모든 기능의 구현은 Python 코드 656줄입니다.

peak.rules.core는 현재 DecoratorTools와 BytecodeAssembler 모듈에 의존하지만, 이 두 의존성은 모두 대체할 수 있습니다. DecoratorTools는 주로 Python 2.3 호환성과 구조체 타입 구현(이후 버전의 Python에서는 이름 있는 튜플로 할 수 있습니다)을 위해 사용되기 때문입니다. BytecodeAssembler의 사용은 적절한 노력을 들이면 “exec”나 “compile” 우회 방법으로 대체할 수 있습니다. (함수 객체의 func_closure 속성에 쓰기가 가능하다면 이 작업이 더 쉬울 것입니다.)

Interface 클래스는 이전에 프로토타입으로 만들어진 적이 있지만, 현재 PEAK-Rules에는 포함되어 있지 않습니다.

“암묵적 클래스 규칙”은 이전에 RuleDispatch 라이브러리에서 구현된 적이 있습니다. 그러나 이는 현재 PEP 3115에서 제거된 __metaclass__ 훅에 의존합니다.

클래스 본문에서 @overload가 classmethod 및 staticmethod와 원활하게 동작하도록 만드는 방법은 현재로서는 알지 못합니다. 하지만 그렇게 할 필요가 있는지도 사실 분명하지 않습니다.