Python中的接口和协议

2021-03-20 09:05:45

你们中的一些人在键入中读到了我的上一篇文章.Procolols和ProbiCals想知道:“Zope.Interface怎么样?”我在过去强烈倡导它 - 现在我们有Mypy和协议,这是一个近期的遗物吗?我们可以完全用协议替换它吗?

在上一篇文章中,我讨论了结构与标称打字。在MyPy'Stype系统中,大多数类都是名字检查的,而协议是检查结构的。但是,另一种方式可以从Anormal类别不同:正常类是具体类型,协议是抽象的。

无法实例化:抽象类型的每个实例都是一些具体子类型的实例,

协议和接口都既摘要,但接口是标称值。两者之间的最高级别区别是当您有一个问题需要抽象类型时,但标称检查是优选的,界面是更好的解决方案。

Python的内置抽象底座aretechn上摘要 - 名义上,但它们在一个奇怪的半空间;它们是正式的“摘要”,因为它们无法实例化,但部分地混凝土中的情况下,它们可以包含任何数量的逻辑逻辑本身,从而使一个物体在所有通常的问题中拖累多个ABCS的Asubtpe。多重继承中的冲突名称空间。

从理论上讲,有一种方法可以将ABCS视为纯粹的摘要 - 这是使用ABCMETA.Register-但是,截至本文(3月2021),它不起作用,所以在“Python中的静态键入”的背景下,我们目前在“Python”的背景下忽略它。

第一个主要优势,即协议的首要优势是,由于它现在内置于Python本身,因此没有理由不使用它。当协议不那么存在时,无论添加显式抽象Typesto你的项目与Zope.Interface,它仍然有一个新的依赖,它仍然有一个新的依赖,所有可能意味着的次要头痛。

除了理论区别之外,还有一个问题upepports zope.interface的问题。有一些明显的差距;对Zope.Interface没有一个吨的内置IDE支持;较为复杂的Linterswill有时仍然抱怨接口不会像他们的第一次参数一样自我。实际上,Mypy本身默认做到这一点 - 虽然在片刻里更多的on ..较少的主流表现性能的职业验询?像PyRE和热情的类型检查不支持Zope.Interface,尽管它们缺乏对Zope.Interface的支持,但它只是宽度伸展性的秘方问题的一部分问题;他们也不能支持Qlalchemy或Django Orm而没有在工具本身的特殊套管。

但是,如果我们必须折扣abcmeta.register of topractical工具缺陷,即使它们原则上的内置方式提供了折扣,即使他们提供了一个内置的方式,我们需要能够在mypy中使用zope.interface。以及与协议的合理比较。我们?

幸运的是,是的!由于Shoobx,有一个相当积极维护的MyPyPlugin,支持Zope.Interface,您可以用来静态检查您的接口。

但是,此插件确实具有少量键合,如本撰写(再次,3月2021),这使其安全保证了一点较低质量的协议。

这一结果的净结果是协议具有“主场优势”最重要的情况;开箱即用,他们将与您现有的本人/ Linter设置更顺利,只要您的项目支持Python 3.6+,Atworst(如果您无法使用Python 3.7,那么协议建立在键入的情况下)要在键入_extensionspackage上拍摄类型核对时间依赖性,而使用zope.Interface,您需要运行时依赖项zope.interface本身和类型检查时间的MyPy插件。

因此,在两者都是相当等同的情况下,协议往往会赢得ByDefault。使用协议存在界面和协议overlap的无可否认的大领域,以及其中很多。但是,Zope.Interface闪耀的一些清晰的地方arestill。

首先,让我们来看看接口比协议更优雅地处理的案例:选择匹配简单的形状,其中形状对于其自身的含义。

该字符串是一个Stark数据结构,到处都是它有没有进程的重复。它是一个完美的隐藏信息的工具。

结构键入具有最大的优点的地方是当类型系统表现得足够足以在类型本身的结构内完全编码所需的突出的含义。考虑一个协议,该协议将可以在一起添加一些整数的对象:

这是一个相当明确的宣传本协议应该做的,并且任何人都可以看到这样的事情应该能够清楚地告诉发布的方法,以在一起添加几个整数;关于整数的影子没有什么可以隐藏的,没有约束类型系统不会让我们指定。如果任何没有预期行为的任何没有匹配此协议,那就会非常令人惊讶。

频谱的另一端,我们可能有一个具有很多隐藏结构的插件界面。对于此示例,我们有一个名为iplugin的接口,其中包含一个具有易于冲突的方法(“名称”)以其返回类型的非常特定的约束重载:字符串MustContain将Python对象的虚线路径名称在进口能力模块中(例如," os.path.join")。

Class iplugin(接口):def name() - > str:"返回要加载的东西的完全合格的Python标识符。"

通过协议,您可以通过手动制造更难匹配来解决这些限制;将元素添加到嵌入与其语义相关的名称的结构,从而使类型表现得更好像它更好地键入。

您可以长期且丑陋的方法(plugin_name_to_load,假设)或添加未使用的附加属性(yep_i_am_a_plugin = literal [true]),以便降低意外匹配的风险,但是这些WorksAnks看起来很喧嚣,他们必须手动命名方式;如果您希望Tomark将其作为与特定插件系统相关联的语义,则您将嵌入属性本身中该系统的名称;在这里,我们只是说“插件”,但如果我们想真正地小心,我们必须嵌入那里我们项目的oonle名称。

使用接口,每个实现的维护者必须明确选择,通过选择是否指定它们是@Implementer(iplugin).since他们必须从某个地方导入iplugin,这个注释是一个特定的,名称的语义意图声明:“我知道界面iplugin意味着什么,我保证我可以提供它“。

这是协议和接口之间最突出的区别:如果您有强烈的理由希望追随追随者以选择替代选择,则为Youwant界面;如果您希望它们自动匹配,则需要协议。

您可以说,一个对象直接提出的界面,允许某种级别的(至少运行时)类型安全,如果iplugin是.providedby的一些对象。

您可以通过协议执行大部分,但它是尴尬的。 @runtime_checkabledecorator允许您的协议使InInstance(x,myprotocol)类似于imyinterface.providedby(x),但是:

你仍然缺少直接缺少;运行时检查全部按类型,而不是实例的各个属性;

这不是默认值,因此如果您不是定义协议的那个,则无法保证您将能够使用它。

使用界面,Implementer(即,其实例适合指定的形状的类型)和Provider(可以适应指定形状的特定对象)之间也没有强制性关系。这意味着您可以获得ClassProvidesand ModuleProvides“免费”的功能。

接口特别适用于框架和应用码之间的通信。例如,假设您正在不断发展由时间 - eventHandler,eventHandler2,eventHandler3实现的界面的含义 - 这类似地命名和键入的方法,但对其生命周期或准确地将调用方法时对其进行巧妙的预期。面临此问题的框架可以使用接口系列,并在运行时检查,以查看这些应用程序的内容中的哪些,并确保应用程序已正确地采用新界面,并且不会发生串行对旧版本的方法名称。

最后,zope.Interface为您提供了适应和构建的,它是一个有用的机制,用于做模板,如模板,如标准库的一个更新版本的SingleSpatch。

适配器注册表是细微的,复杂的工具,不幸的是,一个示例据称,他们权力的完整效用本身就是相称的。但是,适应的核心是一个想法,如果你有anarbitrary对象x,你想要一个界面的提供商iy,你可以替换以下内容:

如果x已经提供IY(通过Inversioner,Provider,SenseProvides,ClassProvides或ModuleProvides),它只是退回;所以你不需要特殊情况,你已经得到了你想要的东西。

如果x具有__conform __(接口)方法,则将其称为IY作为界面,如果__Conform__返回任何内容,则不会从此返回该结果。

如果IY有一个专门定义的__Adapt__方法,它可以直接为此挂钩实现自己的逻辑。

zope.Interface的Adapter_hooks中的每个全局注册的函数都将调用以查找可以将X转换为IY提供程序的函数。 Twisted在此列表中有自己的全局注册表,这是一个registerAdapter操纵。

但从呼叫者的角度来看,你可以说“我想要一个IY”。

使用协议,您可以使用functools.singledispatchby验证函数返回协议类型并注册variooksypes进行转换。适配器注册表具有优势的地方是intheir中央性质和一致的成语,用于转换为目标类型;您可以以相同的方式使用适应任何界面,以及任何类型的CANParticate以上面列出的方式,通过灵活的机制在它的位置进行了灵活的选择掌握实现有意义,而转换为协议的任何SingleSispatch函数需要是定向酮的。

与接口不同,协议可以描述已有的内容的类型。要查看该优势时,请考虑庞大的应用程序,即吨图书馆并操纵3D空间数据点。

这些不同库中有一个惯例,其中它们都代表了一个“点”作为具有.x,.y和.z属性的对象,这些属性都是浮点数。鉴于域名,这是一种充分的足够的形状,大量的yourlibraries只是意外地符合它。您想要编写可以使用这些库中的任何数据输出的功能,只要它似乎是您自己的观点的概念,即可:

在这种情况下,定义协议的东西是您的应用程序;实现协议的换句话是您的库集合。由于您的英尺不知道应用程序 - 依赖arrowpoints的另一个方式 - 他们不能引用协议来注意,请注意它们的主题。

例如,假设我们正在实现“邮箱”类型模式,其中SomeSystems提供消息,其他系统稍后检索它们。要避免使用,请发送消息的系统不应该检索它们和viceversa - 接收器只接收,并且仅发送发件人。通过协议,WeCan描述了这一点而不具有任何新的仿真混凝土类型,如下所示:

从键入导入协议,typevar t_co = typevar(" t_co; t_co" covariant = true)t_con = typevar(" t_con",contrafariant = true)类发件人(协议[t_con]):def添加(self,项目:t_con) - >没有:"把一个物品放在插槽中。"类接收器(协议[t_co]):def pop(self) - > t_co:"从邮政信箱中检索一个项目。"

所有代码都只是告诉Mypy我们的意图;还没有行为。

从字面上没有我们所描述的工作已经没有我们自己的代码。然后我们使用这个吗?

def send(发件人:发件人[int]) - >没有:发件人。添加(3)DEF接收(接收器:接收器[INT]) - >无:接收器。 POP()接收器。添加(3)#mypy停止我们犯了这个错误:#"接收器[int]"没有属性"添加"发送(邮箱)接收(邮箱)

有关其初始实施,该系统在标准库中只需要在可排版中的任何类型不可用;只是一套。然而,通过分别将分析器和接收器视为发送者而不是集合,发送和接收,防止自身使用从已经通过的集合中传递的任何功能,除了它们各自的角色所呈现为“查看”的一个方法。因此,Mypy现在将告诉我们,如果有任何代码,该代码是否有哪些代码,该代码将尝试删除对象。

这使我们能够在库中使用现有数据结构,而没有对所有客户端的广告宣传的usualattentant问题,每个客户端的每个微小的实现都是publicInterface的预期一部分。 Python一直试图使这些区别无证或者说叙述,你所能依赖的东西,但它始终是击球(通常会错过)图书馆监考是否会看到那些劝诫;通过使其成为Programing Environment的一个特征,Mypy使忽略更难。

在现代python代码中,当您有一个抽象的行为集合时,您可能会考虑使用一个协议来描述它默认情况下。无论何种,接口还与Mypy支持一起保持最新的Python工具,它可以获得更多复杂的消费者希望支持标称打字,或者希望绘制其达到的rackaptation和组件注册功能集。