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 코드에서 수신한 인자의 유형을 검사하여 객체에 대해 무엇을 수행할지 결정하는 것은 흔한 안티패턴입니다. 예를 들어 코드는 어떤 유형의 객체 하나 또는 해당 유형의 객체 시퀀스를 받아들이려고 할 수 있습니다.

현재 이를 수행하는 “분명한 방법”은 유형 검사이지만, 이 방법은 취약하고 확장에 닫혀 있습니다. 이미 작성된 라이브러리를 사용하는 개발자는 그러한 코드가 자신의 객체를 처리하는 방식을 변경하지 못할 수 있으며, 특히 사용 중인 객체가 제3자에 의해 생성된 경우에는 더욱 그렇습니다.

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

  • Java 및 C++와 같은 언어에서 볼 수 있는 정적 오버로딩과 유사한 동적 오버로딩 기능을 제공하지만, CLOS 및 AspectJ에서 볼 수 있는 선택적 메서드 결합 기능도 포함합니다.
  • Haskell의 타입클래스에서 영감을 얻은 간단한 “인터페이스 및 적응” 라이브러리를 제공합니다(더 동적이며 정적 유형 검사는 없음). 또한 PyProtocols와 Zope에서 볼 수 있는 사용자 정의 인터페이스 유형을 등록할 수 있도록 확장 API를 제공합니다.
  • 상태를 유지하는 어댑터를 쉽게 만들고 기타 상태 유지 AOP를 수행할 수 있는 간단한 “애스펙트” 구현을 제공합니다.

이러한 기능은 확장된 구현을 만들고 사용할 수 있는 방식으로 제공됩니다. 예를 들어 라이브러리가 제너릭 함수에 대한 새로운 디스패치 기준과 새로운 종류의 인터페이스를 정의하고, 이를 미리 정의된 기능 대신 사용할 수 있어야 합니다. 예를 들어 zope.interface패키지가 올바르게 등록했거나(또는 제3자가 등록했다면), 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 3119Iterable과 같은 추상 베이스 클래스가 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” 객체를 받습니다.

기본 predicate 구현은 오버로드된 함수의 인자에 위치 기반으로 매칭되는 형식들의 튜플입니다. 그러나 Extension API를 사용하여 임의의 수의 다른 종류의 predicate를 생성하고 등록할 수 있으며, 그러면 @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 를 통해 적절히 등록된 다른 객체일 수도 있습니다.)

“Next” 메서드로 진행하기

오버로드된 함수의 첫 번째 매개변수 이름이 __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__가 오류 인스턴스인지 확인하거나, 단순히 이를 호출할 수 있습니다. NoApplicableMethodsAmbiguousMethods 오류 클래스는 공통 DispatchError 베이스 클래스를 가지므로, isinstance(__proceed__, overloading.DispatchError)__proceed__를 안전하게 호출할 수 있는지 확인할 수 있습니다.

(구현 참고: __proceed__와 같은 매직 인자 이름은 다음 메서드를 얻기 위해 호출되는 매직 함수로 대체할 수도 있습니다. 그러나 매직 함수는 성능을 저하하며 CPython이 아닌 플랫폼에서는 구현하기가 더 어려울 수 있습니다. 반면 매직 인자 이름을 통한 메서드 연결은 함수에서 바인딩된 메서드를 생성할 수 있는 모든 Python 플랫폼에서 효율적으로 구현할 수 있습니다. 즉, 다음 함수 또는 오류를 바인딩된 메서드의 im_self 로 사용하여 연결할 각 함수를 재귀적으로 바인딩하면 됩니다.)

“Before” 및 “After” 메서드

위에서 보인 간단한 다음 메서드 연결 외에도, 메서드를 결합하는 다른 방법이 유용한 경우가 있습니다. 예를 들어, “옵서버 패턴”은 일반 구현 전이나 후에 실행되는 추가 메서드를 함수에 추가하여 구현할 수 있는 경우가 있습니다.

이러한 사용 사례를 지원하기 위해 overloading 모듈은 @before, @after@around 데코레이터를 제공합니다. 이는 대략 Common Lisp Object System (CLOS)의 동일한 유형의 메서드나 AspectJ의 해당 “advice” 유형에 해당합니다.

@when과 마찬가지로 이러한 모든 데코레이터에는 오버로드할 함수가 전달되어야 하며, 조건자도 선택적으로 받을 수 있습니다.:

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” 메서드는 모든 함수의 “primary” 메서드가 실행된 후 역순으로 호출됩니다. 즉, 가장 덜 구체적인 메서드부터 실행되며, 모호한 메서드는 추가된 순서의 역순으로 실행됩니다.

“before” 및 “after” 메서드의 반환 값은 모두 무시되며, any 메서드(기본 메서드 또는 기타 메서드)가 발생시킨 포착되지 않은 예외는 디스패치 프로세스를 즉시 종료합니다. “Before” 및 “after” 메서드는 다른 메서드를 호출할 책임이 없으므로 __proceed__인자를 가질 수 없습니다. 이러한 메서드는 primary 메서드 전이나 후에 알림 목적으로 단순히 호출됩니다.

따라서 “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에서 “표준 메서드 조합”이라고 부르는 것, 즉 메서드를 조합할 때 사용되는 가장 일반적인 패턴을 구현합니다.

그러나 때로는 애플리케이션이나 라이브러리에서 더 정교한 유형의 메서드 조합이 필요할 수 있습니다. 예를 들어, 주 메서드가 반환한 값에서 차감할 백분율을 반환하는 “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 데코레이터는 다른 코드에서 정의한 새로운 술어와도 올바르게 작동합니다. 예를 들어 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!”이 출력된 다음 “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())

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

효율성을 위해 sStack의 인스턴스인 경우 IStack(s)를 호출하면 IStack 어댑터가 아니라 s반환할 수도 있습니다. (이미 IStack 어댑터인 x에 대해 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....

그러나 인터페이스를 타입 지정자로 사용하기만 한다고 해서 실제 인자가 어떤 방식으로든 변경되거나 적응되는 것은 not 아닙니다. 위에 보인 것처럼 객체를 적절한 인터페이스로 명시적으로 캐스팅해야 합니다.

그러나 다른 인터페이스 사용 패턴도 가능합니다. 예를 들어, 다른 인터페이스 구현은 어댑테이션을 지원하지 않거나, 함수 인자가 지정된 인터페이스에 이미 어댑트되어 있어야 할 수도 있습니다. 따라서 인터페이스를 타입 지정자로 사용하는 정확한 의미 체계는 실제로 사용하는 인터페이스 객체에 따라 달라집니다.

그러나 이 PEP에서 정의하는 인터페이스 객체의 경우 의미 체계는 위에서 설명한 것과 같습니다. 인터페이스 I1의 상속 계층에 있는 디스크립터 집합이 인터페이스 I2의 상속 계층에 있는 디스크립터 집합의 진부분집합을 포함하는 경우, 인터페이스 I1은 다른 인터페이스 I2보다 “더 구체적”이라고 간주합니다.

따라서 예를 들어 ISizedStack은 이 인터페이스들 사이의 상속 관계와 관계없이 ISizableISizedStack모두보다 더 구체적입니다. 이는 순전히 해당 인터페이스에 어떤 연산이 포함되어 있는지에 대한 문제이며, 연산의 names은 중요하지 않습니다.

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

인터페이스의 비메서드 속성

Interface구현은 실제로 모든 속성과 메서드(즉, 디스크립터)를 동일한 방식으로 처리합니다. 즉, 해당 객체를 래핑한 (어댑트된) 객체를 “self”로 하여 해당 객체의 __get__ (그리고 존재하는 경우 __set____delete__) 메서드를 호출합니다. 함수의 경우 이는 제너릭 함수를 래핑된 객체에 연결하는 바운드 메서드를 생성하는 효과가 있습니다.

함수가 아닌 속성의 경우 내장 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-to-1 애스펙트, 매뉴얼을 포함한 전체 애스펙트 API 명세를 작성합니다.
애스펙트 인스턴스의 attach/detach/delete 및 IAspectOwner 인터페이스입니다.

확장 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과 제네릭 함수가 없는 언어 모두에서 그렇습니다.)

모듈이 새로운 제네릭 연산을 정의하는 경우, 일반적으로 같은 위치에서 기존 타입에 필요한 오버로드도 정의합니다. 마찬가지로 모듈이 새로운 타입을 정의하는 경우, 일반적으로 해당 모듈이 알고 있거나 관심을 두는 제네릭 함수에 대한 오버로드도 그곳에서 정의합니다.

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

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

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

요컨대 보편적 오버로딩이 존재하더라도 프로그램을 이해하는 일이 더 어려워질 필요는 없습니다. 대부분의 오버로드가 함수나 해당 함수에 전달되는 타입의 정의 바로 옆에 있기 때문입니다.

무능력이나 의도적으로 모호하게 만들려는 의도가 없는 한, 관련 타입이나 함수 바로 옆에 있지 않은 소수의 오버로드는 일반적으로 해당 오버로드가 정의된 범위 밖에서 이해하거나 알 필요가 없습니다. (모범 관행상 이에 맞게 이름을 지정하는 “지원 모듈”의 경우는 제외합니다.)

구현 참고 사항

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

peak.rules.core는 현재 DecoratorTools 및 BytecodeAssembler 모듈에 의존하지만, DecoratorTools는 주로 Python 2.3 하위 호환성을 위해 사용되고 구조 타입을 구현하기 위해 사용되므로 이 두 의존성 모두를 대체할 수 있습니다. 구조 타입은 이후 버전의 Python에서 명명된 튜플로 구현할 수 있습니다. 합리적인 노력을 기울이면 “exec” 또는 “compile” 우회 방법을 사용하여 BytecodeAssembler의 사용을 대체할 수 있습니다. (함수 객체의 func_closure속성을 쓸 수 있었다면 이를 구현하기가 더 쉬웠을 것입니다.)

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

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

현재로서는 클래스 본문에서 @overloadclassmethodstaticmethod와 원활하게 작동하도록 만드는 방법을 알지 못합니다. 그러나 그렇게 해야 하는지 여부도 실제로 명확하지 않습니다.