PEP 3133 – 引入角色 (Roles)
- 作者:
- 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)?”我们现在需要知道给定对象如何理解“叫”的含义。
一种解决方案,即这里详述的方案,是角色,这是一种与传统类/实例系统正交且互补的机制。类关注状态和实现,而角色机制则完全关注给定类所体现的行为。
该系统最初被称为“traits”,并为 Squeak Smalltalk 实现 [4]。此后,它被改编用于 Perl 6 [3],在那里被称为“roles”,并且主要是从那里,这个概念现在被解释用于 Python 3。Python 3 将保留“roles”这个名称。
简而言之:角色告诉你对象 *做什么*,类告诉你对象 *如何* 做。
在本 PEP 中,我将概述一个 Python 3 系统,该系统将能够轻松确定给定对象的“叫”的含义是树状的还是狗状的。(也可能有一些更严肃的例子。)
关于语法的说明
本 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
不需要装饰器来标记方法为“抽象”,并且该方法永远不会被调用,这意味着它包含的任何代码(如果有)都是无关紧要的。角色 *只* 提供抽象方法;具体的默认实现留给其他更适合的机制,例如混入(mixins)。
一旦定义了一个角色,并且一个类声称扮演该角色,就必须验证该声称。在这里,程序员错误地拼写了一个角色所需的方法。
@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
使用这个抽象基类——更准确地说,一个具体的混入(mixin)——允许程序员定义一组有限的运算符,让混入实际上“派生”出其他运算符。
通过结合这两个正交系统,我们能够 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 中提出的措辞是为了让角色系统可以作为一个独立包发布,但添加定义、分配和查询角色的特殊语法可能是可取的。一个例子可能是 role 关键字,它将被翻译为
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
最后修改: 2025-02-01 08:59:27 GMT