[Rejected] PEP 246 - Object Adaptation

원문 링크: PEP 246 - Object Adaptation

상태: Rejected 유형: Standards Track 작성일: 21-Mar-2001

PEP 246 – 객체 어댑테이션 (Object Adaptation)

  • 작성자: Alex Martelli, Clark C. Evans
  • 상태: Rejected (거부됨)
  • 유형: Standards Track
  • 생성일: 2001년 3월 21일
  • Python 버전: 2.5
  • 최종 수정일: 2025년 2월 1일

거부 통지 (Rejection Notice)

이 PEP는 거부되었습니다. 더 나은 무언가가 곧 나타날 예정입니다. 정확히 무엇인지는 아직 말하기 이르지만, 이 PEP의 제안과 너무 흡사하지는 않을 것이므로 새로운 PEP를 시작하는 것이 더 좋습니다. - GvR (Guido van Rossum)

초록 (Abstract)

이 제안은 특정 프로토콜(예: 특정 타입, 클래스 또는 인터페이스)을 지원하는 객체를 기대하는 컨텍스트에 들어오는 객체를 적응(adaptation)시키기 위한 확장 가능한 협력 메커니즘을 제시합니다.

이 제안은 어떤 객체 X와 어떤 프로토콜 Y에 대해서든 파이썬 환경에 Y를 준수하는 X 버전을 요청하는 데 사용될 수 있는 내장 adapt 함수를 제공합니다. 이 메커니즘은 내부적으로 객체 X에게 “현재 프로토콜 Y를 지원하거나, 프로토콜 Y를 지원하도록 자신을 래핑(wrap)하는 방법을 알고 있습니까?”라고 묻습니다. 이 요청이 실패하면, 함수는 프로토콜 Y에게 “객체 X가 당신을 지원합니까, 아니면 그러한 지원자를 얻기 위해 객체 X를 래핑하는 방법을 알고 있습니까?”라고 묻습니다. 이러한 이중성은 중요합니다. 왜냐하면 프로토콜은 객체 후에 개발될 수도 있고, 그 반대일 수도 있으며, 이 PEP는 기존 구성 요소에 비침습적인 방식으로 두 가지 경우를 모두 지원할 수 있도록 하기 때문입니다.

마지막으로, 객체도 프로토콜도 서로에 대해 모르는 경우, 메커니즘은 어댑터 팩토리(adapter factories) 레지스트리(registry)를 확인할 수 있습니다. 이 레지스트리에는 특정 객체를 특정 프로토콜에 적응시킬 수 있는 호출 가능한 객체(callables)가 동적으로 등록될 수 있습니다. 이 제안의 이 부분은 선택 사항입니다. 예를 들어, 적절한 사용자 지정 메타클래스(custom metaclasses)를 통해 특정 종류의 프로토콜 및/또는 객체가 어댑터 팩토리의 동적 등록을 허용하도록 보장함으로써 동일한 효과를 얻을 수 있습니다. 그러나 이 선택적 부분은 프로토콜이나 다른 객체에 비침습적인 방식으로 어댑테이션을 더 유연하고 강력하게 만들 수 있으며, 파이썬 표준 라이브러리의 copy_reg 모듈이 직렬화(serialization) 및 지속성(persistence)을 위해 제공하는 것과 거의 동일한 종류의 이점을 어댑테이션에 제공합니다.

이 제안은 프로토콜이 무엇인지, “프로토콜 준수(compliance to a protocol)”가 정확히 무엇을 의미하는지, 또는 래퍼(wrapper)가 정확히 무엇을 해야 하는지를 특별히 제한하지 않습니다. 이러한 생략은 기존 타입 및 클래스 시스템과 같은 기존 프로토콜 범주뿐만 아니라 PEP 245의 인터페이스, Zope3의 인터페이스, 2004년 말과 2005년 초에 BDFL의 Artima 블로그에서 논의된 인터페이스와 같이 파이썬을 위해 제안되거나 구현된 “인터페이스” 개념과 이 제안이 호환되도록 의도된 것입니다. 그러나 이러한 주제에 대한 일부 성찰(제안적이며 규범적이지 않은 의도)도 포함되어 있습니다.

동기 (Motivation)

현재 파이썬에는 객체가 특정 프로토콜을 지원하는지 확인하는 표준화된 메커니즘이 없습니다. 일반적으로 __getitem__과 같은 특정 메서드, 특히 특수 메서드의 존재가 특정 프로토콜 지원의 지표로 사용됩니다. 이 기술은 BDFL이 승인한 몇 가지 특정 프로토콜에 잘 작동합니다. ‘isinstance’를 확인하는 대체 기술(내장 클래스 basestring은 객체가 “[내장] 문자열”인지 확인하기 위해 ‘isinstance’를 사용하도록 특별히 존재합니다)도 마찬가지입니다. 두 가지 접근 방식 모두 표준 파이썬 코어 외부에서 애플리케이션 및 타사 프레임워크에 의해 정의된 다른 프로토콜에 쉽고 일반적으로 확장할 수 없습니다.

객체가 주어진 프로토콜을 이미 지원하는지 확인하는 것보다 훨씬 더 중요한 것은, 지원이 아직 없는 경우 객체에 대한 적절한 어댑터(래퍼 또는 프록시)를 얻는 작업일 수 있습니다. 예를 들어, 문자열은 파일 프로토콜을 지원하지 않지만, StringIO 인스턴스로 래핑하여 해당 프로토콜을 지원하고 래핑된 문자열에서 데이터를 가져오는 객체를 얻을 수 있습니다. 이런 식으로, 문자열(적절히 래핑된)을 파일처럼 읽을 수 있는 객체를 인수로 요구하는 서브시스템에 전달할 수 있습니다. 불행히도 현재 이러한 “래핑을 통한 어댑테이션”이라는 매우 중요한 종류의 작업을 자동화하는 일반적이고 표준화된 방법은 없습니다.

일반적으로 오늘날 객체를 특정 프로토콜을 기대하는 컨텍스트에 전달할 때, 객체가 컨텍스트를 알고 자체 래퍼를 제공하거나 컨텍스트가 객체를 알고 적절하게 래핑합니다. 이러한 접근 방식의 어려움은 이러한 어댑테이션이 일회성(one-offs)이며, 사용자 코드의 단일 위치에 집중되지 않고, 공통된 기술로 실행되지 않는다는 것입니다. 이러한 표준화 부족은 동일한 어댑터가 여러 곳에서 발생하여 코드 중복을 증가시키거나, 클래스를 어댑트(adapt)하는 대신 재작성하도록 장려합니다. 어느 경우든 유지보수성이 저하됩니다.

객체의 특정 프로토콜 준수 여부를 확인하고, 쉽게 사용할 수 있는 래퍼가 있는 경우 이를 제공하는 표준 함수가 있다면 매우 좋을 것입니다. 이 모든 것을 특정 사례에 적합한 문서(incantation)를 찾기 위해 각 라이브러리의 문서를 뒤질 필요 없이 말입니다.

요구사항 (Requirements)

객체의 프로토콜 준수 여부를 고려할 때, 몇 가지 경우를 조사해야 합니다.

  1. 프로토콜이 타입 또는 클래스이고, 객체가 정확히 해당 타입이거나 정확히 해당 클래스의 인스턴스인 경우(서브클래스가 아닌 경우). 이 경우, 준수는 자동입니다.
  2. 객체가 프로토콜에 대해 알고 있으며, 스스로 준수한다고 간주하거나 적절하게 자신을 래핑하는 방법을 아는 경우.
  3. 프로토콜이 객체에 대해 알고 있으며, 객체가 이미 준수하거나 프로토콜이 객체를 적절하게 래핑하는 방법을 아는 경우.
  4. 프로토콜이 타입 또는 클래스이고, 객체가 서브클래스의 멤버인 경우. 이것은 위 첫 번째 경우 (a)와 다릅니다. 상속(불행히도)이 반드시 대체 가능성(substitutability)을 의미하지 않으므로 신중하게 처리해야 합니다.
  5. 컨텍스트가 객체와 프로토콜에 대해 알고 있으며, 필요한 프로토콜이 충족되도록 객체를 어댑트하는 방법을 아는 경우. 이는 어댑터 레지스트리 또는 유사한 접근 방식을 사용할 수 있습니다.

위 네 번째 경우는 미묘합니다. 대체 가능성이 깨질 수 있는 경우는 서브클래스가 메서드의 시그니처를 변경하거나, 메서드 인수에 허용되는 도메인을 제한하거나(인수 타입의 “공변성”), 반환 값의 공변역(co-domain)을 확장하여 기본 클래스가 결코 생성하지 않을 수 있는 반환 값을 포함하는 경우(“반환 타입의 “반공변성”)입니다. 클래스 상속을 기반으로 한 준수는 자동이어야 하지만, 이 제안은 객체가 기본 클래스 프로토콜을 준수하지 않음을 알릴 수 있도록 합니다.

그러나 파이썬이 인터페이스에 대한 표준 “공식” 메커니즘을 얻게 된다면, “고속 경로(fast-path)” 경우 (a)는 프로토콜이 인터페이스이고 객체가 해당 인터페이스 준수를 주장하는 타입 또는 클래스의 인스턴스인 경우로 확장될 수 있고 또 확장되어야 합니다. 예를 들어,에서 논의된 “interface” 키워드가 파이썬에 채택된다면, 인터페이스를 구현하는 인스턴스화 가능한 클래스는 대체 가능성을 깨는 것이 허용되지 않으므로 경우 (a)의 “고속 경로”가 사용될 수 있습니다.

명세 (Specification)

이 제안은 이러한 요구 사항을 지원하는 기반이 되는 새로운 내장 함수 adapt()를 도입합니다.

adapt() 함수는 세 가지 매개변수를 가집니다.

  • obj: 어댑트될 객체
  • protocol: 객체에 요청된 프로토콜
  • alternate: 객체를 어댑트할 수 없는 경우 반환될 선택적 객체

adapt() 함수의 성공적인 결과는, 객체가 이미 프로토콜을 준수하는 경우 전달된 obj 객체를 반환하거나, 프로토콜을 준수하는 객체의 뷰(view)를 제공하는 보조 객체 wrapper를 반환합니다. wrapper의 정의는 의도적으로 모호하며, 필요하다면 자체 상태를 가진 완전한 객체가 될 수 있습니다. 그러나 디자인 의도는 어댑테이션 래퍼가 래핑하는 원래 객체에 대한 참조를 유지하고, (필요한 경우) 래핑된 객체에 위임할 수 없는 최소한의 추가 상태를 가져야 한다는 것입니다.

어댑테이션 래퍼의 훌륭한 예시는 들어오는 문자열을 마치 텍스트 파일처럼 읽을 수 있도록 어댑트하는 StringIO 인스턴스입니다. 래퍼는 문자열에 대한 참조를 유지하지만, “현재 읽기 지점”(예: 다음 readline 호출에 대한 문자가 래핑된 문자열의 어느 부분에서 올 것인지)을 스스로 처리합니다. 왜냐하면 이를 래핑된 객체(문자열은 “현재 읽기 지점” 또는 그와 관련된 어떤 개념도 없습니다)에 위임할 수 없기 때문입니다.

객체를 프로토콜에 어댑트하는 데 실패하면 AdaptationError ( TypeError의 서브클래스)가 발생합니다. alternate 매개변수가 사용된 경우에는 대신 alternate 인수가 반환됩니다.

요구 사항에 나열된 첫 번째 경우를 활성화하기 위해, adapt() 함수는 먼저 객체의 타입 또는 객체의 클래스가 프로토콜과 동일한지 확인합니다. 그렇다면 adapt() 함수는 더 이상 조치 없이 객체를 직접 반환합니다.

두 번째 경우, 객체가 프로토콜에 대해 아는 경우를 활성화하려면, 객체에 __conform__() 메서드가 있어야 합니다. 이 선택적 메서드는 두 가지 인수를 취합니다.

  • self: 어댑트되는 객체
  • protocol: 요청된 프로토콜

오늘날의 파이썬에서 다른 특수 메서드와 마찬가지로, __conform__는 (고전 클래스 인스턴스를 여전히 지원해야 하는 한) 객체 자체에서 가져오는 것이 아니라 객체의 클래스에서 가져오도록 의도되었습니다. 이는 원할 경우 향후 파이썬의 Type 객체에 가능한 tp_conform 슬롯을 추가할 수 있도록 합니다.

객체는 준수를 나타내기 위해 __conform__의 결과로 자신을 반환할 수 있습니다. 또는 객체는 프로토콜을 준수하는 래퍼 객체를 반환할 수도 있습니다. 객체가 프로토콜의 서브클래스인 타입에 속하지만 준수하지 않는다고 판단하는 경우, __conform__LiskovViolation 예외 ( AdaptationError의 서브클래스)를 발생시켜야 합니다. 마지막으로, 객체가 준수를 결정할 수 없는 경우, 나머지 메커니즘을 활성화하기 위해 None을 반환해야 합니다. __conform__가 다른 예외를 발생시키면, adapt는 단순히 이를 전파합니다.

세 번째 경우, 프로토콜이 객체에 대해 아는 경우를 활성화하려면, 프로토콜에 __adapt__() 메서드가 있어야 합니다. 이 선택적 메서드는 두 가지 인수를 취합니다.

  • self: 요청된 프로토콜
  • obj: 어댑트되는 객체

프로토콜이 객체가 준수한다고 판단하면, obj를 직접 반환할 수 있습니다. 또는 메서드는 프로토콜을 준수하는 래퍼를 반환할 수 있습니다. 프로토콜이 객체가 프로토콜의 서브클래스인 타입에 속하지만 준수하지 않는다고 판단하는 경우, __adapt__LiskovViolation 예외 ( AdaptationError의 서브클래스)를 발생시켜야 합니다. 마지막으로, 준수를 결정할 수 없는 경우, 이 메서드는 나머지 메커니즘을 활성화하기 위해 None을 반환해야 합니다. __adapt__가 다른 예외를 발생시키면, adapt는 단순히 이를 전파합니다.

객체의 클래스가 프로토콜의 서브클래스인 네 번째 경우는 내장 adapt() 함수에 의해 처리됩니다. 일반적인 상황에서 isinstance(object, protocol)이면 adapt()는 객체를 직접 반환합니다. 그러나 객체가 대체 가능하지 않은 경우, 위에서 언급한 __conform__() 또는 __adapt__() 메서드가 LiskovViolation ( AdaptationError의 서브클래스)을 발생시켜 이 기본 동작을 방지할 수 있습니다.

처음 네 가지 메커니즘 중 어느 것도 작동하지 않는 경우, 마지막 시도로 adapt는 다섯 번째 경우를 충족하기 위해 프로토콜과 obj의 타입으로 인덱싱된 어댑터 팩토리 레지스트리를 확인하는 것으로 돌아갑니다. 어댑터 팩토리는 해당 레지스트리에 동적으로 등록 및 제거되어 서로에 대해 알지 못하는 객체와 프로토콜의 “제3자 어댑테이션”을 객체나 프로토콜에 비침습적인 방식으로 제공할 수 있습니다.

의도된 사용 (Intended Use)

adapt의 일반적인 의도된 사용은, 인수로 또는 어떤 함수 호출의 결과로 “외부에서” 어떤 객체 X를 받아 특정 프로토콜 Y에 따라 해당 객체를 사용해야 하는 코드에 있습니다. Y와 같은 “프로토콜”은 일반적으로 일부 의미론적 제약 조건(“계약에 의한 설계(design by contract)” 접근 방식에서 일반적으로 사용되는 것과 같은)으로 보강된 인터페이스를 나타내며, 종종 일부 실용적인 기대치(“특정 작업의 실행 시간은 O(N)보다 나쁘지 않아야 함” 등)도 나타냅니다. 이 제안은 프로토콜이 어떻게 설계되는지, 프로토콜 준수 여부가 어떻게 또는 왜 확인되는지, 또는 준수를 주장했지만 실제로는 이행하지 않았을 때의 결과가 무엇인지를 명시하지 않습니다. (“문법적” 준수 부족(메서드의 이름과 시그니처)은 종종 예외를 발생시킬 것이고; “의미론적” 준수 부족은 미묘하고 아마도 가끔 발생하는 오류로 이어질 수 있으며(예를 들어, 스레드 안전하다고 주장하지만 실제로는 미묘한 경합 조건에 노출되는 메서드를 상상해 보십시오); “실용적” 준수 부족은 일반적으로 올바르게 실행되지만 실제 사용에는 너무 느리거나 때로는 메모리나 디스크 공간과 같은 리소스 고갈로 이어지는 코드로 이어질 것입니다).

프로토콜 Y가 구체적인 타입 또는 클래스인 경우, 이에 대한 준수는 객체가 Y 인스턴스에 대해 수행될 수 있는 모든 작업을 “비교 가능한” 의미론 및 실용성과 함께 허용한다는 것을 의미합니다. 예를 들어, 단일 연결 리스트인 가상의 객체 X는 리스트의 모든 메서드를 구현하더라도 ‘list’ 프로토콜 준수를 주장해서는 안 됩니다. X[n] 인덱싱에 O(n) 시간이 걸리는 반면, 리스트에서는 동일한 작업이 O(1)이라는 사실이 차이를 만듭니다. 반면에 StringIO.StringIO 인스턴스는 ‘file’ 프로토콜을 준수합니다. 일부 작업(marshal 모듈의 작업과 같은)은 명시적인 타입 검사를 수행하기 때문에 서로를 대체하는 것을 허용하지 않을 수 있지만, 이러한 타입 검사는 프로토콜 준수 관점에서 “도를 넘는” 것입니다.

이러한 규칙은 이 제안의 목적을 위해 구체적인 타입 또는 클래스를 프로토콜로 사용하는 것을 가능하게 하지만, 그러한 사용은 종종 최적이 아닐 것입니다. ‘adapt’를 호출하는 코드가 특정 구체적인 타입의 모든 기능을 필요로 하는 경우는 드물며, 특히 file, list, dict와 같은 풍부한 타입의 경우 더욱 그렇습니다. 또한, 래퍼가 좋은 실용성과 함께 구체적인 타입과 정말 동일한 구문 및 의미론을 제공할 수 있는 경우도 드뭅니다.

오히려, 이 제안이 수락되면, 현재 파이썬, 특히 표준 라이브러리 내에서 사용되는 프로토콜의 필수적인 특성을 식별하고, 어떤 종류의 “인터페이스” 구성(새로운 구문을 반드시 요구하지는 않음: 간단한 사용자 지정 메타클래스를 통해 시작할 수 있으며, 이 노력의 결과는 궁극적으로 파이썬 언어에 채택될 “인터페이스” 구성으로 나중에 마이그레이션될 수 있음)을 사용하여 공식화하는 설계 노력이 시작되어야 합니다. 이러한 보다 공식적으로 설계된 프로토콜 팔레트를 통해, ‘adapt’를 사용하는 코드는 ‘file’ 프로토콜에 너무 일반적으로 준수를 요청하는 대신, 예를 들어 “읽고 탐색 가능한 파일과 같은 객체” 또는 필요한 다른 특정 수준의 “세분성(granularity)”으로 어댑테이션을 요청할 수 있을 것입니다.

어댑테이션은 “캐스팅(casting)”이 아닙니다. 객체 X 자체가 프로토콜 Y를 따르지 않는 경우, XY에 어댑트한다는 것은 X에 대한 참조를 유지하고 Y가 요구하는 모든 작업을 대부분 적절한 방식으로 X에 위임하여 구현하는 어떤 종류의 래퍼 객체 Z를 사용하는 것을 의미합니다. 예를 들어, X가 문자열이고 Y가 ‘file’인 경우, XY에 어댑트하는 올바른 방법은 StringIO(X)를 만드는 것이지, file(X)를 호출하는 것이 아닙니다 (이는 X가 지정한 이름의 파일을 열려고 시도할 것입니다).

그러나 숫자 타입과 프로토콜은 이 “어댑테이션은 캐스팅이 아니다”라는 원칙의 예외가 될 수 있습니다.

Guido의 “선택적 정적 타이핑: 논쟁을 멈춰라” 블로그 글 (Guido’s “Optional Static Typing: Stop the Flames” Blog Entry)

어댑테이션의 일반적인 간단한 사용 사례는 다음과 같습니다.

def f(X):
    X = adapt(X, Y)
    # 프로토콜 Y에 따라 X를 계속 사용

에서 BDFL은 다음 구문을 도입할 것을 제안했습니다.

def f(X: Y):
    # 프로토콜 Y에 따라 X를 계속 사용

이는 adapt의 이러한 일반적인 사용을 위한 편리한 단축키이며, 파서가 이 새로운 구문을 받아들이도록 수정될 때까지 실험의 기반으로서 의미론적으로 동등한 데코레이터입니다.

@arguments(Y)
def f(X):
    # 프로토콜 Y에 따라 X를 계속 사용

이러한 BDFL 아이디어는 이 제안과 완전히 호환되며, 같은 블로그에 있는 Guido의 다른 제안들도 마찬가지입니다.

참조 구현 및 테스트 케이스 (Reference Implementation and Test Cases)

다음 참조 구현은 고전 클래스를 다루지 않습니다. 새 스타일 클래스만 고려합니다. 고전 클래스를 지원해야 하는 경우, 추가 사항은 다소 지저분하지만( x.__class__type(x), 타입에서 직접이 아닌 객체에서 바운드 메서드를 가져오는 등) 꽤 명확해야 합니다.

(참고: 원문의 코드는 Python 2.5를 기반으로 하며, 현재 Python 3 환경에서는 작동하지 않을 수 있습니다.)

# adapt.py
class AdaptationError(TypeError):
    pass

class LiskovViolation(AdaptationError):
    pass

_adapter_factory_registry = {}

def registerAdapterFactory(objtype, protocol, factory):
    _adapter_factory_registry[objtype, protocol] = factory

def unregisterAdapterFactory(objtype, protocol):
    del _adapter_factory_registry[objtype, protocol]

def _adapt_by_registry(obj, protocol, alternate):
    factory = _adapter_factory_registry.get((type(obj), protocol))
    if factory is None:
        adapter = alternate
    else:
        adapter = factory(obj, protocol, alternate)
    if adapter is AdaptationError:
        raise AdaptationError
    else:
        return adapter

def adapt(obj, protocol, alternate=AdaptationError):
    t = type(obj)
    # (a) 먼저 객체가 정확한 프로토콜을 가지고 있는지 확인
    if t is protocol:
        return obj
    try:
        # (b) 다음으로 t.__conform__가 존재하고 프로토콜을 선호하는지 확인
        conform = getattr(t, '__conform__', None)
        if conform is not None:
            result = conform(obj, protocol)
            if result is not None:
                return result
        # (c) 그런 다음 protocol.__adapt__가 존재하고 obj를 선호하는지 확인
        _adapt = getattr(type(protocol), '__adapt__', None) # 'adapt'는 내장 함수와 이름이 겹치므로 '_adapt'로 변경
        if _adapt is not None:
            result = _adapt(protocol, obj)
            if result is not None:
                return result
    except LiskovViolation:
        pass
    else:
        # (d) 객체가 프로토콜의 인스턴스인지 확인
        if isinstance(obj, protocol):
            return obj
        # (e) 마지막 기회: 레지스트리 시도
        return _adapt_by_registry(obj, protocol, alternate)

Microsoft QueryInterface와의 관계 (Relationship To Microsoft’s QueryInterface)

이 제안은 Microsoft의 (COM) QueryInterface와 일부 유사점이 있지만, 여러 측면에서 다릅니다.

첫째, 이 제안의 어댑테이션은 양방향이며, 인터페이스(프로토콜)도 쿼리(query)할 수 있도록 하여 더 동적인 기능(더 파이써닉, Pythonic)을 제공합니다. 둘째, 원래의 래핑되지 않은 객체 ID를 확인하거나 얻는 데 사용할 수 있는 특별한 “IUnknown” 인터페이스는 없지만, 이는 “특별히” 승인된 인터페이스 프로토콜 식별자 중 하나로 제안될 수 있습니다. 셋째, QueryInterface를 사용하면 객체가 특정 인터페이스를 지원하면 항상 그 인터페이스를 지원해야 합니다. 이 제안은 그러한 보장을 하지 않습니다. 특히, 어댑터 팩토리는 레지스트리에 동적으로 추가될 수 있고 나중에 다시 제거될 수 있기 때문입니다.

넷째, Microsoft의 QueryInterface 구현은 특정 의미에서 재귀적(reflexive), 대칭적(symmetrical), 전이적(transitive)인 일종의 등가 관계를 지원해야 합니다. 이 제안에 따른 프로토콜 어댑테이션에 대한 동등한 조건도 바람직한 속성을 나타낼 것입니다.

# 먼저 성공적인 어댑테이션이 주어졌을 때:
X_as_Y = adapt(X, Y)

# 재귀성(reflexive):
assert adapt(X_as_Y, Y) is X_as_Y

# 전이성(transitive):
X_as_Z = adapt(X, Z, None)
X_as_Y_as_Z = adapt(X_as_Y, Z, None)
assert (X_as_Y_as_Z is None) == (X_as_Z is None)

# 대칭성(symmetrical):
X_as_Z_as_Y = adapt(X_as_Z, Y, None)
assert (X_as_Y_as_Z is None) == (X_as_Z_as_Y is None)

그러나 이러한 속성들은 바람직하지만, 모든 경우에 보장할 수 없을 수도 있습니다. QueryInterface는 객체, 인터페이스, 어댑터가 어떻게 코딩되어야 하는지를 어느 정도 지시하기 때문에 동등한 조건을 부과할 수 있습니다. 이 제안은 반드시 침습적이지 않고, 서로에 대해 알지 못하는 두 프레임워크 간의 어댑테이션을 소급하여 적용(retrofit)하는 데 사용될 수 있도록 의도되었으며, 두 프레임워크를 수정할 필요가 없습니다.

어댑테이션의 전이성은 상속과의 관계(있다면)와 마찬가지로 사실 다소 논란의 여지가 있습니다.

후자는 상속이 항상 Liskov 대체 가능성을 의미한다는 것을 안다면 논란의 여지가 없을 것입니다. 불행히도 우리는 그렇지 않습니다.에서 제안된 인터페이스와 같은 일부 특별한 형태가 실제로 Liskov 대체 가능성을 보장할 수 있다면, 그러한 종류의 상속에 대해서만, XY를 따르고 YZ를 상속한다면 XZ를 따른다고 주장할 수 있을 것입니다. 그러나 이는 대체 가능성이 의미론 및 실용성을 포함하는 매우 강력한 의미로 받아들여질 때만 가능하며, 이는 의심스럽습니다. (참고: QueryInterface에서는 상속이 준수를 요구하거나 의미하지 않습니다). 이 제안은 위에 특별히 자세히 설명된 작은 효과를 넘어 상속의 “강력한” 효과를 포함하지 않습니다.

마찬가지로, 전이성은 adapt(adapt(X, Y), Z)와 같이 적절하고 자동으로 선택된 중간 Y를 통해 adapt(X, Z)의 결과를 얻기 위해 여러 “내부” 어댑테이션 패스를 의미할 수 있습니다. 다시 말하지만, 이는 적절하게 강력한 제약 조건 하에서는 가능할 수 있지만, 그러한 스키마의 실제적인 의미는 이 제안의 저자들에게 여전히 불분명합니다. 따라서 이 제안은 어떤 상황에서도 어댑테이션의 자동 또는 암시적 전이성을 포함하지 않습니다.

전이성 및 상속의 효과 측면에서 더 고급 처리를 수행하는 이 제안의 원본 구현은 Phillip J. Eby의 PyProtocols를 참조하십시오. PyProtocols에 대한 문서는 어댑터를 어떻게 코딩하고 사용해야 하는지, 그리고 어댑테이션이 애플리케이션 코드에서 타입 검사의 필요성을 어떻게 제거할 수 있는지에 대한 고려 사항을 연구할 가치가 있습니다.

질의응답 (Questions and Answers)

Q: 이 제안은 어떤 이점을 제공합니까?

A: 일반적인 파이썬 프로그래머는 다양한 공급업체의 구성 요소를 연결하는 통합자(integrator)입니다. 종종 이러한 구성 요소 간의 인터페이스를 위해 중간 어댑터가 필요합니다. 일반적으로 한 구성 요소가 노출하고 다른 구성 요소가 요구하는 인터페이스를 연구하고, 직접 호환되는지 판단하거나, 어댑터를 개발하는 부담은 프로그래머에게 있습니다. 때로는 공급업체가 적절한 어댑터를 포함할 수도 있지만, 어댑터를 찾고 배포 방법을 알아내는 데에도 시간이 걸립니다.

이 기술은 공급업체가 필요에 따라 __conform__ 또는 __adapt__를 구현하여 서로 직접 협력할 수 있도록 합니다. 이는 통합자가 자체 어댑터를 만드는 부담을 덜어줍니다. 본질적으로, 이는 구성 요소들이 서로 간단한 대화를 할 수 있도록 합니다. 통합자는 단순히 한 구성 요소를 다른 구성 요소에 연결하며, 타입이 자동으로 일치하지 않으면 어댑테이션 메커니즘이 내장되어 있습니다.

더욱이, 어댑터 레지스트리 덕분에 “제4자(fourth party)”는 서로에 대해 전혀 알지 못하는 프레임워크의 상호 운용성을 위해 어댑터를 공급할 수 있으며, 비침습적인 방식으로 통합자가 시작 시 레지스트리에 적절한 어댑터 팩토리를 설치하는 것 외에는 아무것도 할 필요가 없습니다.

라이브러리 및 프레임워크가 여기에 제안된 어댑테이션 인프라(본질적으로 프로토콜을 적절하게 정의하고 사용하며, 수신된 인수 및 콜백 팩토리 함수의 결과에 필요에 따라 ‘adapt’를 호출함으로써)와 협력하는 한, 통합자의 작업은 훨씬 간단해집니다.

예를 들어, SAX1 및 SAX2 인터페이스를 고려해 보십시오. 둘 사이를 전환하는 데 어댑터가 필요합니다. 일반적으로 프로그래머는 이를 알고 있어야 합니다. 그러나 이 어댑테이션 제안이 적용되면 더 이상 그렇지 않습니다. 사실, 어댑터 레지스트리 덕분에 SAX1을 제공하는 프레임워크와 SAX2를 요구하는 프레임워크가 서로에 대해 알지 못하더라도 이 필요가 제거될 수 있습니다.

Q: 왜 이것이 내장(built-in)되어야 합니까? 독립적으로는 안 됩니까?

A: 예, 독립적으로 작동합니다. 그러나 내장되면 사용될 가능성이 더 높습니다. 이 제안의 가치는 주로 표준화에 있습니다. 파이썬 표준 라이브러리를 포함하여 다른 공급업체에서 오는 라이브러리와 프레임워크가 어댑테이션에 단일 접근 방식을 사용하도록 하는 것입니다. 또한:

  • 이 메커니즘은 본질적으로 싱글톤(singleton)입니다. 자주 사용되면 내장으로 훨씬 빠를 것입니다.
  • 확장 가능하고 겸손합니다(unassuming). ‘adapt’가 내장되면 구문 확장을 지원하고 타입 추론 시스템에도 도움이 될 수 있습니다.

Q: 왜 동사 __conform____adapt__를 사용합니까?

A:

  • conform (자동사):
    • 형태나 성격이 일치하다; 유사하다.
    • 일치하거나 동의하다; 준수하다.
    • 현재 관습이나 방식에 따라 행동하다.
  • adapt (타동사):
    • 특정 용도나 상황에 적합하게 만들다.

출처: The American Heritage Dictionary of the English Language, Third Edition

하위 호환성 (Backwards Compatibility)

누군가가 __conform__ 또는 __adapt__라는 특수 이름을 다른 방식으로 사용하지 않는 한 하위 호환성에 문제가 없어야 합니다. 그러나 이는 unlikely(가능성이 낮으며), 어떤 경우든 사용자 코드는 비표준 목적으로 특수 이름을 사용해서는 안 됩니다.

이 제안은 인터프리터 변경 없이 구현 및 테스트될 수 있습니다.

기여자 (Credits)

이 제안은 주로 주요 파이썬 메일링 리스트와 type-sig 리스트의 재능 있는 개인들의 피드백으로 인해 만들어졌습니다. (누락된 분이 있다면 사과드립니다!) 제안의 저자 외에 특정 기여자를 언급하자면, 제안의 첫 버전의 주요 제안은 Paul Prescod로부터 왔고, Robin Thomas로부터 상당한 피드백을 받았습니다. 우리는 또한 Marcin ‘Qrczak’ Kowalczyk와 Carlos Ribeiro의 아이디어를 빌렸습니다.

다른 기여자(댓글을 통해)로는 Michel Pelletier, Jeremy Hylton, Aahz Maruch, Fredrik Lundh, Rainer Deyke, Timothy Delaney, Huaiyu Zhu가 있습니다. 현재 버전은 Phillip J. Eby, Guido van Rossum, Bruce Eckel, Jim Fulton, Ka-Ping Yee 등과의 토론과 파이썬에서 인터페이스 및 프로토콜의 사용 및 어댑테이션에 대한 그들의 제안, 구현 및 문서 연구 및 성찰에 많은 빚을 지고 있습니다.

참조 및 각주 (References and Footnotes)

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


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

Comments