PEP 246 – 对象适配
- 作者:
- Alex Martelli <aleaxit at gmail.com>,Clark C. Evans <cce at clarkevans.com>
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建:
- 2001年3月21日
- Python 版本:
- 2.5
- 历史记录:
- 2001年3月29日,2005年1月10日
拒绝通知
我拒绝了这个 PEP。很快就会有更好的东西出现;现在说具体是什么还为时过早,但它不会与这个 PEP 中的提议过于相似,所以最好从一个新的 PEP 开始。GvR。
摘要
本提案提出了一种可扩展的协作机制,用于将传入的对象适配到期望支持特定协议(例如特定类型、类或接口)的上下文中。
本提案提供了一个内置的“adapt”函数,对于任何对象 X 和任何协议 Y,都可以使用它来请求 Python 环境提供一个符合 Y 的 X 版本。在幕后,该机制会询问对象 X:“你现在是,或者你知道如何包装自己以提供,协议 Y 的支持者吗?”。如果此请求失败,则该函数会询问协议 Y:“对象 X 是否支持你,或者你知道如何包装它以获得这样的支持者?”。这种二元性很重要,因为协议可以在对象之后开发,反之亦然,而本 PEP 允许两种情况都以非侵入的方式得到支持,而无需考虑预先存在的组件。
最后,如果对象和协议都不了解对方,则该机制可能会检查适配器工厂的注册表,其中可以动态注册能够将某些对象适配到某些协议的可调用对象。该提案的这部分是可选的:可以通过确保某些类型的协议和/或对象可以接受适配器工厂的动态注册来获得相同的效果,例如通过合适的自定义元类。但是,这部分可选内容允许以一种对协议或其他对象不造成侵入的方式使适配更灵活和更强大,从而为适配获得与 Python 标准库的“copy_reg”模块为序列化和持久化提供的类似优势。
本提案没有具体限制协议是什么,“符合协议”的确切含义,以及包装器应该做什么。这些遗漏是为了使本提案与现有的协议类别(例如现有的类型和类系统)以及为 Python 提出的或实现的许多“接口”概念(例如 PEP 245 中的接口、Zope3 中的接口[2],或 BDFL 在 2004 年底和 2005 年初的 Artima 博客中讨论的接口[3])兼容。但是,也包含了一些关于这些主题的思考,旨在启发而非规范。
动机
目前,Python 中没有用于检查对象是否支持特定协议的标准化机制。通常,某些方法的存在,特别是特殊方法(如 __getitem__
),被用作支持特定协议的指示器。此技术适用于 BDFL(终身仁慈独裁者)认可的少数特定协议。基于检查“isinstance”(内置类“basestring”专门存在以便您可以使用“isinstance”来检查对象“是否为[内置]字符串”)的替代技术也可以这么说。这两种方法都不容易且普遍地扩展到其他协议,这些协议由应用程序和第三方框架(而非标准 Python 核心)定义。
比检查对象是否已支持给定协议更重要的是获得对象合适的适配器(包装器或代理)的任务,如果支持尚不存在。例如,字符串不支持文件协议,但您可以将其包装到 StringIO 实例中以获得支持该协议并从其包装的字符串获取数据的对象;这样,您可以将字符串(经过适当包装)传递给需要可读文件作为参数的子系统。不幸的是,目前还没有通用的、标准化的方式来自动化这种极其重要的“通过包装进行适配”操作。
通常,今天,当您将对象传递到期望特定协议的上下文中时,对象会了解上下文并提供自己的包装器,或者上下文会了解对象并对其进行适当的包装。这些方法的难点在于,这种适配是一次性的,未集中在用户代码的单个位置,并且未以通用技术执行等。这种缺乏标准化会增加代码重复(相同的适配器出现在多个位置),或者鼓励重写类而不是适配。无论哪种情况,可维护性都会受到影响。
拥有一个标准函数将非常不错,可以调用该函数来验证对象的协议一致性,并在有现成包装器的情况下提供包装器——所有这些都无需遍历每个库的文档来查找适用于该特定情况的咒语。
需求
在考虑对象与协议的一致性时,需要检查以下几种情况
- 当协议是类型或类,并且对象恰好具有该类型或恰好是该类的实例(而非子类)时。在这种情况下,一致性是自动的。
- 当对象了解协议,并且自身认为符合协议,或者知道如何适当地包装自身时。
- 当协议了解对象,并且对象已符合协议,或者协议知道如何适当地包装对象时。
- 当协议是类型或类,并且对象是子类的成员时。这与上面的第一种情况 (a) 不同,因为继承(不幸的是)并不一定意味着可替换性,因此必须谨慎处理。
- 当上下文了解对象和协议,并且知道如何适配对象以满足所需的协议时。这可以使用适配器注册表或类似方法。
上面提到的第四种情况很微妙。当子类更改方法的签名,或限制方法参数接受的域(“参数类型协变”),或扩展协域以包含基类可能永远不会产生的返回值(“返回值逆变”)时,可能会发生可替换性的破坏。虽然基于类继承的一致性应该是自动的,但本提案允许对象发出信号表明它不符合基类协议。
但是,如果 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__
旨在从对象的类中获取,而不是从对象本身获取(对于所有对象,除了“经典类”的实例,只要我们仍然必须支持后者)。这使得将来可以根据需要将可能的“tp_conform”槽添加到 Python 的类型对象中。
对象可以通过返回自身作为 __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)”,或类似的);本提案没有规定协议本身是如何设计的,也没有规定如何或是否检查对协议的符合性,以及声称符合但实际上没有实现符合性(缺乏“语法”符合性——方法的名称和签名——通常会导致抛出异常;缺乏“语义”符合性可能会导致细微且可能偶尔的错误[例如,想象一个声称是线程安全但实际上存在一些细微的竞争条件的方法];缺乏“实用”符合性通常会导致代码“正确”运行,但对于实际使用来说速度太慢,或者有时会导致资源(如内存或磁盘空间)耗尽)。
当协议 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 意味着使用某种包装对象 Z,它持有对 X 的引用,并实现 Y 所需的任何操作,主要通过以适当的方式委托给 X。例如,如果 X 是一个字符串,而 Y 是“file”,则将 X 适配到 Y 的正确方法是创建一个 StringIO(X)
,**而不是**调用 file(X)
[这将尝试打开一个名为 X 的文件]。
但是,数值类型和协议可能需要成为此“适配不是强制转换”口号的例外。
Guido 的“可选静态类型:停止争论”博客文章
适配的一个典型的简单用例是
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 的关系
尽管本提案与微软的 (COM) QueryInterface 有一些相似之处,但它在许多方面有所不同。
首先,本提案中的适配是双向的,允许查询接口(协议),这提供了更动态的功能(更 Pythonic)。其次,没有特殊的“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 可以强加其等效项,因为它在某种程度上规定了对象、接口和适配器如何编码;本提案旨在不一定是侵入性的、可用的,并且可以“改造”两个在相互不知道对方的情况下编写的框架之间的适配,而无需修改任何一个框架。
适配的传递性实际上是有争议的,就像适配与继承之间的关系(如果有的话)一样。
如果我们知道继承总是意味着 Liskov 可替换性,那么后者就不会有争议,但不幸的是,我们不知道。如果某种特殊形式,例如 [4] 中提出的接口,确实可以确保 Liskov 可替换性,那么对于那种继承,我们也许可以断言如果 X 符合 Y 并且 Y 继承自 Z 则 X 符合 Z……但前提是可替换性以非常强的意义来理解,包括语义和实用性,这似乎令人怀疑。(就其价值而言:在 QueryInterface 中,继承既不需要也不暗示符合性)。本提案不包含继承的任何“强”影响,除了上面具体详细说明的小影响之外。
类似地,传递性可能意味着需要多次“内部”适配传递才能获得 adapt(X, Z)
的结果,通过某个中间 Y,本质上类似于 adapt(adapt(X, Y), Z)
,对于某个合适的和自动选择的 Y。同样,这在适当的强约束下可能是可行的,但这种方案的实际影响对于本提案的作者来说仍然不清楚。因此,本提案不包含任何自动或隐式的适配传递性,无论在何种情况下。
有关此提案原始版本的实现,该实现就传递性和继承的影响执行更高级的处理,请参阅 Phillip J. Eby 的 PyProtocols
[5]。随附 PyProtocols
的文档非常值得研究,因为它考虑了如何编码和使用适配器,以及适配如何消除应用程序代码中对类型检查的任何需求。
问答
- 问:本提案提供了什么好处?
答:典型的 Python 程序员是集成者,他们连接来自不同供应商的组件。通常,为了在这些组件之间进行接口,需要中间适配器。通常,程序员需要承担研究一个组件公开的接口和另一个组件所需的接口、确定它们是否直接兼容或开发适配器的负担。有时供应商甚至可能包含合适的适配器,但即使那样,搜索适配器并弄清楚如何部署适配器也需要时间。
此技术允许供应商通过实现
__conform__
或__adapt__
(根据需要)来直接协作。这使集成者无需自行创建适配器。从本质上讲,这允许组件之间进行简单的对话。集成者只需将一个组件连接到另一个组件,如果类型不自动匹配,则会内置一个适配机制。此外,由于适配器注册表的存在,“第三方”可以提供适配器,以允许完全彼此不了解的框架之间进行互操作,且不会造成侵入,并且集成者无需执行任何操作,只需在启动时将相应的适配器工厂安装到注册表中即可。
只要库和框架与这里提出的适配基础设施协作(本质上是通过适当定义和使用协议,并在需要时对接收到的参数和回调工厂函数的结果调用“adapt”),集成者的工作就会变得更加简单。
例如,考虑 SAX1 和 SAX2 接口:需要一个适配器来在它们之间切换。通常,程序员必须意识到这一点;但是,在此适配建议到位后,情况不再如此——事实上,由于适配器注册表的存在,即使提供 SAX1 的框架和需要 SAX2 的框架彼此不了解,此需求也可能被消除。
- 问:为什么必须内置,不能独立存在吗?
答:是的,它可以独立工作。但是,如果它是内置的,则它更有可能被使用。此建议的价值主要在于标准化:使来自不同供应商(包括 Python 标准库)的库和框架使用单一的方法进行适配。此外
- 该机制本质上是一个单例。
- 如果频繁使用,作为内置功能它会更快。
- 它是可扩展且不显眼的。
- 一旦“adapt”内置,它就可以支持语法扩展,甚至可以帮助类型推断系统。
- 问:为什么使用动词
__conform__
和__adapt__
?答:conform,不及物动词
- 在形式或特征上相对应;相似。
- 行动或意见一致;遵守。
- 根据当前的习俗或方式行事。
adapt,及物动词
- 使适合或适应特定用途或情况。
来源:《美国传统英语词典》第三版
向后兼容性
除非有人以其他方式使用了特殊名称__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 中关于接口和协议的使用和适配的建议、实现和文档的研究和反思。
参考文献和脚注
版权
本文件已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0246.rst
上次修改时间:2023-09-09 17:39:29 GMT