Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 的类装饰器——仍在形式化中,可能会发生变化。函数名称自然会受到漫长的“自行车棚”式辩论的影响。

履行你的角色

静态角色分配

让我们从定义 TreeDog 类开始

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 角色,从而允许它也能处理 WolfLaughingHyenaAibo [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 的实例将扮演 DoglikeFourLegs 两个角色。

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]。)

  1. 静态类角色分配
    @perform_role(Thieving)
    class Elf(Character):
      ...
    

    perform_role() 接受多个参数,因此以下也是合法的

    @perform_role(Thieving, Spying, Archer)
    class Elf(Character):
      ...
    

    Elf 类现在扮演 ThievingSpyingArcher 角色。

  2. 查询实例
    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 的内置 tuplefrozenset 类遵循此语义,因此可以将 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