PEP 246 – 객체 적응
- Author:
- Alex Martelli <aleaxit at gmail.com>, Clark C. Evans <cce at clarkevans.com>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 21-Mar-2001
- Python-Version:
- 2.5
- Post-History:
- 29-Mar-2001, 10-Jan-2005
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
거부 공지
저는 이 PEP를 거부합니다. 훨씬 더 나은 무언가가 곧 일어날 것입니다. 정확히 무엇인지 말하기는 아직 이르지만, 이 PEP의 제안과 그다지 유사하지는 않을 것이므로 새 PEP를 시작하는 편이 낫습니다. GvR.
초록
이 제안은 특정 프로토콜(예를 들어 특정 타입, 클래스, 또는 인터페이스)을 지원하는 객체를 기대하는 컨텍스트에 유입되는 객체를 적응시키기 위한 확장 가능한 협력적 메커니즘을 제시합니다.
이 제안은 임의의 객체 X와 임의의 프로토콜 Y에 대해, Python 환경에 Y를 준수하는 X의 버전을 요청하는 데 사용할 수 있는 내장 “adapt” 함수를 제공합니다. 내부적으로 이 메커니즘은 객체 X에게 “당신은 지금 프로토콜 Y를 지원합니까, 아니면 지원하도록 스스로를 감싸는 방법을 알고 있습니까?”라고 묻습니다. 그리고 이 요청이 실패하면, 함수는 이어서 프로토콜 Y에게 “객체 X가 당신을 지원합니까, 아니면 그러한 지원자를 얻기 위해 그것을 감싸는 방법을 알고 있습니까?”라고 묻습니다. 이 이중성은 중요한데, 프로토콜이 객체보다 나중에 개발될 수도 있고 그 반대일 수도 있으며, 이 PEP는 기존 컴포넌트(들)에 대해 비침습적으로 두 경우 모두를 지원할 수 있게 하기 때문입니다.
마지막으로, 객체와 프로토콜이 서로에 대해 알지 못하는 경우, 이 메커니즘은 특정 객체를 특정 프로토콜에 적응시킬 수 있는 호출 가능 객체를 동적으로 등록할 수 있는 어댑터 팩토리 레지스트리를 확인할 수 있습니다. 제안의 이 부분은 선택 사항입니다. 예를 들어 적합한 커스텀 메타클래스를 통해 특정 종류의 프로토콜 및/또는 객체가 어댑터 팩토리의 동적 등록을 받아들일 수 있게 함으로써도 동일한 효과를 얻을 수 있습니다. 그러나 이 선택적 부분은 프로토콜이나 다른 객체 어느 쪽에도 침습적이지 않은 방식으로 적응을 더 유연하고 강력하게 만들어주며, 이를 통해 Python 표준 라이브러리의 “copy_reg” 모듈이 직렬화와 지속성에 제공하는 것과 거의 같은 종류의 이점을 적응에도 부여합니다.
이 제안은 프로토콜이 무엇인지, “프로토콜 준수”가 정확히 무엇을 의미하는지, 또는 래퍼가 정확히 무엇을 해야 하는지를 구체적으로 제약하지 않습니다. 이러한 생략은 이 제안이 타입과 클래스의 기존 시스템과 같은 기존 프로토콜 범주뿐만 아니라, PEP 245의 것, Zope3 [2]의 것, 또는 BDFL의 Artima 블로그에서 2004년 말과 2005년 초에 논의된 것들 [3]과 같이 Python을 위해 제안되었거나 구현된 “인터페이스” 자체에 대한 많은 개념과도 호환되도록 하기 위한 것입니다. 그러나 규범적이 아니라 시사적인 성격을 의도한, 이러한 주제에 대한 몇 가지 견해도 포함되어 있습니다.
동기
현재 파이썬에는 객체가 특정 프로토콜을 지원하는지 확인하기 위한 표준화된 메커니즘이 없습니다. 일반적으로 특정 메서드, 특히 __getitem__과 같은 특수 메서드의 존재 여부가 특정 프로토콜에 대한 지원의 지표로 사용됩니다. 이 기법은 BDFL(선의의 종신 독재자)이 승인한 몇몇 특정 프로토콜에는 잘 작동합니다. ‘isinstance’ 확인에 기반한 대안적인 기법에 대해서도 마찬가지로 말할 수 있습니다(‘isinstance’를 사용해 객체가 “[내장] 문자열”인지 확인할 수 있도록, 내장 클래스 “basestring”이 특별히 존재합니다). 이 두 접근법 모두 파이썬 표준 코어 외부에서 애플리케이션과 서드파티 프레임워크가 정의하는 다른 프로토콜로 쉽고 일반적으로 확장될 수 없습니다.
객체가 주어진 프로토콜을 이미 지원하는지 확인하는 것보다 훨씬 더 중요한 것은, 아직 지원이 없는 경우 그 객체에 적합한 어댑터(래퍼 또는 프록시)를 얻는 작업일 수 있습니다. 예를 들어 문자열은 파일 프로토콜을 지원하지 않지만, 이를 StringIO 인스턴스로 감싸면 그 프로토콜을 지원하면서 감싸고 있는 문자열로부터 데이터를 얻는 객체를 얻을 수 있습니다. 이렇게 하면 파일처럼 읽을 수 있는 객체를 인자로 요구하는 서브시스템에 (적절히 감싼) 문자열을 전달할 수 있습니다. 불행히도 현재는 이처럼 매우 중요한 “래핑을 통한 적응” 작업을 자동화하는 일반적이고 표준화된 방법이 없습니다.
오늘날 일반적으로 특정 프로토콜을 예상하는 컨텍스트에 객체를 전달할 때, 객체가 그 컨텍스트를 알고 자체 래퍼를 제공하거나, 컨텍스트가 그 객체를 알고 적절히 감쌉니다. 이러한 접근법의 어려움은 그런 적응이 일회성이고, 사용자 코드의 한 곳에 집중되어 있지 않으며, 공통된 기법으로 실행되지 않는다는 점 등입니다. 이러한 표준화의 부재는 동일한 어댑터가 여러 곳에 나타나는 코드 중복을 늘리거나, 클래스를 적응시키는 대신 다시 작성하도록 조장합니다. 어느 경우든 유지보수성이 저하됩니다.
특정 프로토콜에 대한 객체의 준수 여부를 검증하고, 마침 사용 가능한 래퍼가 있다면 이를 제공해 주는 표준 함수가 있다면 매우 좋을 것입니다 – 그 특정한 경우에 맞는 방법을 찾으려고 각 라이브러리의 문서를 일일이 뒤질 필요 없이 말입니다.
요구사항
객체가 프로토콜을 준수하는지를 고려할 때 검토해야 할 여러 경우가 있습니다:
- 프로토콜이 타입이나 클래스이고, 객체가 정확히 그 타입을 가지거나 정확히 그 클래스(서브클래스가 아닌)의 인스턴스인 경우입니다. 이 경우, 준수 여부는 자동으로 결정됩니다.
- 객체가 프로토콜에 대해 알고 있으며, 자신이 준수한다고 여기거나 스스로를 적절히 래핑하는 방법을 알고 있는 경우입니다.
- 프로토콜이 객체에 대해 알고 있으며, 객체가 이미 준수하거나 프로토콜이 객체를 적절히 래핑하는 방법을 알고 있는 경우입니다.
- 프로토콜이 타입이나 클래스이고, 객체가 서브클래스의 구성원인 경우입니다. 이는 위의 첫 번째 경우 (a)와는 구별되는데, 상속이 (안타깝게도) 반드시 대체 가능성을 의미하지는 않으므로 신중하게 처리해야 하기 때문입니다.
- 컨텍스트가 객체와 프로토콜에 대해 알고 있으며, 요구되는 프로토콜이 충족되도록 객체를 적응(adapt)시키는 방법을 알고 있는 경우입니다. 이는 어댑터 레지스트리나 이와 유사한 접근 방식을 사용할 수 있습니다.
위의 네 번째 경우는 미묘합니다. 대체 가능성의 위반은 서브클래스가 메서드의 시그니처를 변경하거나, 메서드 인자에 허용되는 도메인을 제한하거나(인자 타입에 대한 “공변성”), 베이스 클래스가 결코 만들어내지 않을 반환값을 포함하도록 공역(co-domain)을 확장할 때(반환 타입에 대한 “반공변성”) 발생할 수 있습니다. 클래스 상속에 기반한 준수는 자동적이어야 마땅하지만, 이 제안은 객체가 베이스 클래스 프로토콜을 준수하지 않는다고 신호를 보낼 수 있도록 허용합니다.
그러나 파이썬이 인터페이스를 위한 어떤 표준 “공식” 메커니즘을 갖추게 된다면, “빠른 경로”에 해당하는 경우 (a)를 프로토콜이 인터페이스인 경우로 확장할 수 있고 또 마땅히 그래야 하며, 이때 객체는 해당 인터페이스를 준수한다고 주장하는 타입이나 클래스의 인스턴스가 됩니다. 예를 들어, [3] 에서 논의된 “interface” 키워드가 파이썬에 채택된다면, 경우 (a)의 “빠른 경로”를 사용할 수 있는데, 이는 인터페이스를 구현하는 인스턴스화 가능한 클래스가 대체 가능성을 위반하도록 허용되지 않기 때문입니다.
명세
이 제안은 이러한 요구 사항을 지원하는 기반이 되는 새로운 내장 함수 adapt()를 도입합니다.
adapt() 함수는 세 개의 매개변수를 가집니다:
obj, 적응시킬 객체protocol, 객체에 요청된 프로토콜alternate, 객체를 적응시킬 수 없을 경우 반환할 선택적 객체
adapt() 함수가 성공하면, 객체가 이미 프로토콜을 준수하는 경우 전달된 객체 obj를 반환하거나, 그렇지 않으면 프로토콜을 준수하는 객체의 뷰를 제공하는 보조 객체 wrapper를 반환합니다. wrapper의 정의는 의도적으로 모호하게 되어 있으며, 필요한 경우 wrapper는 자체 상태를 가진 완전한 객체가 될 수 있습니다. 하지만 설계 의도는, 적응 wrapper가 자신이 감싸는 원본 객체에 대한 참조와, (필요한 경우) wrapper 객체에 위임할 수 없는 최소한의 추가 상태를 함께 보유해야 한다는 것입니다.
적응 wrapper의 훌륭한 예로, 들어오는 문자열을 마치 텍스트 파일인 것처럼 읽을 수 있도록 적응시키는 StringIO의 인스턴스를 들 수 있습니다. 이 wrapper는 문자열에 대한 참조를 보유하지만, “현재 읽기 위치”(감싸인 문자열의 어디에서 다음, 예를 들어 “readline” 호출의 문자들이 나올 것인가)는 스스로 처리하는데, 이는 감싸인 객체에 위임할 수 없기 때문입니다(문자열에는 “현재 읽기 위치”라는 개념도, 그와 조금이라도 관련된 다른 개념도 전혀 없습니다).
객체를 프로토콜에 어댑테이션하는 데 실패하면 AdaptationError(TypeError의 서브클래스)가 발생하며, 다만 대체(alternate) 매개변수가 사용된 경우에는 대신 그 대체 인자가 반환됩니다.
요구 사항에 나열된 첫 번째 경우를 가능하게 하기 위해, adapt() 함수는 먼저 객체의 타입 또는 객체의 클래스가 프로토콜과 동일한지 확인합니다. 그렇다면, adapt() 함수는 더 이상의 절차 없이 객체를 곧바로 반환합니다.
두 번째 경우를 가능하게 하려면, 즉 객체가 프로토콜에 대해 알고 있을 때는, 객체가 __conform__() 메서드를 가지고 있어야 합니다. 이 선택적 메서드는 두 개의 인자를 받습니다:
self, 적응 중인 객체protocol, 요청된 프로토콜
오늘날의 파이썬에 있는 다른 모든 특수 메서드와 마찬가지로, __conform__은 객체 자체가 아니라 객체의 클래스로부터 가져오도록 되어 있습니다(단, “classic classes”의 인스턴스에 대해서는, 이를 여전히 지원해야 하는 한 예외입니다). 이를 통해 원한다면 향후 파이썬의 타입 객체에 ‘tp_conform’ 슬롯을 추가하는 것이 가능해집니다.
객체는 준수 여부를 나타내기 위해 __conform__의 결과로 자기 자신을 반환할 수 있습니다. 또는 객체는 프로토콜을 준수하는 래퍼 객체를 반환하는 방법도 있습니다. 객체가 프로토콜의 서브클래스인 타입에 속하지만 자신이 준수하지 않는다는 것을 알고 있다면, __conform__은 LiskovViolation 예외(AdaptationError의 서브클래스)를 발생시켜야 합니다. 마지막으로, 객체가 자신의 준수 여부를 판단할 수 없는 경우, 나머지 메커니즘이 작동하도록 None을 반환해야 합니다. __conform__이 다른 예외를 발생시키면, “adapt”는 그 예외를 그대로 전파합니다.
세 번째 경우를 가능하게 하려면, 프로토콜이 객체에 대해 알고 있을 때 프로토콜은 __adapt__() 메서드를 가지고 있어야 합니다. 이 선택적 메서드는 두 개의 인자를 받습니다:
self, 요청된 프로토콜obj, 적응되는 객체
프로토콜이 객체가 준수한다고 판단하면, obj를 직접 반환할 수 있습니다. 또는 이 메서드는 프로토콜을 준수하는 래퍼를 반환할 수도 있습니다. 프로토콜이 객체가 프로토콜의 서브클래스인 타입에 속하지만 준수하지 않는다는 것을 알고 있다면, __adapt__은 LiskovViolation 예외(AdaptationError의 서브클래스)를 발생시켜야 합니다. 마지막으로, 준수 여부를 판단할 수 없는 경우, 이 메서드는 나머지 메커니즘이 작동하도록 None을 반환해야 합니다. __adapt__이 다른 예외를 발생시키면, “adapt”는 그 예외를 그대로 전파합니다.
네 번째 경우, 즉 객체의 클래스가 프로토콜의 서브클래스인 경우는 내장 adapt() 함수가 처리합니다. 일반적인 상황에서는 “isinstance(object, 프로토콜)”이면 adapt()이 객체를 직접 반환합니다. 하지만 객체를 대체할 수 없는 경우, 위에서 언급한 __conform__() 또는 __adapt__() 메서드는 이 기본 동작을 막기 위해 LiskovViolation(AdaptationError의 서브클래스)을 발생시킬 수 있습니다.
처음 네 가지 메커니즘 중 어느 것도 작동하지 않은 경우, 최후의 수단으로 ‘adapt’는 다섯 번째 경우를 충족시키기 위해 프로토콜과 obj의 타입으로 색인된 어댑터 팩토리 레지스트리를 확인하는 방식으로 대체됩니다. 어댑터 팩토리는 서로에 대해 알지 못하는 객체와 프로토콜에 “제3자 적응(third party adaptation)”을 제공하기 위해, 객체나 프로토콜 어느 쪽에도 침해적이지 않은 방식으로 이 레지스트리에 동적으로 등록되거나 제거될 수 있습니다.
의도된 용도
adapt의 전형적인 의도된 용도는 인자로든 어떤 함수 호출의 결과로든 “외부로부터” 어떤 객체 X를 받았고, 그 객체를 특정 프로토콜 Y에 따라 사용해야 하는 코드에서입니다. Y와 같은 “프로토콜”은 인터페이스를 나타내기 위한 것으로, 보통 (“계약에 의한 설계(design by contract)” 접근법에서 전형적으로 사용되는 것과 같은) 몇 가지 의미론적 제약으로 보강되며, 종종 (“특정 연산의 실행 시간이 O(N)보다 나빠서는 안 된다”는 식의) 실용적인 기대도 포함합니다. 이 제안서는 프로토콜이 실제로 어떻게 설계되는지, 프로토콜 준수 여부가 어떻게 또는 실제로 검사되는지, 준수를 주장했지만 실제로는 이행하지 않을 경우 어떤 결과가 초래되는지를 명시하지 않습니다(“구문적(syntactic)” 준수 — 메서드의 이름과 시그니처 — 의 결여는 흔히 예외 발생으로 이어질 것입니다. “의미론적(semantic)” 준수의 결여는 미묘하고 때로는 간헐적인 오류로 이어질 수 있습니다[예를 들어, 스레드 안전(threadsafe)을 주장하는 메서드가 실제로는 미묘한 경쟁 조건(race condition)에 노출되어 있는 경우를 상상해 보십시오]. “실용적(pragmatic)” 준수의 결여는 일반적으로 올바르게 동작하지만 실용적으로 쓰기에는 너무 느리거나, 때로는 메모리나 디스크 공간 같은 자원을 고갈시키는 코드로 이어질 것입니다).
프로토콜 Y가 구체적인 타입이나 클래스인 경우, 그것에 대한 준수는 객체가 Y의 인스턴스에 대해 수행될 수 있는 모든 연산을 “비슷한” 의미론과 실용성으로 허용한다는 것을 의미하도록 의도되어 있습니다. 예를 들어, 단일 연결 리스트(singly-linked list)인 가상의 객체 X는 list의 모든 메서드를 구현하더라도 ‘list’ 프로토콜에 대한 준수를 주장해서는 안 됩니다. X[n]을 인덱싱하는 데 O(n) 시간이 걸리는 반면, list에서는 같은 연산이 O(1)이라는 사실이 차이를 만들기 때문입니다. 반면에, StringIO.StringIO의 인스턴스는 ‘file’ 프로토콜을 준수합니다. 비록 (모듈 ‘marshal’의 연산과 같은) 일부 연산이 명시적인 타입 검사를 수행하기 때문에 서로 대체하는 것을 허용하지 않을 수 있더라도, 그러한 타입 검사는 프로토콜 준수의 관점에서 볼 때 “도를 넘어선(beyond the pale)” 것입니다.
이러한 관례는 이 제안서의 목적을 위해 구체적인 타입이나 클래스를 프로토콜로 사용하는 것을 실현 가능하게 하지만, 그러한 사용은 종종 최적이 아닐 것입니다. ‘adapt’를 호출하는 코드가 특정 구체적인 타입의 모든 기능을 필요로 하는 경우는 드뭅니다. 특히 file, list, dict처럼 풍부한 타입에서는 더욱 그렇습니다. 그리고 구체적인 타입의 것과 실제로 동일한 구문과 의미론뿐만 아니라 좋은 실용성까지 갖춘 래퍼로 그 모든 기능을 제공할 수 있는 경우도 드뭅니다.
오히려, 이 제안서가 받아들여지면, 현재 파이썬에서, 특히 표준 라이브러리 내에서 사용되고 있는 프로토콜들의 본질적인 특성을 파악하고, 이를 일종의 “인터페이스” 구조를 이용해 정형화하는 설계 작업을 시작할 필요가 있습니다(반드시 새로운 구문이 필요한 것은 아닙니다. 단순한 커스텀 메타클래스만으로도 시작할 수 있으며, 그 작업의 결과는 나중에 파이썬 언어에 최종적으로 받아들여지는 어떤 “인터페이스” 구조로든 이전될 수 있습니다). 이렇게 더 형식적으로 설계된 프로토콜들의 팔레트를 갖추게 되면, ‘adapt’를 사용하는 코드는 ‘file’ 프로토콜에 대한 준수를 지나치게 포괄적으로 요구하는 대신, 이를테면 “읽기 가능하고 탐색 가능한 파일 유사(filelike) 객체”로의 적응이나, 적절한 수준의 “세밀함(granularity)”을 갖춘 구체적으로 필요한 다른 무언가를 요청할 수 있게 될 것입니다.
적응(Adaptation)은 “캐스팅”이 아닙니다. 객체 X 자체가 프로토콜 Y를 따르지 않을 때, X를 Y에 적응시킨다는 것은 X에 대한 참조를 가지고 있으며 Y가 요구하는 연산을 대체로 적절한 방식으로 X에 위임함으로써 구현하는 일종의 래퍼 객체 Z를 사용하는 것을 의미합니다. 예를 들어, X가 문자열이고 Y가 ‘file’이라면, X를 Y에 적응시키는 올바른 방법은 StringIO(X)를 만드는 것이지, file(X)를 호출하는 것이 아닙니다 [이는 X로 이름 붙여진 파일을 열려고 시도할 것입니다].
그러나 수치 타입과 프로토콜은 이 “적응은 캐스팅이 아니다”라는 만트라의 예외가 되어야 할 수도 있습니다.
귀도의 “선택적 정적 타이핑: 논쟁을 멈추자” 블로그 게시물
적응의 전형적인 간단한 사용 사례는 다음과 같습니다:
def f(X):
X = adapt(X, Y)
# continue by using X according to protocol Y
[4]에서 BDFL은 다음 구문을 도입할 것을 제안했습니다:
def f(X: Y):
# continue by using X according to protocol Y
바로 이 전형적인 adapt 사용에 대한 편리한 축약형으로, 그리고 파서가 이 새로운 구문을 받아들이도록 수정될 때까지 실험을 위한 기반으로서, 의미적으로 동등한 데코레이터를:
@arguments(Y)
def f(X):
# continue by using X according to protocol Y
이러한 BDFL의 아이디어는 이 제안과 완전히 호환되며, 같은 블로그에 있는 귀도의 다른 제안들도 마찬가지입니다.
참조 구현과 테스트 사례
다음 참조 구현은 클래식 클래스를 다루지 않으며, 새 스타일 클래스만 고려합니다. 클래식 클래스를 지원해야 한다면, 다소 지저분하기는 하겠지만 추가해야 할 내용은 꽤 명확할 것입니다(x.__class__와 type(x)의 차이, 타입이 아니라 객체에서 직접 바운드 메서드를 가져오는 것 등).
-----------------------------------------------------------------
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) first check to see if object has the exact protocol
if t is protocol:
return obj
try:
# (b) next check if t.__conform__ exists & likes protocol
conform = getattr(t, '__conform__', None)
if conform is not None:
result = conform(obj, protocol)
if result is not None:
return result
# (c) then check if protocol.__adapt__ exists & likes obj
adapt = getattr(type(protocol), '__adapt__', None)
if adapt is not None:
result = adapt(protocol, obj)
if result is not None:
return result
except LiskovViolation:
pass
else:
# (d) check if object is instance of protocol
if isinstance(obj, protocol):
return obj
# (e) last chance: try the registry
return _adapt_by_registry(obj, protocol, alternate)
-----------------------------------------------------------------
test.py
-----------------------------------------------------------------
from adapt import AdaptationError, LiskovViolation, adapt
from adapt import registerAdapterFactory, unregisterAdapterFactory
import doctest
class A(object):
'''
>>> a = A()
>>> a is adapt(a, A) # case (a)
True
'''
class B(A):
'''
>>> b = B()
>>> b is adapt(b, A) # case (d)
True
'''
class C(object):
'''
>>> c = C()
>>> c is adapt(c, B) # case (b)
True
>>> c is adapt(c, A) # a failure case
Traceback (most recent call last):
...
AdaptationError
'''
def __conform__(self, protocol):
if protocol is B:
return self
class D(C):
'''
>>> d = D()
>>> d is adapt(d, D) # case (a)
True
>>> d is adapt(d, C) # case (d) explicitly blocked
Traceback (most recent call last):
...
AdaptationError
'''
def __conform__(self, protocol):
if protocol is C:
raise LiskovViolation
class MetaAdaptingProtocol(type):
def __adapt__(cls, obj):
return cls.adapt(obj)
class AdaptingProtocol:
__metaclass__ = MetaAdaptingProtocol
@classmethod
def adapt(cls, obj):
pass
class E(AdaptingProtocol):
'''
>>> a = A()
>>> a is adapt(a, E) # case (c)
True
>>> b = A()
>>> b is adapt(b, E) # case (c)
True
>>> c = C()
>>> c is adapt(c, E) # a failure case
Traceback (most recent call last):
...
AdaptationError
'''
@classmethod
def adapt(cls, obj):
if isinstance(obj, A):
return obj
class F(object):
pass
def adapt_F_to_A(obj, protocol, alternate):
if isinstance(obj, F) and issubclass(protocol, A):
return obj
else:
return alternate
def test_registry():
'''
>>> f = F()
>>> f is adapt(f, A) # a failure case
Traceback (most recent call last):
...
AdaptationError
>>> registerAdapterFactory(F, A, adapt_F_to_A)
>>> f is adapt(f, A) # case (e)
True
>>> unregisterAdapterFactory(F, A)
>>> f is adapt(f, A) # a failure case again
Traceback (most recent call last):
...
AdaptationError
>>> registerAdapterFactory(F, A, adapt_F_to_A)
'''
doctest.testmod()
마이크로소프트의 QueryInterface와의 관계
이 제안은 마이크로소프트의 (COM) QueryInterface와 몇 가지 유사점이 있지만, 여러 측면에서 차이가 있습니다.
첫째, 이 제안에서 어댑테이션은 양방향이며, 인터페이스(프로토콜)도 질의할 수 있게 해주므로 더 동적인 능력을 제공합니다(더 파이썬다움). 둘째, 원래의 래핑되지 않은 객체 아이덴티티를 확인하거나 얻는 데 사용할 수 있는 특별한 “IUnknown” 인터페이스는 없습니다. 다만 이는 그러한 “특별한” 공인 인터페이스 프로토콜 식별자 중 하나로 제안될 수도 있습니다. 셋째, QueryInterface에서는 객체가 특정 인터페이스를 한 번 지원하면 이후 항상 이 인터페이스를 지원해야 합니다. 이 제안은 그러한 보장을 하지 않는데, 특히 어댑터 팩토리가 레지스트리에 동적으로 추가되었다가 나중에 다시 제거될 수 있기 때문입니다.
넷째, 마이크로소프트의 QueryInterface 구현은 일종의 동치 관계를 지원해야 합니다. 즉 특정한 의미에서 반사적이고, 대칭적이고, 추이적이어야 합니다. 이 제안에 따른 프로토콜 어댑테이션의 동등한 조건들 역시 바람직한 속성을 나타낼 것입니다:
# given, to start with, a successful adaptation:
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는 객체, 인터페이스, 어댑터가 어떻게 코딩되어야 하는지를 어느 정도 규정하기 때문에 이에 상응하는 조건을 강제할 수 있습니다. 이 제안은 반드시 침습적이지는 않으며, 사용 가능하고, 서로를 전혀 모른 채 코딩된 두 프레임워크 사이에 어느 쪽도 수정하지 않고 어댑테이션을 “역접목”할 수 있도록 하는 것을 의도합니다.
어댑테이션의 추이성은 사실 다소 논쟁의 여지가 있으며, 어댑테이션과 상속 사이의 관계(만약 있다면)도 마찬가지입니다.
만약 상속이 항상 리스코프 치환 가능성을 함의한다는 것을 알고 있다면 후자는 논쟁거리가 되지 않겠지만, 안타깝게도 그렇지 않습니다. 만약 [4]에서 제안된 인터페이스와 같은 어떤 특수한 형태가 실제로 리스코프 치환 가능성을 보장할 수 있다면, 오직 그러한 종류의 상속에 대해서만, X가 Y를 준수하고 Y가 Z를 상속한다면 X는 Z를 준수한다고 아마도 주장할 수 있을 것입니다… 하지만 이는 오직 치환 가능성을 의미론과 화용론까지 포함하는 매우 강한 의미로 받아들일 때만 성립하며, 이는 의심스러워 보입니다. (참고로: QueryInterface에서는 상속이 준수를 요구하지도, 함의하지도 않습니다). 이 제안은 위에서 구체적으로 상술한 작은 효과들을 넘어서는 상속의 “강한” 효과는 포함하지 않습니다.
마찬가지로, 추이성은 적절하고 자동으로 선택된 어떤 Y를 거쳐 adapt(X, Z)의 결과를 얻기 위해, 본질적으로 adapt(adapt(X, Y), Z)와 같은 여러 번의 “내부” 어댑테이션 과정을 함의할 수도 있습니다. 다시 말하지만, 이는 적절히 강한 제약 조건 하에서는 아마도 실현 가능할 수 있지만, 그러한 방식의 실질적인 함의는 이 제안의 저자들에게도 여전히 명확하지 않습니다. 따라서 이 제안은 어떠한 상황에서도 어댑테이션의 자동적이거나 암묵적인 추이성을 포함하지 않습니다.
이행성(transitivity)과 상속의 영향이라는 측면에서 더 고급한 처리를 수행하는, 이 제안의 원래 버전에 대한 구현으로는 Phillip J. Eby의 PyProtocols [5]를 참고하십시오. PyProtocols에 딸린 문서는 어댑터가 어떻게 작성되고 사용되어야 하는지, 그리고 적응(adaptation)이 애플리케이션 코드에서 타입 검사의 필요성을 어떻게 제거할 수 있는지에 대한 고찰을 담고 있어 정독할 가치가 충분합니다.
질문과 답변
- Q: 이 제안은 어떤 이점을 제공합니까?
A: 전형적인 파이썬 프로그래머는 통합자(integrator)로서, 다양한 공급자로부터 온 컴포넌트를 연결하는 사람입니다. 흔히 이러한 컴포넌트 사이를 연결하려면 중간 어댑터가 필요합니다. 보통 프로그래머는 한 컴포넌트가 노출하는 인터페이스와 다른 컴포넌트가 요구하는 인터페이스를 조사하여 이들이 직접 호환되는지 판단하거나, 어댑터를 개발해야 하는 부담을 안게 됩니다. 때로는 공급자가 적절한 어댑터를 함께 제공하기도 하지만, 그런 경우에도 그 어댑터를 찾아내고 배치하는 방법을 파악하는 데 시간이 걸립니다.
이 기법은 필요에 따라
__conform__또는__adapt__를 구현함으로써 공급자들이 서로 직접 연동할 수 있게 해줍니다. 이로써 통합자는 스스로 어댑터를 만들어야 하는 부담에서 벗어납니다. 본질적으로 이는 컴포넌트들이 서로 간에 단순한 대화를 나눌 수 있게 해줍니다. 통합자는 단순히 한 컴포넌트를 다른 컴포넌트에 연결하기만 하면 되며, 타입이 자동으로 일치하지 않을 경우 적응 메커니즘이 내장되어 있습니다.게다가 어댑터 레지스트리 덕분에 “제4의 당사자”가, 서로에 대해 전혀 알지 못하는 프레임워크들이 비침습적으로 상호운용할 수 있도록 어댑터를 제공할 수 있으며, 이때 통합자는 시작 시점에 레지스트리에 적절한 어댑터 팩토리를 설치하는 것 이상의 어떠한 작업도 할 필요가 없습니다.
라이브러리와 프레임워크가 여기서 제안된 적응 인프라와 협력하는 한(본질적으로 프로토콜을 적절히 정의하고 사용하며, 전달받은 인자와 콜백 팩토리 함수의 결과에 대해 필요에 따라 ‘adapt’를 호출함으로써), 그로 인해 통합자의 작업은 훨씬 더 단순해집니다.
예를 들어, SAX1과 SAX2 인터페이스를 생각해 보십시오: 이 둘 사이를 전환하려면 어댑터가 필요합니다. 일반적으로 프로그래머는 이 점을 인식하고 있어야 하지만, 이 어댑테이션 제안이 적용되면 더 이상 그럴 필요가 없어집니다 – 실제로 어댑터 레지스트리 덕분에, SAX1을 제공하는 프레임워크와 SAX2를 요구하는 프레임워크가 서로를 알지 못하더라도 이러한 필요성이 사라질 수 있습니다.
- Q: 왜 이것이 내장되어야 합니까? 독립적으로 사용할 수는 없습니까?
A: 네, 독립적으로도 동작합니다. 하지만 내장된다면 사용될 가능성이 더 커집니다. 이 제안의 가치는 주로 표준화에 있습니다: Python 표준 라이브러리를 포함하여 서로 다른 공급자로부터 온 라이브러리와 프레임워크가 어댑테이션에 대해 단일한 접근 방식을 사용하게 하는 것입니다. 게다가:
- 이 메커니즘은 본질적으로 싱글턴입니다.
- 자주 사용된다면 내장 기능일 때 훨씬 더 빨라질 것입니다.
- 확장 가능하며 자기 주장을 내세우지 않습니다.
- 일단 ‘adapt’가 내장되면, 문법 확장을 지원할 수 있고 타입 추론 시스템에도 어느 정도 도움이 될 수 있습니다.
- Q: 왜
__conform__과__adapt__라는 동사를 사용합니까?A: conform, 자동사
- 형태나 성격이 상응합니다; 유사합니다.
- 조화나 합의에 따라 행동합니다; 순응합니다.
- 현재의 관습이나 방식에 따라 행동합니다.
adapt, 타동사
- 특정 용도나 상황에 적합하게 또는 맞게 만드는 것입니다.
출처: The American Heritage 영어 딕셔너리, 제3판
하위 호환성
누군가 특수 이름 __conform__이나 __adapt__를 다른 용도로 사용했던 경우가 아니라면 하위 호환성에는 문제가 없어야 하지만, 그럴 가능성은 낮아 보이며, 어떤 경우든 사용자 코드는 특수 이름을 비표준적인 용도로 사용해서는 안 됩니다.
이 제안은 인터프리터를 변경하지 않고도 구현하고 테스트할 수 있습니다.
감사의 말
이 제안은 주요 Python 메일링 리스트와 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와의 논의에 많은 것을 빚졌으며, Python에서 인터페이스와 프로토콜의 사용 및 적응에 관한 이들의 제안, 구현, 문서에 대한 연구와 숙고에도 힘입었습니다.
참고 자료 및 각주
Copyright
This document has been placed in the public domain.