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

Python 增强提案

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 的类装饰器——仍在规范化中,可能会发生变化。函数名称当然会经历漫长的讨论。

执行您的角色

静态角色分配

让我们从定义 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 实例执行三个角色:GuardMummysLittleDarling 是直接应用的,而 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

不需要装饰器来将方法标记为“抽象”,而且该方法永远不会被调用,这意味着它所包含的代码(如果有的话)与之无关。角色只提供抽象方法;具体的默认实现留给其他更合适的机制,例如混合类。

一旦你定义了一个角色,并且一个类声称执行该角色,那么就必须验证这个声明。这里,程序员拼错了角色所需的一种方法。

@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
    

    使用这个抽象基类——更确切地说是一个具体的混合类——允许程序员定义一组有限的运算符,并让混合类有效地“推导出”其他运算符。

通过结合这两个正交系统,我们能够 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 中列出的措辞旨在使角色系统能够作为一个独立的包发布,但可能需要为定义、分配和查询角色添加特殊的语法。一个例子可能是一个角色关键字,它将转换

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