[Deferred] PEP 3124 - Overloading, Generic Functions, Interfaces, and Adaptation

원문 링크: PEP 3124 - Overloading, Generic Functions, Interfaces, and Adaptation

상태: Deferred 유형: Standards Track 작성일: 28-Apr-2007

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

  • 작성자: Phillip J. Eby
  • 논의처: Python-3000 list
  • 상태: Deferred (보류됨)
  • 유형: Standards Track
  • 요구사항: PEP 3107, 3115, 3119
  • 생성일: 2007년 4월 28일
  • 교체 대상: PEP 245, 246

개요 (Abstract)

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

제안된 API는 확장이 가능합니다. 즉, 라이브러리 개발자는 자신만의 특화된 인터페이스 유형, 제네릭 함수 디스패처, 메서드 결합 알고리즘 등을 구현할 수 있으며, 이러한 확장 기능은 제안된 API에 의해 일급 객체(first-class citizens)로 취급됩니다.

이 API는 C 코드를 사용하지 않고 순수 Python으로 구현될 예정이지만, sys._getframe 및 함수의 func_code 속성과 같은 CPython 특정 기능에 의존할 수 있습니다. Jython 및 IronPython과 같은 다른 Python 구현체들은 유사한 기능을 구현하기 위해 다른 방식(예: Java 또는 C# 사용)을 사용할 것으로 예상됩니다.

배경 및 목표 (Rationale and Goals)

Python은 len(), iter(), pprint.pprint(), 그리고 operator 모듈의 대부분 함수와 같이 다양한 내장 및 표준 라이브러리 제네릭 함수를 항상 제공해왔습니다. 그러나 현재 Python은 다음과 같은 단점을 가지고 있습니다:

  • 개발자가 새로운 제네릭 함수를 생성하는 간단하거나 직접적인 방법이 없습니다.
  • 기존 제네릭 함수에 메서드를 추가하는 표준적인 방법이 없습니다 (일부는 등록 함수를 사용하고, 다른 일부는 __special__ 메서드를 정의해야 하며, 이는 monkeypatching을 통해 이루어질 수 있습니다).
  • 다중 인자 유형에 대한 디스패치를 허용하지 않습니다 (산술 연산자의 제한된 형태를 제외하고, “오른쪽” (__r*__) 메서드를 사용하여 두 인자 디스패치를 수행할 수 있습니다).

또한, Python 코드에서 객체를 어떻게 처리할지 결정하기 위해 전달받은 인자의 유형을 검사하는 것은 일반적인 안티 패턴(anti-pattern)입니다. 예를 들어, 코드는 특정 유형의 객체 또는 해당 유형의 객체 시퀀스를 허용하고 싶을 수 있습니다. 현재 “명확한 방법”은 유형 검사를 통한 것이지만, 이는 깨지기 쉽고 확장에 폐쇄적입니다. 이미 작성된 라이브러리를 사용하는 개발자는 해당 코드가 자신의 객체를 처리하는 방식을 변경할 수 없으며, 특히 타사에서 생성된 객체를 사용하는 경우 더욱 그렇습니다.

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

  • Java 및 C++와 같은 언어에서 볼 수 있는 정적 오버로딩과 유사하지만, CLOS 및 AspectJ에서 볼 수 있는 선택적 메서드 결합 기능을 포함하는 동적 오버로딩 기능.
  • Haskell의 타입클래스(typeclasses)에서 영감을 받은 간단한 “인터페이스 및 어댑테이션” 라이브러리(더 동적이며 정적 타입 검사는 없음). PyProtocols 및 Zope에서 발견되는 것과 같은 사용자 정의 인터페이스 유형을 등록할 수 있는 확장 API를 포함합니다.
  • 상태 저장 어댑터(stateful adapters)를 생성하고 다른 상태 저장 AOP를 쉽게 수행할 수 있는 간단한 “Aspect” 구현.

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

이러한 방식으로 제안된 API는 모든 라이브러리, 프레임워크 및 애플리케이션에 사용할 단일 구현을 규정하기보다는, 단순히 범위 내의 기능에 접근하는 균일한 방법을 제공합니다.

사용자 API (User API)

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

오버로딩/제네릭 함수 (Overloading/Generic Functions)

@overload 데코레이터는 인자 유형에 따라 특화된 함수의 대체 구현을 정의할 수 있도록 합니다. 동일한 이름의 함수가 이미 로컬 네임스페이스에 존재해야 합니다. 기존 함수는 데코레이터에 의해 제자리에서 수정되어 새 구현을 추가하고, 수정된 함수가 데코레이터에 의해 반환됩니다.

예시:

from overloading import overload
from collections import Iterable

def flatten(ob):
    """객체를 구성 요소 이터러블로 평탄화합니다."""
    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

단, overloading으로 정의된 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 vs. @when

@overload 데코레이터는 더 일반적인 @when 데코레이터의 일반적인 단축 표기입니다. @overload는 오버로드할 함수의 이름을 생략할 수 있지만, 대상 함수가 로컬 네임스페이스에 있어야 한다는 제약이 있습니다. 또한 인자 어노테이션을 통해 지정된 기준 외에 추가 기준을 추가하는 것은 지원하지 않습니다.

다음 함수 정의는 이름 바인딩(name binding) 부작용을 제외하고는 동일한 효과를 가집니다.

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을 이전에 바인딩되었던 값에 바인딩합니다. 두 번째 정의도 flatten이 이미 @when 데코레이터의 첫 번째 인자에 바인딩되어 있었다면 동일하게 작동합니다. flatten이 바인딩되지 않았거나 다른 것에 바인딩되어 있다면, 주어진 함수 정의에 다시 바인딩됩니다. 마지막 두 정의는 항상 flatten_basestring을 주어진 함수 정의에 바인딩합니다.

이 접근 방식을 사용하면 메서드에 설명적인 이름을 부여하고(트레이스백(tracebacks)에서 유용!) 나중에 메서드를 재사용할 수 있습니다.

달리 지정되지 않는 한, 모든 오버로딩 데코레이터는 @when과 동일한 시그니처와 바인딩 규칙을 가집니다. 이들은 함수와 선택적인 “프레디케이트(predicate)” 객체를 인자로 받습니다.

기본 프레디케이트 구현은 오버로드된 함수의 인자와 위치적으로 일치하는 유형 튜플입니다. 그러나 확장 API를 사용하여 임의의 수의 다른 종류의 프레디케이트를 생성하고 등록할 수 있으며, 이들은 @when 및 이 모듈에 의해 생성된 다른 데코레이터(예: @before, @after, @around)와 함께 사용할 수 있습니다.

메서드 결합 및 오버라이딩 (Method Combination and Overriding)

오버로드된 함수가 호출되면, 호출 인자와 가장 구체적으로 일치하는 시그니처를 가진 구현이 사용됩니다. 일치하는 구현이 없으면 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를 포함(implies)합니다. S1이 S2를 포함하고 S2가 S1을 포함하지 않을 때, S1은 S2보다 “더 구체적”입니다.

위 예시들은 모두 구체적 또는 추상적 유형을 인자 어노테이션으로 사용했지만, 어노테이션이 반드시 그래야 하는 것은 아닙니다. 이들은 “인터페이스” 객체(Interfaces and Adaptation 섹션에서 논의됨)도 될 수 있으며, 사용자 정의 인터페이스 유형도 포함합니다. (또한 유형이 확장 API를 통해 적절하게 등록된 다른 객체들도 될 수 있습니다.)

“다음” 메서드로 진행 (Proceeding to the “Next” Method)

오버로드된 함수의 첫 번째 매개변수 이름이 __proceed__라면, 다음으로 가장 구체적인 메서드를 나타내는 호출 가능한 객체(callable)가 전달됩니다.

예시:

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__를 안전하게 호출할 수 있는지 식별하기에 충분합니다.

“Before” 및 “After” 메서드

위에서 설명한 간단한 다음-메서드 체이닝 외에도, 메서드를 결합하는 다른 방법이 유용할 때가 있습니다. 예를 들어, “옵저버 패턴(observer pattern)”은 함수에 추가 메서드를 추가하여 구현할 수 있으며, 이 메서드는 정상적인 구현 전후에 실행됩니다.

이러한 사용 사례를 지원하기 위해 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” 메서드는 함수의 “주요” 메서드(즉, 일반 @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” 메서드를 호출한 다음 주요 메서드 체인의 결과를 반환하는 합성(synthetic) 메서드 객체가 될 것입니다.

따라서 일반 메서드와 마찬가지로, __proceed__DispatchError인지 확인할 수 있거나 단순히 호출할 수 있습니다. “around” 메서드는 __proceed__가 반환한 값을 반환해야 합니다. 물론 전체 함수에 대해 다른 반환 값으로 수정하거나 대체하려는 경우는 예외입니다.

사용자 정의 결합 (Custom Combinations)

위에서 설명한 데코레이터들 (@overload, @when, @before, @after, @around)은 CLOS에서 “표준 메서드 결합(standard method combination)”이라고 불리는 것을 공동으로 구현합니다. 이는 메서드를 결합하는 데 사용되는 가장 일반적인 패턴입니다.

그러나 때때로 애플리케이션이나 라이브러리가 더 정교한 유형의 메서드 결합을 사용할 필요가 있을 수 있습니다. 예를 들어, 주요 메서드가 반환하는 값에서 공제될 할인율을 반환하는 “할인” 메서드를 사용하려면 다음과 같이 작성할 수 있습니다.

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

class Discount(MethodList):
    """반환 값을 할인으로 적용합니다."""
    def __call__(self, *args, **kw):
        retval = self.tail(*args, **kw)
        for sig, body in self.sorted():
            retval -= retval * body(*args, **kw)
        return retval

# 우선순위에 따라 할인을 병합합니다.
merge_by_default(Discount)

# 할인은 before/after/primary 메서드보다 우선합니다.
always_overrides(Discount, Before)
always_overrides(Discount, After)
always_overrides(Discount, Method)

# 하지만 "around" 메서드보다는 우선하지 않습니다.
always_overrides(Around, Discount)

# 표준 데코레이터와 똑같이 작동하는 "discount" 데코레이터를 만듭니다.
discount = Discount.make_decorator('discount')

# 이제 사용해봅시다.
def price(product):
    return product.list_price

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

유사한 기술을 사용하여 다양한 CLOS 스타일의 메서드 한정자(qualifiers) 및 결합 규칙을 구현할 수 있습니다. 사용자 정의 메서드 결합 객체와 해당 데코레이터를 생성하는 과정은 확장 API 섹션에서 더 자세히 설명됩니다.

@discount 데코레이터는 다른 코드에 의해 정의된 새로운 프레디케이트와도 올바르게 작동합니다. 예를 들어, zope.interface가 인터페이스 유형을 인자 어노테이션으로 올바르게 작동하도록 등록한다면, 클래스나 overloading에 정의된 인터페이스 유형뿐만 아니라 해당 인터페이스 유형을 기반으로 할인을 지정할 수 있습니다.

유사하게, RuleDispatch 또는 PEAK-Rules와 같은 라이브러리가 적절한 프레디케이트 구현 및 디스패치 엔진을 등록한다면, 해당 프레디케이트를 할인에도 사용할 수 있습니다.

from somewhere import Pred # 일부 프레디케이트 구현

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

사용자 정의 프레디케이트 유형 및 디스패치 엔진을 정의하는 과정도 확장 API 섹션에서 더 자세히 설명됩니다.

클래스 내부 오버로딩 (Overloading Inside Classes)

위의 모든 데코레이터는 클래스 본문 내에서 직접 호출될 때 특별한 추가 동작을 가집니다. 데코레이트된 함수의 첫 번째 매개변수(만약 __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는 로컬 네임스페이스에 정의되어야 합니다.
    @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는 클래스 본문이 실행되기 전에 클래스의 메타클래스가 결정되어야 한다고 요구하므로, 클래스 데코레이션에 이 기술을 더 이상 사용할 수 없습니다. 이 작성 시점에는 이 문제에 대한 논의가 진행 중입니다.

인터페이스 및 어댑테이션 (Interfaces and Adaptation)

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

from overloading import abstract, Interface

class IStack(Interface):
    @abstract
    def push(self, ob):
        """'ob'를 스택에 푸시합니다."""
    @abstract
    def pop(self):
        """값을 팝하고 반환합니다."""

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 클래스는 일종의 “범용 어댑터(universal adapter)”입니다. 이 클래스는 단일 인자, 즉 어댑팅할 객체를 받습니다. 그런 다음 모든 메서드를 자신 대신 대상 객체에 바인딩합니다. 따라서 mystack.push(42)를 호출하는 것은 IStack.push(mylist, 42)를 호출하는 것과 같습니다.

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

위 예시에서 list.append 메서드는 인자가 list와 임의의 객체일 때 IStack.push()의 메서드로 추가됩니다. 따라서 IStack.push(mylist, 42)list.append(mylist, 42)로 번역되어 원하는 작업을 구현합니다.

추상 및 구체적 메서드 (Abstract and Concrete Methods)

@abstract 데코레이터는 인터페이스 정의에만 국한되지 않습니다. 처음에는 메서드가 없는 “빈” 제네릭 함수를 생성하려는 모든 곳에서 사용할 수 있습니다. 특히 클래스 내부에 사용될 필요는 없습니다.

또한 인터페이스 메서드가 추상일 필요는 없습니다. 예를 들어 다음과 같은 인터페이스를 작성할 수 있습니다.

class IWriteMapping(Interface):
    @abstract
    def __setitem__(self, key, value):
        """이것은 구현되어야 합니다."""
    def update(self, other:IReadMapping):
        for k, v in IReadMapping(other).items():
            self[k] = v

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

서브클래싱 및 재조립 (Subclassing and Re-assembly)

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

class ISizedStack(IStack):
    @abstract
    def __len__(self):
        """스택의 항목 수를 반환합니다."""
# ISizedStack에 대한 __len__ 지원 정의
when(ISizedStack.__len__, (list,))(list.__len__)

또는 기존 인터페이스의 함수를 결합하여 재조립될 수 있습니다.

class Sizable(Interface):
    __len__ = ISizedStack.__len__

# 이제 list는 새로운 선언 없이도 ISizedStack뿐만 아니라 Sizable도 구현합니다!

어떤 시점에서 클래스의 인스턴스에 호출될 때 인터페이스에 정의된 어떤 메서드도 NoApplicableMethods 에러를 발생시키지 않을 것이 보장된다면, 그 클래스는 해당 인터페이스에 “적응(adapt to)”한다고 간주될 수 있습니다.

그러나 일반적인 사용에서는 “용서를 구하는 것이 허락을 구하는 것보다 쉽습니다”. 즉, 객체를 인터페이스에 어댑팅(예: IStack(mylist))하거나 인터페이스 메서드를 직접 호출(예: IStack.push(mylist, 42))하여 객체에 인터페이스를 사용하는 것이, 객체가 인터페이스에 적응 가능한지(또는 직접 구현하는지) 알아내려고 시도하는 것보다 쉽습니다.

클래스에서 인터페이스 구현 (Implementing an Interface in a Class)

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() 메서드에 위임됩니다.

효율성을 위해 sStack의 인스턴스인 경우 IStack(s)를 호출하면 IStack 어댑터 대신 s가 반환될 수 있습니다. (참고로, x가 이미 IStack 어댑터인 경우 IStack(x)를 호출하면 항상 x가 변경되지 않고 반환됩니다. 이는 어댑팅되는 객체가 어댑테이션 없이 인터페이스를 직접 구현하는 것으로 알려진 경우 허용되는 추가 최적화입니다.)

편의를 위해 클래스 헤더에서 구현을 선언하는 것이 유용할 수 있습니다.

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

이는 스위트(suite) 끝에서 declare_implementation()을 호출하는 대신 사용될 수 있습니다.

유형 지정자로서의 인터페이스 (Interfaces as Type Specifiers)

인터페이스 서브클래스는 오버로드에 허용되는 객체 유형을 나타내기 위해 인자 어노테이션으로 사용될 수 있습니다.

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

그러나 인터페이스를 유형 지정자로 사용하는 것만으로는 실제 인자가 어떤 식으로든 변경되거나 어댑팅되지 않습니다. 위에서 보듯이 객체를 적절한 인터페이스로 명시적으로 캐스팅해야 합니다.

다른 인터페이스 구현은 어댑테이션을 지원하지 않거나, 함수 인자가 이미 지정된 인터페이스에 어댑팅되어 있어야 할 수도 있습니다. 따라서 인터페이스를 유형 지정자로 사용하는 정확한 의미는 실제로 사용하는 인터페이스 객체에 따라 다릅니다.

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

따라서 예를 들어 ISizedStackISizableISizedStack 모두보다 더 구체적입니다. 이는 이러한 인터페이스 간의 상속 관계와는 무관합니다. 이는 순전히 해당 인터페이스에 어떤 작업이 포함되어 있는지의 문제이며, 작업의 이름은 중요하지 않습니다.

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

인터페이스의 비-메서드 속성 (Non-Method Attributes in Interfaces)

Interface 구현은 실제로 모든 속성 및 메서드(즉, 디스크립터)를 동일한 방식으로 처리합니다. __get__ (및 __set__, __delete__가 있다면) 메서드가 래핑된 (어댑팅된) 객체를 “self”로 하여 호출됩니다. 함수의 경우, 이는 제네릭 함수를 래핑된 객체에 연결하는 바운드 메서드를 생성하는 효과를 가집니다.

비-함수 속성의 경우 property 내장 함수와 해당 fget, fset, fdel 속성을 사용하여 지정하는 것이 가장 쉽습니다.

class ILength(Interface):
    @property
    @abstract
    def length(self):
        """읽기 전용 길이 속성"""
# ILength(aList).length == list.__len__(aList)
when(ILength.length.fget, (list,))(list.__len__)

대안으로, _get_foo()_set_foo()와 같은 메서드를 인터페이스의 일부로 정의하고, 해당 메서드를 기반으로 property를 정의할 수 있습니다. 그러나 이는 인터페이스를 직접 구현하는 클래스를 생성할 때 사용자가 올바르게 구현하기가 약간 더 어렵습니다. 왜냐하면 속성이나 속성 이름뿐만 아니라 모든 개별 메서드 이름도 일치시켜야 하기 때문입니다.

Aspect (애스펙트)

위에서 설명한 어댑테이션 시스템은 어댑터가 “상태 비저장(stateless)”이라고 가정합니다. 즉, 어댑터는 어댑팅된 객체 외에는 속성이나 상태가 없습니다. 이는 Haskell의 “typeclass/instance” 모델과 “순수(pure)” (즉, 전이적으로 조합 가능한) 어댑터의 개념을 따릅니다.

그러나 때때로 어떤 인터페이스의 완전한 구현을 제공하기 위해 어떤 종류의 추가 상태가 필요한 경우가 있습니다.

물론 한 가지 가능성은 어댑팅되는 객체에 monkeypatched “프라이빗(private)” 속성을 붙이는 것입니다. 그러나 이는 이름 충돌의 위험이 있고 초기화 과정을 복잡하게 만듭니다 (이러한 속성을 사용하는 모든 코드가 해당 속성의 존재를 확인하고 필요한 경우 초기화해야 하기 때문입니다). 또한 __dict__ 속성이 없는 객체에서는 작동하지 않습니다.

따라서 Aspect 클래스는 다음 중 하나인 객체에 추가 정보를 쉽게 연결할 수 있도록 제공됩니다.

  1. __dict__ 속성을 가지고 있는 경우 (Aspect 클래스를 키로 사용하여 Aspect 인스턴스를 저장할 수 있음).
  2. 약한 참조(weak referencing)를 지원하는 경우 (전역적이지만 스레드 안전한 약한 참조 딕셔너리를 사용하여 Aspect 인스턴스를 관리할 수 있음).
  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.some_method()Target 인스턴스에서 성공적으로 호출된 횟수를 추적합니다 (즉, 더 구체적인 “after” 메서드에서 에러가 발생하지 않는 한 에러는 계산하지 않습니다). 다른 코드는 Count(someTarget).count를 사용하여 카운트에 접근할 수 있습니다.

Aspect 인스턴스는 물론 __init__ 메서드를 가질 수 있으며, 데이터 구조를 초기화하는 데 사용됩니다. 저장에 __slots__ 또는 딕셔너리 기반 속성을 사용할 수 있습니다.

이 기능은 AspectJ와 같은 완전한 AOP 도구에 비하면 다소 원시적이지만, pointcut 라이브러리 또는 다른 AspectJ와 유사한 기능을 구축하려는 사람들은 Aspect 객체와 메서드 결합 데코레이터를 기반으로 더 표현적인 AOP 도구를 구축할 수 있습니다.

확장 API (Extension 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

오버로딩 사용 패턴 (Overloading Usage Patterns)

Python-3000 목록에서 논의된 바에 따르면, 임의의 함수를 오버로드할 수 있도록 하는 제안된 기능은 다소 논란이 있었습니다. 일부 사람들은 이것이 프로그램을 이해하기 더 어렵게 만들 것이라고 우려를 표했습니다.

이 주장의 일반적인 요지는 함수가 프로그램의 어느 곳에서든 언제든지 변경될 수 있다면, 그 함수가 무엇을 하는지에 의존할 수 없다는 것입니다. 비록 원칙적으로는 monkeypatching이나 코드 대체(code substitution)를 통해 이미 발생할 수 있지만, 이는 나쁜 관행으로 간주됩니다. 그러나 어떤 함수든 오버로딩을 지원하는 것은 (이 주장에 따르면) 그러한 변경을 허용 가능한 관행으로 암묵적으로 승인하는 것입니다.

이 주장은 이론적으로는 타당해 보이지만, 실제로는 두 가지 이유로 거의 무의미합니다.

첫째, 사람들은 일반적으로 비뚤어지지 않습니다. 한 곳에서 한 가지를 하도록 함수를 정의한 다음, 다른 곳에서 완전히 반대되는 것을 하도록 정의하는 경우는 없습니다. 특정하게 제네릭(generic)으로 만들어지지 않은 함수의 동작을 확장하는 주요 이유는 다음과 같습니다.

  • 원래 함수 작성자가 고려하지 않은 특별한 경우를 추가하는 것 (예: 추가 유형 지원).
  • 원래 작업이 수행되기 전, 후 또는 둘 다에 관련된 작업이 수행되도록 동작에 대한 알림을 받는 것. 여기에는 로깅, 타이밍 또는 추적과 같은 일반적인 목적의 작업뿐만 아니라 애플리케이션별 동작도 포함될 수 있습니다.

그러나 이러한 오버로드 추가 이유는 기존 함수의 의도된 기본 또는 전체 동작에 어떤 변경도 의미하지 않습니다. 기본 클래스 메서드가 동일한 두 가지 이유로 서브클래스에 의해 오버라이드될 수 있듯이, 함수도 그러한 개선을 제공하기 위해 오버로드될 수 있습니다.

다시 말해, “범용 오버로딩(universal overloading)”이 “임의 오버로딩(arbitrary overloading)”을 의미하지는 않습니다. 우리는 사람들이 기존 함수의 동작을 비논리적이거나 예측 불가능한 방식으로 무작위로 재정의할 것이라고 예상할 필요가 없습니다. 만약 그렇게 한다면, 그것은 비논리적이거나 예측 불가능한 코드를 작성하는 다른 어떤 방식보다 나쁜 관행일 것입니다!

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

모듈이 새로운 제네릭 작업을 정의한다면, 일반적으로 기존 유형에 필요한 오버로드도 같은 곳에서 정의할 것입니다. 마찬가지로, 모듈이 새로운 유형을 정의한다면, 일반적으로 해당 유형에 대해 알고 있거나 중요하게 생각하는 모든 제네릭 함수에 대한 오버로드를 그곳에서 정의할 것입니다.

결과적으로, 대부분의 오버로드는 오버로드되는 함수 또는 새로 정의된 유형 옆에서 발견될 수 있습니다. 따라서 일반적인 경우 오버로드를 쉽게 찾을 수 있습니다. 함수 또는 유형, 또는 둘 다를 보고 있기 때문입니다.

오버로드가 추가되는 함수나 유형이 모두 없는 모듈에 오버로드가 있는 경우는 상당히 드뭅니다. 이는 예를 들어, 타사가 한 라이브러리의 유형과 다른 라이브러리의 제네릭 함수 사이에 지원 브릿지를 생성한 경우에 해당할 것입니다. 그러나 이러한 경우, 모듈 이름을 통해 이를 눈에 띄게 알리는 것이 모범 사례입니다.

예를 들어, PyProtocolsprotocols.twisted_supportprotocols.zope_support라는 모듈을 사용하여 Zope 인터페이스 및 레거시 Twisted 인터페이스와의 작업을 위한 이러한 브릿지 지원을 정의합니다. (이러한 브릿지는 제네릭 함수보다는 인터페이스 어댑터로 이루어지지만, 기본 원리는 동일합니다.)

요약하면, 범용 오버로딩이 있는 환경에서 프로그램을 이해하는 것은 더 어려워질 필요가 없습니다. 대부분의 오버로드는 함수 옆에 있거나, 해당 함수에 전달되는 유형의 정의 옆에 있기 때문입니다. 그리고 무능함이나 고의적인 모호함의 의도가 없는 한, 관련 유형이나 함수 옆에 있지 않은 소수의 오버로드는 일반적으로 해당 오버로드가 정의된 범위를 벗어나 이해하거나 알 필요가 없을 것입니다. (“지원 모듈”의 경우는 모범 사례가 그에 따라 이름을 지정할 것을 제안합니다.)

구현 노트 (Implementation Notes)

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

peak.rules.core는 현재 DecoratorToolsBytecodeAssembler 모듈에 의존하지만, 이 두 종속성 모두 대체될 수 있습니다. DecoratorTools는 주로 Python 2.3 호환성 및 구조 유형(나중에 Python 버전에서는 이름 있는 튜플로 수행 가능)을 구현하는 데 사용됩니다. BytecodeAssembler의 사용은 합리적인 노력을 기울이면 “exec” 또는 “compile” 워크어라운드(workaround)를 사용하여 대체될 수 있습니다. (함수 객체의 func_closure 속성이 쓰기 가능했다면 더 쉬웠을 것입니다.)

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

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

클래스 본문에서 @overloadclassmethodstaticmethod와 어떻게 잘 작동하게 할지는 현재로서는 알 수 없습니다. 그러나 그것이 꼭 필요한지는 명확하지 않습니다.

이 문서는 퍼블릭 도메인에 공개되었습니다.

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments