PEP 3133 – 引入角色
- 作者:
- Collin Winter <collinwinter at google.com>
- 状态:
- 拒绝
- 类型:
- 标准轨迹
- 需要:
- 3115, 3129
- 创建:
- 2007 年 5 月 1 日
- Python 版本:
- 3.0
- 历史记录:
- 2007 年 5 月 13 日
拒绝通知
这份 PEP 帮助推动了 PEP 3119 采用更理智、更简洁的方法。但考虑到 PEP 3119 的最新版本,我更喜欢这样。GvR。
摘要
Python 现有的对象模型根据对象的实现来组织对象。在像 Python 这样的基于鸭子类型的语言中,通常需要根据对象在更大的系统中扮演的角色(意图)而不是如何实现这些角色(实现)来组织对象。这份 PEP 介绍了角色的概念,它是一种根据对象的意图而不是实现来组织对象的机制。
基本原理
最初是对象。它们允许程序员将函数和状态结合起来,并通过多态性和继承等概念提高代码的可重用性,并且结果很好。然而,在某个时刻,继承和多态性已经不足够。随着狗和树的发明,我们不再满足于仅仅知道“它是否理解‘bark’?” 我们现在需要知道给定对象认为“bark”意味着什么。
一种解决方案是本文中详细介绍的角色,它是一种与传统类/实例系统正交且互补的机制。类关注状态和实现,而角色机制专门处理给定类中体现的行为。
这个系统最初被称为“特性”,并在 Squeak Smalltalk [4] 中实现。它后来被改编用于 Perl 6 [3],在那里它被称为“角色”,而且主要从那里开始,这个概念现在被解释用于 Python 3。Python 3 将保留“角色”这个名称。
简而言之:角色告诉你对象做什么,类告诉你对象怎么做。
在这份 PEP 中,我将概述一个用于 Python 3 的系统,它将使我们能够轻松确定给定对象对“bark”的理解是像树一样的还是像狗一样的。(也可能存在更严肃的例子。)
关于语法的说明
这份 PEP 中的语法提议只是初步的,应该被认为是稻草人。这份 PEP 所依赖的必要部分——即 PEP 3115 的类定义语法和 PEP 3129 的类装饰器——仍在规范化中,可能会发生变化。函数名称当然会经历漫长的讨论。
执行您的角色
静态角色分配
让我们从定义 Tree
和 Dog
类开始
class Tree(Vegetable):
def bark(self):
return self.is_rough()
class Dog(Animal):
def bark(self):
return self.goes_ruff()
虽然两者都实现了具有相同签名的 bark()
方法,但它们所做的事情却截然不同。我们需要某种方法来区分我们期望什么。依赖继承和简单的 isinstance()
测试会限制代码重用,或者强制任何类似狗的类继承自 Dog
,无论这是否合理。让我们看看角色是否能提供帮助。
@perform_role(Doglike)
class Dog(Animal):
...
@perform_role(Treelike)
class Tree(Vegetable):
...
@perform_role(SitThere)
class Rock(Mineral):
...
我们使用来自 PEP 3129 的类装饰器将特定角色与类关联起来。客户端代码现在可以验证传入对象是否执行了 Doglike
角色,允许它处理 Wolf
、LaughingHyena
和 Aibo
[1] 实例。
角色可以通过正常的继承进行组合
@perform_role(Guard, MummysLittleDarling)
class GermanShepherd(Dog):
def guard(self, the_precious):
while True:
if intruder_near(the_precious):
self.growl()
def get_petted(self):
self.swallow_pride()
这里,GermanShepherd
实例执行三个角色:Guard
和 MummysLittleDarling
是直接应用的,而 Doglike
是从 Dog
继承的。
在运行时分配角色
角色也可以在运行时分配,方法是解包装饰器提供的语法糖。
假设我们从另一个模块导入 Robot
类,并且由于我们知道 Robot
已经实现了我们的 Guard
接口,所以我们希望它也能与与警卫相关的代码配合使用。
>>> perform(Guard)(Robot)
这会立即生效,并影响所有 Robot
实例。
询问有关角色的问题
仅仅因为我们告诉我们的机器人军队他们是警卫,我们希望偶尔检查一下他们,并确保他们仍在执行任务。
>>> performs(our_robot, Guard)
True
那个那边的机器人怎么样?
>>> performs(that_robot_over_there, Guard)
True
performs()
函数用于询问给定对象是否满足给定角色。但是,它不能用于询问类其实例是否满足角色
>>> performs(Robot, Guard)
False
这是因为 Robot
类不能与 Robot
实例互换。
定义新角色
空角色
角色的定义方式与普通类相同,但使用 Role
元类。
class Doglike(metaclass=Role):
...
元类用于指示 Doglike
是一个 Role
,就像 5 是一个 int
并且 tuple
是一个 type
一样。
通过继承组合角色
角色可以从其他角色继承;这样做的效果是将它们组合起来。这里,Dog
的实例将同时执行 Doglike
和 FourLegs
角色。
class FourLegs(metaclass=Role):
pass
class Doglike(FourLegs, Carnivor):
pass
@perform_role(Doglike)
class Dog(Mammal):
pass
需要具体方法
到目前为止,我们只定义了空角色——没什么用。现在让我们要求所有声称执行 Doglike
角色的类都定义一个 bark()
方法
class Doglike(FourLegs):
def bark(self):
pass
不需要装饰器来将方法标记为“抽象”,而且该方法永远不会被调用,这意味着它所包含的代码(如果有的话)与之无关。角色只提供抽象方法;具体的默认实现留给其他更合适的机制,例如混合类。
一旦你定义了一个角色,并且一个类声称执行该角色,那么就必须验证这个声明。这里,程序员拼错了角色所需的一种方法。
@perform_role(FourLegs)
class Horse(Mammal):
def run_like_teh_wind(self)
...
这将导致角色系统引发异常,抱怨你缺少 run_like_the_wind()
方法。角色系统在类被标记为执行给定角色后立即进行这些检查。
具体方法需要完全匹配角色要求的签名。这里,我们试图通过定义 bark()
的具体版本来满足我们的角色,但我们有点走错了方向。
@perform_role(Doglike)
class Coyote(Mammal):
def bark(self, target=moon):
pass
这个方法的签名与 Doglike
角色所期望的签名不完全匹配,因此角色系统会发脾气。
机制
以下是如何在 Python 中表达角色的草案提议。这里的示例以角色机制可以在不更改 Python 解释器的情况下实现的方式来表达。(示例改编自 Curtis Poe 关于 Perl 6 角色的文章 [2]。)
- 静态类角色分配
@perform_role(Thieving) class Elf(Character): ...
perform_role()
接受多个参数,因此这在语法上也是合法的@perform_role(Thieving, Spying, Archer) class Elf(Character): ...
Elf
类现在同时执行Thieving
、Spying
和Archer
角色。 - 查询实例
if performs(my_elf, Thieving): ...
performs()
的第二个参数也可以是任何具有__contains__()
方法的对象,这意味着以下语法是合法的if performs(my_elf, set([Thieving, Spying, BoyScout])): ...
与
isinstance()
一样,对象只需要执行集合中的单个角色,表达式就为真。
与抽象基类之间的关系
这份 PEP 的早期草案 [5] 将角色设想为与 PEP 3119 中提出的抽象基类竞争。经过进一步的讨论和协商,达成了一种妥协,并对职责和用例进行了委派,具体如下
- 角色提供了一种指示对象语义和抽象能力的方法。角色可以定义抽象方法,但仅作为一种通过特定语义集访问的接口的划分方式。
Ordering
角色可能要求定义一组排序运算符。class Ordering(metaclass=Role): def __ge__(self, other): pass def __le__(self, other): pass def __ne__(self, other): pass # ...and so on
通过这种方式,我们能够指示对象在更大系统中的作用或功能,而不必限制或关注特定实现。
- 相反,抽象基类是一种重用通用、离散的实现单元的方法。例如,可以定义一个
OrderingMixin
,它根据其他运算符来实现多个排序运算符。class OrderingMixin: def __ge__(self, other): return self > other or self == other def __le__(self, other): return self < other or self == other def __ne__(self, other): return not self == other # ...and so on
使用这个抽象基类——更确切地说是一个具体的混合类——允许程序员定义一组有限的运算符,并让混合类有效地“推导出”其他运算符。
通过结合这两个正交系统,我们能够 a) 提供功能,以及 b) 提醒消费者系统此功能的存在和可用性。例如,由于上面的 OrderingMixin
类满足了 Ordering
角色中表达的接口和语义,我们说该混合类执行了该角色。
@perform_role(Ordering)
class OrderingMixin:
def __ge__(self, other):
return self > other or self == other
def __le__(self, other):
return self < other or self == other
def __ne__(self, other):
return not self == other
# ...and so on
现在,任何使用该混合类的类都会自动地——也就是说,无需程序员进一步操作——被标记为执行 Ordering
角色。
将关注点分离成两个不同的、正交的系统是可取的,因为它允许我们分别使用每个系统。例如,一个第三方包提供了一个 RecursiveHash
角色,表示一个容器在确定其哈希值时会考虑其内容。由于 Python 的内置 tuple
和 frozenset
类遵循这种语义,因此 RecursiveHash
角色可以应用于它们。
>>> perform_role(RecursiveHash)(tuple)
>>> perform_role(RecursiveHash)(frozenset)
现在,任何使用 RecursiveHash
对象的代码现在将能够使用元组和冻结集。
未解决的问题
允许实例执行与其类不同的角色
Perl 6 允许实例执行与其类不同的角色。这些更改仅限于单个实例,不影响该类的其他实例。例如
my_elf = Elf()
my_elf.goes_on_quest()
my_elf.becomes_evil()
now_performs(my_elf, Thieving) # Only this one elf is a thief
my_elf.steals(["purses", "candy", "kisses"])
在 Perl 6 中,这是通过创建一个匿名类来完成的,该类继承自实例的原始父类并执行额外的角色。这在 Python 3 中是可能的,但它是否可取还有待商榷。
包含此特性当然会使用 Python 表达查尔斯·狄更斯的作品变得容易得多。
>>> from literature import role, BildungsRoman
>>> from dickens import Urchin, Gentleman
>>>
>>> with BildungsRoman() as OliverTwist:
... mr_brownlow = Gentleman()
... oliver, artful_dodger = Urchin(), Urchin()
... now_performs(artful_dodger, [role.Thief, role.Scoundrel])
...
... oliver.has_adventures_with(ArtfulDodger)
... mr_brownlow.adopt_orphan(oliver)
... now_performs(oliver, role.RichWard)
要求属性
Neal Norwitz 请求使用与要求方法相同的机制,来断言属性存在。由于角色在类定义时生效,并且由于绝大多数属性是在运行时由类的 __init__()
方法定义的,因此似乎没有一个好方法可以在与方法相同的时间检查属性。
即使仅出于文档目的,在角色定义中包含未强制执行的属性可能仍然是可取的。
角色的角色
在提议的语义下,角色可以拥有自己的角色。
@perform_role(Y)
class X(metaclass=Role):
...
虽然这在技术上是可能的,但它是没有意义的,因为角色通常不会被实例化。一些脱机讨论围绕着赋予此表达意义,但迄今为止还没有出现好的想法。
class_performs()
目前无法询问一个类其实例是否执行了给定的角色。可能需要提供一个类似于 performs()
的函数,使得
>>> isinstance(my_dwarf, Dwarf)
True
>>> performs(my_dwarf, Surly)
True
>>> performs(Dwarf, Surly)
False
>>> class_performs(Dwarf, Surly)
True
更漂亮的动态角色分配
此 PEP 的早期草案包括一个单独的机制,用于将角色动态地分配给一个类。其语法为
>>> now_perform(Dwarf, GoldMiner)
相同的功能已经存在,可以通过解包装饰器提供的语法糖来实现
>>> perform_role(GoldMiner)(Dwarf)
问题在于,动态角色分配是否足够重要,以至于需要专门的语法。
语法支持
虽然此 PEP 中列出的措辞旨在使角色系统能够作为一个独立的包发布,但可能需要为定义、分配和查询角色添加特殊的语法。一个例子可能是一个角色关键字,它将转换
class MyRole(metaclass=Role):
...
为
role MyRole:
...
分配角色可以利用 PEP 3115 中提出的类定义参数。
class MyClass(performs=MyRole):
...
实现
参考实现即将推出。
致谢
感谢 Jeffery Yasskin、Talin 和 Guido van Rossum 在几个小时的面对面讨论中,帮助我们理清了角色和抽象基类的差异、重叠和细节。
参考文献
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-3133.rst
最后修改时间: 2023-09-09 17:39:29 GMT