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.
초록
이 제안은 특정 프로토콜(예를 들어 특정 타입, 클래스 또는 인터페이스)을 지원하는 객체를 기대하는 컨텍스트에 들어오는 객체를 적응시키기 위한, 확장 가능한 협력적 메커니즘을 제시합니다.
이 제안은 내장 “adapt” 함수를 제공합니다. 이 함수는 임의의 객체 X와 임의의 프로토콜 Y에 대해, Y를 준수하는 X의 버전을 Python 환경에 요청하는 데 사용할 수 있습니다. 내부적으로 이 메커니즘은 객체 X에게 “당신은 지금 프로토콜 Y의 지원자입니까, 아니면 프로토콜 Y의 지원자가 되도록 자신을 감싸는 방법을 알고 있습니까?”라고 묻습니다. 그리고 이 요청이 실패하면, 함수는 이어서 프로토콜 Y에게 “객체 X가 당신을 지원합니까, 아니면 그러한 지원자를 얻기 위해 객체 X를 감싸는 방법을 알고 있습니까?”라고 묻습니다. 이러한 이중성은 중요합니다. 프로토콜이 객체보다 나중에 개발될 수도 있고 그 반대일 수도 있는데, 이 PEP는 어느 경우든 기존 구성 요소(들)를 침범하지 않고 지원할 수 있게 해 주기 때문입니다.
마지막으로, 객체와 프로토콜이 서로에 대해 모르는 경우, 이 메커니즘은 어댑터 팩토리 레지스트리를 확인할 수 있습니다. 이 레지스트리에는 특정 객체를 특정 프로토콜에 적응시킬 수 있는 호출 가능 객체를 동적으로 등록할 수 있습니다. 제안의 이 부분은 선택 사항입니다. 예를 들어 적절한 사용자 정의 메타클래스를 통해, 특정 종류의 프로토콜 및/또는 객체가 어댑터 팩토리의 동적 등록을 받아들일 수 있도록 보장하면 같은 효과를 얻을 수 있습니다. 하지만 이 선택적인 부분은 프로토콜이나 다른 객체를 침범하지 않는 방식으로 적응을 더 유연하고 강력하게 만들어 줍니다. 이를 통해 Python 표준 라이브러리의 “copy_reg” 모듈이 직렬화와 영속성에 제공하는 것과 거의 같은 종류의 이점을 적응에서도 얻을 수 있습니다.
이 제안은 프로토콜이 무엇인지, “프로토콜 준수”가 정확히 무엇을 의미하는지, 래퍼가 정확히 무엇을 해야 하는지를 구체적으로 제한하지 않습니다. 이러한 사항을 정하지 않은 것은 이 제안이 기존의 타입과 클래스 체계 같은 기존 범주의 프로토콜뿐만 아니라, Python을 위해 제안되거나 구현된 수많은 “인터페이스” 개념 자체와도 호환되도록 하기 위함입니다. 그러한 개념의 예로는 PEP 245의 인터페이스, Zope3의 인터페이스 [2], 2004년 말과 2005년 초에 BDFL의 Artima 블로그에서 논의된 인터페이스들 [3] 등이 있습니다. 다만 이러한 주제에 대한 몇 가지 고찰도 포함되어 있으며, 이는 규범적인 것이 아니라 시사적인 것으로 의도되었습니다.
동기
현재 Python에는 객체가 특정 프로토콜을 지원하는지 확인하는 표준화된 메커니즘이 없습니다. 일반적으로 특정 메서드, 특히 __getitem__ 같은 특수 메서드의 존재 여부가 특정 프로토콜 지원의 지표로 사용됩니다. 이 기법은 BDFL(Benevolent Dictator for Life, 자비로운 종신 독재자)이 인정한 몇몇 특정 프로토콜에 대해서는 잘 동작합니다. ‘isinstance’ 검사에 기반한 대안 기법도 마찬가지입니다(내장 클래스 “basestring”은 바로 ‘isinstance’를 사용하여 객체가 “[내장] 문자열인지” 확인할 수 있게 하려고 존재합니다). 두 방식 모두 표준 Python 코어 밖에서 애플리케이션과 서드파티 프레임워크가 정의한 다른 프로토콜로 쉽고 일반적으로 확장할 수 없습니다.
객체가 주어진 프로토콜을 이미 지원하는지 확인하는 것보다 더 중요할 수 있는 일은, 아직 지원하지 않는 경우 그 객체에 적합한 어댑터(래퍼 또는 프록시)를 얻는 작업입니다. 예를 들어 문자열은 파일 프로토콜을 지원하지 않지만, 문자열을 StringIO 인스턴스로 감싸면 그 프로토콜을 지원하면서 감싼 문자열에서 데이터를 가져오는 객체를 얻을 수 있습니다. 그렇게 하면 파일처럼 읽을 수 있는 객체를 인자로 요구하는 하위 시스템에 (적절히 감싼) 문자열을 전달할 수 있습니다. 안타깝게도 현재는 이처럼 매우 중요한 종류의 “감싸기를 통한 적응” 작업을 자동화하는 일반적이고 표준화된 방법이 없습니다.
오늘날 특정 프로토콜을 기대하는 컨텍스트에 객체를 전달할 때는 일반적으로 객체가 컨텍스트에 대해 알고 자체 래퍼를 제공하거나, 컨텍스트가 객체에 대해 알고 객체를 적절히 감쌉니다. 이러한 방식의 어려움은 그러한 적응이 일회성이고, 사용자 코드의 한 곳에 집중되어 있지 않으며, 공통된 기법으로 실행되지 않는다는 점 등입니다. 이러한 표준화의 부재는 같은 어댑터가 여러 곳에 나타나게 하여 코드 중복을 늘리거나, 클래스를 적응시키는 대신 다시 작성하도록 부추깁니다. 어느 경우든 유지 보수성이 떨어집니다.
객체가 특정 프로토콜을 준수하는지 검증하고, 바로 사용할 수 있는 래퍼가 있으면 그것을 제공하도록 호출할 수 있는 표준 함수가 있다면 매우 좋을 것입니다. 그 특정한 개별 사례에 맞는 주문을 찾으려고 각 라이브러리의 문서를 뒤질 필요 없이 말입니다.
요구 사항
객체의 프로토콜 준수 여부를 고려할 때 살펴보아야 할 경우가 몇 가지 있습니다:
- 프로토콜이 타입이나 클래스이고, 객체가 정확히 그 타입을 가지거나 정확히 그 클래스(서브클래스가 아님)의 인스턴스인 경우입니다. 이 경우 준수는 자동으로 이루어집니다.
- 객체가 프로토콜을 알고 있으며, 스스로를 준수한다고 간주하거나 자신을 적절히 감싸는 방법을 알고 있는 경우입니다.
- 프로토콜이 객체를 알고 있으며, 객체가 이미 준수하거나 프로토콜이 객체를 적절히 감싸는 방법을 알고 있는 경우입니다.
- 프로토콜이 타입이나 클래스이고, 객체가 서브클래스의 멤버인 경우입니다. 이는 위의 첫 번째 경우 (a)와 구별되는데, 상속이 (불행히도) 반드시 대체 가능성을 의미하지는 않으므로 신중하게 다루어야 하기 때문입니다.
- 컨텍스트가 객체와 프로토콜을 알고 있으며, 요구되는 프로토콜을 만족하도록 객체를 적응시키는 방법을 알고 있는 경우입니다. 이 경우 어댑터 레지스트리나 그와 비슷한 접근 방식을 사용할 수 있습니다.
위의 네 번째 경우는 미묘합니다. 서브클래스가 메서드의 시그니처를 변경하거나, 메서드 인자가 받아들이는 정의역을 제한하거나(인자 타입에 대한 “공변성(co-variance)”), 베이스 클래스가 결코 만들어 내지 않을 수도 있는 반환값을 포함하도록 공역을 확장하는 경우(반환 타입에 대한 “반공변성(contra-variance)”) 대체 가능성이 깨질 수 있습니다. 클래스 상속에 기반한 준수는 자동으로 이루어지는 것이 마땅하지만, 이 제안은 객체가 베이스 클래스 프로토콜을 준수하지 않는다는 사실을 알릴 수 있도록 허용합니다.
그러나 Python에 인터페이스를 위한 표준 “공식” 메커니즘이 생긴다면, “빠른 경로”인 경우 (a)를 프로토콜이 인터페이스이고 객체가 그 인터페이스를 준수한다고 주장하는 타입이나 클래스의 인스턴스인 경우까지 확장할 수 있으며, 또 그렇게 해야 합니다. 예를 들어 [3]에서 논의된 “interface” 키워드가 Python에 채택된다면, 인터페이스를 구현하는 인스턴스화 가능한 클래스는 대체 가능성을 깨뜨리는 것이 허용되지 않으므로 경우 (a)의 “빠른 경로”를 사용할 수 있습니다.
명세
이 제안은 새로운 내장 함수 adapt()를 도입하며, 이 함수는 이러한 요구 사항을 지원하는 기반이 됩니다.
adapt() 함수에는 세 개의 매개변수가 있습니다:
obj, 적응시킬 객체protocol, 객체에 요구되는 프로토콜alternate, 객체를 적응시킬 수 없을 때 반환할 선택적 객체
adapt() 함수가 성공하면, 객체가 이미 프로토콜을 준수하는 경우에는 전달된 객체 obj를 반환하고, 그렇지 않으면 프로토콜을 준수하는 객체의 뷰를 제공하는 보조 객체 wrapper를 반환합니다. 래퍼의 정의는 의도적으로 모호하게 두었으며, 필요하다면 래퍼가 자체 상태를 가진 완전한 객체여도 됩니다. 그러나 설계 의도는, 적응 래퍼가 자신이 감싸는 원래 객체에 대한 참조와 함께 (필요한 경우) 래퍼 객체에 위임할 수 없는 최소한의 추가 상태만을 보유해야 한다는 것입니다.
적응 래퍼의 훌륭한 예는 들어오는 문자열을 마치 텍스트 파일인 것처럼 읽을 수 있도록 적응시키는 StringIO 인스턴스입니다. 이 래퍼는 문자열에 대한 참조를 보유하지만, “현재 읽기 위치”(예를 들어 다음 “readline” 호출에서 읽을 문자들이 감싼 문자열의 어느 위치에서 오는지)는 스스로 처리합니다. 감싼 객체에 이를 위임할 수 없기 때문입니다(문자열에는 “현재 읽기 위치”라는 개념도, 그 개념과 조금이라도 관련된 그 밖의 어떤 것도 없습니다).
객체를 프로토콜에 적응시키지 못하면 AdaptationError(TypeError의 서브클래스)가 발생합니다. 단, alternate 매개변수를 사용한 경우에는 대신 alternate 인자가 반환됩니다.
요구 사항에 나열된 첫 번째 경우를 가능하게 하기 위해, adapt() 함수는 먼저 객체의 타입이나 객체의 클래스가 프로토콜과 동일한지 확인합니다. 동일하다면 adapt() 함수는 더 이상의 절차 없이 객체를 바로 반환합니다.
객체가 프로토콜을 알고 있는 두 번째 경우를 가능하게 하려면, 객체에 __conform__() 메서드가 있어야 합니다. 이 선택적 메서드는 두 개의 인자를 받습니다:
self, 적응되는 객체protocol, 요구되는 프로토콜
오늘날 Python의 다른 모든 특수 메서드와 마찬가지로, __conform__은 객체 자체가 아니라 객체의 클래스에서 가져오도록 되어 있습니다(모든 객체에 해당하며, “클래식 클래스”의 인스턴스는 그것을 계속 지원해야 하는 동안에는 예외입니다). 이를 통해 원한다면 향후 Python의 타입 객체에 ‘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의 타입으로 색인된 어댑터 팩토리 레지스트리를 확인하는 방식으로 대체합니다. 어댑터 팩토리는 그 레지스트리에 동적으로 등록되거나 제거될 수 있으며, 이를 통해 서로에 대해 알지 못하는 객체와 프로토콜에 대해 객체나 프로토콜 어느 쪽에도 침습적이지 않은 방식으로 “서드파티 적응”을 제공할 수 있습니다.
의도된 용도
adapt의 전형적인 의도된 용도는 인자로 전달되었거나 어떤 함수를 호출한 결과로 얻는 등 “외부로부터” 어떤 객체 X를 받았고, 그 객체를 특정 프로토콜 Y에 따라 사용해야 하는 코드에서 쓰이는 것입니다. Y와 같은 “프로토콜”은 인터페이스를 나타내기 위한 것으로, 보통 (“계약에 의한 설계” 접근 방식에서 흔히 사용되는 것과 같은) 몇 가지 의미론적 제약으로 보강되며, 흔히 (“특정 연산의 실행 시간이 O(N)보다 나빠서는 안 된다” 등과 같은) 몇 가지 실용적 기대도 함께 포함합니다. 이 제안은 프로토콜 자체를 어떻게 설계하는지, 프로토콜 준수 여부를 어떻게 검사하는지 또는 검사하기는 하는지, 그리고 준수한다고 주장하면서 실제로는 그렇지 않을 경우 어떤 결과가 생길 수 있는지를 규정하지 않습니다(메서드의 이름과 시그니처 같은 “구문적” 준수가 결여되면 흔히 예외가 발생하게 됩니다. “의미론적” 준수가 결여되면 미묘하고 어쩌면 간헐적인 오류로 이어질 수 있습니다[예를 들어 스레드 안전하다고 주장하지만 실제로는 미묘한 경쟁 상태에 놓이는 메서드를 생각해 보십시오]. “실용적” 준수가 결여되면 일반적으로 correctly 실행되지만 실제로 쓰기에는 너무 느린 코드가 되거나, 때로는 메모리나 디스크 공간 같은 자원이 고갈되는 결과로 이어집니다).
프로토콜 Y가 구체적인 타입이나 클래스일 때, 그것을 준수한다는 것은 객체가 Y의 인스턴스에 대해 수행할 수 있는 모든 연산을 “비슷한” 의미론과 실용성으로 허용한다는 뜻입니다. 예를 들어, 단일 연결 리스트인 가상의 객체 X는 list의 모든 메서드를 구현하더라도 ‘list’ 프로토콜을 준수한다고 주장해서는 안 됩니다. X[n]의 인덱싱에는 O(n) 시간이 걸리는 반면 같은 연산이 리스트에서는 O(1)이라는 사실이 차이를 만들기 때문입니다. 반면에 StringIO.StringIO의 인스턴스는 ‘file’ 프로토콜을 준수합니다. (‘marshal’ 모듈의 연산과 같은) 일부 연산은 명시적인 타입 검사를 수행하기 때문에 서로를 대체하는 것을 허용하지 않을 수도 있지만, 그러한 타입 검사는 프로토콜 준수의 관점에서 보면 “용인할 수 있는 범위 밖”의 것입니다.
이 관례 덕분에 이 제안의 목적상 구체적인 타입이나 클래스를 프로토콜로 사용하는 것이 가능해지지만, 그런 사용은 대개 최적이 아닙니다. ‘adapt’를 호출하는 코드가 특정 구체 타입의 기능을 모두 필요로 하는 경우는 드물며, file, list, dict처럼 기능이 풍부한 타입이라면 더욱 그렇습니다. 또한 그 모든 기능을 실용성이 좋으면서도 문법과 의미가 구체 타입과 정말로 동일한 래퍼로 제공할 수 있는 경우도 드뭅니다.
오히려 이 제안이 채택되고 나면, 현재 Python에서, 특히 표준 라이브러리 안에서 사용되는 프로토콜들의 본질적인 특성을 파악하고, 이를 일종의 “인터페이스” 구성 요소를 사용해 형식화하는 설계 작업을 시작해야 합니다(반드시 새로운 문법이 필요한 것은 아닙니다. 간단한 사용자 정의 메타클래스로 시작할 수 있으며, 그 작업의 결과는 나중에 Python 언어에 최종적으로 채택되는 어떤 “인터페이스” 구성 요소로든 옮겨갈 수 있습니다). 이처럼 더 형식적으로 설계된 프로토콜들을 갖추게 되면, ‘adapt’를 사용하는 코드는 ‘file’ 프로토콜에 대한 준수를 지나치게 포괄적으로 요구하는 대신, 예컨대 “읽기와 탐색이 가능한 파일류 객체”로의 적응이나, 그 밖에 구체적으로 필요한 무엇이든 적절한 수준의 “세분성”으로 요청할 수 있게 됩니다.
적응은 “캐스팅”이 아닙니다. 객체 X 자체가 프로토콜 Y를 준수하지 않을 때, X를 Y에 적응시킨다는 것은 X에 대한 참조를 보유하고, 주로 X에 적절한 방식으로 위임함으로써 Y가 요구하는 연산을 구현하는 일종의 래퍼 객체 Z를 사용한다는 뜻입니다. 예를 들어 X가 문자열이고 Y가 ‘file’이라면, X를 Y에 적응시키는 올바른 방법은 StringIO(X)를 만드는 것이며, file(X)를 호출하는 것이 아닙니다 [그렇게 하면 X라는 이름의 파일을 열려고 시도하게 됩니다].
다만 숫자 타입과 프로토콜은 이 “적응은 캐스팅이 아니다”라는 원칙의 예외가 되어야 할 수도 있습니다.
Guido의 “Optional Static Typing: Stop the Flames” 블로그 글
적응의 전형적이고 간단한 사용 사례는 다음과 같습니다:
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의 아이디어는 같은 블로그에 있는 Guido의 다른 제안들과 마찬가지로 이 제안과 완전히 호환됩니다.
참조 구현과 테스트 케이스
다음 참조 구현은 클래식 클래스를 다루지 않으며, 새 스타일 클래스만 고려합니다. 클래식 클래스를 지원해야 한다면 무엇을 추가해야 할지는 꽤 분명하지만, 다소 번거로울 것입니다(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()
Microsoft의 QueryInterface와의 관계
이 제안은 Microsoft(COM)의 QueryInterface와 몇 가지 유사점이 있지만, 여러 측면에서 다릅니다.
첫째, 이 제안에서 적응은 양방향이어서 인터페이스(프로토콜) 쪽에도 질의할 수 있으며, 이는 더 동적인(더 파이썬다운) 능력을 제공합니다. 둘째, 원래의 래핑되지 않은 객체의 정체성을 확인하거나 얻는 데 사용할 수 있는 특별한 “IUnknown” 인터페이스가 없습니다. 다만 이것을 그러한 “특별한” 공인 인터페이스 프로토콜 식별자 중 하나로 제안할 수는 있습니다. 셋째, QueryInterface에서는 객체가 특정 인터페이스를 한 번 지원하면 그 이후로도 항상 이 인터페이스를 지원해야 합니다. 이 제안은 그러한 보장을 하지 않는데, 특히 어댑터 팩토리를 레지스트리에 동적으로 추가했다가 나중에 다시 제거할 수 있기 때문입니다.
넷째, Microsoft의 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)와 같은 여러 차례의 “내부” 적응 과정을 함의할 수도 있습니다. 이 역시 충분히 강한 제약 조건하에서는 실현 가능할 수도 있지만, 그러한 방식이 실제로 어떤 영향을 미칠지는 이 제안의 작성자들에게 아직 분명하지 않습니다. 따라서 이 제안은 어떠한 상황에서도 적응의 자동적이거나 암묵적인 추이성을 포함하지 않습니다.
이 제안의 원래 버전을 구현한 것으로, 전이성과 상속의 효과 측면에서 더 고급 처리를 수행하는 구현은 Phillip J. Eby의 PyProtocols [5]를 참고하십시오. PyProtocols에 딸린 문서는 어댑터를 어떻게 코딩하고 사용해야 하는지, 그리고 적응이 어떻게 애플리케이션 코드에서 타입 검사의 필요성을 없앨 수 있는지에 대한 고찰을 담고 있어 충분히 공부할 가치가 있습니다.
질문과 답변
- Q: 이 제안은 어떤 이점을 제공합니까?
A: 전형적인 Python 프로그래머는 통합자(integrator), 즉 여러 공급자의 컴포넌트를 연결하는 사람입니다. 이러한 구성 요소들 사이를 인터페이스로 연결하려면 중간 어댑터가 필요한 경우가 많습니다. 보통은 프로그래머가 이 부담을 집니다. 한 컴포넌트가 노출하고 다른 컴포넌트가 요구하는 인터페이스를 살펴보고, 둘이 직접 호환되는지 판단하거나 어댑터를 개발해야 합니다. 때로는 공급자가 적절한 어댑터를 함께 제공하기도 하지만, 그 경우에도 어댑터를 찾고 배치하는 방법을 알아내는 데 시간이 걸립니다.
이 기법을 사용하면 공급자들이 필요에 따라
__conform__이나__adapt__를 구현하여 서로 직접 협력할 수 있습니다. 따라서 통합자는 어댑터를 직접 만들지 않아도 됩니다. 본질적으로 이 기법은 컴포넌트들이 자기들끼리 간단한 대화를 나눌 수 있게 합니다. 통합자는 한 컴포넌트를 다른 컴포넌트에 연결하기만 하면 됩니다. 타입이 자동으로 맞지 않는 경우에는 내장된 적응 메커니즘이 있습니다.더욱이 어댑터 레지스트리 덕분에 “제4자”가 어댑터를 제공하여, 서로의 존재를 전혀 모르는 프레임워크들이 비침습적으로 상호 운용되게 할 수 있습니다. 이때 통합자는 시작 시점에 적절한 어댑터 팩토리를 레지스트리에 설치하는 것 외에는 아무것도 할 필요가 없습니다.
라이브러리와 프레임워크가 여기서 제안하는 적응 인프라와 협력하기만 하면(본질적으로 프로토콜을 적절히 정의하여 사용하고, 받은 인자와 콜백 팩토리 함수의 결과에 필요에 따라 ‘adapt’를 호출하는 방식으로), 통합자의 작업은 그만큼 훨씬 단순해집니다.
예를 들어 SAX1과 SAX2 인터페이스를 생각해 보십시오. 둘 사이를 전환하려면 어댑터가 필요합니다. 보통은 프로그래머가 이 사실을 알고 있어야 합니다. 그러나 이 적응 제안이 도입되면 더 이상 그럴 필요가 없습니다. 실제로 어댑터 레지스트리 덕분에, SAX1을 제공하는 프레임워크와 SAX2를 요구하는 프레임워크가 서로의 존재를 모르더라도 이 필요성이 사라질 수 있습니다.
- Q: 왜 이것이 내장되어야 합니까? 독립적으로 둘 수는 없습니까?
A: 네, 독립적으로도 동작합니다. 그러나 내장되면 사용될 가능성이 더 커집니다. 이 제안의 가치는 주로 표준화에 있습니다. 즉, Python 표준 라이브러리를 포함하여 여러 공급자에게서 나온 라이브러리와 프레임워크가 적응에 대해 하나의 방식을 사용하게 하는 것입니다. 게다가 다음과 같은 이점도 있습니다.
- 이 메커니즘은 그 본질상 싱글턴입니다.
- 자주 사용된다면 내장 기능일 때 훨씬 빠를 것입니다.
- 확장 가능하며 아무것도 강요하지 않습니다.
- ‘adapt’가 내장되면 문법 확장을 지원할 수 있으며, 타입 추론 시스템에도 어느 정도 도움이 될 수 있습니다.
- Q: 왜
__conform__과__adapt__라는 동사를 사용합니까?A: conform, 자동사
- 형태나 성격이 상응하는 것, 곧 비슷한 것입니다.
- 조화나 합의에 맞게 행동하거나 그런 상태에 있는 것, 곧 따르는 것입니다.
- 현재의 관습이나 방식에 맞게 행동하는 것입니다.
adapt, 타동사
- 특정 용도나 상황에 적합하게 하거나 맞게 만드는 것입니다.
출처: 아메리칸 헤리티지 영어 딕셔너리(The American Heritage Dictionary of the English Language), 제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.