Python简单特性
项目描述
- 日期:
- 2015-08-16
- 版本:
- 0.5.3
- 下载:
- 许可证:
BSD
摘要
我为Python提供了一种简单实现特性作为可组合行为单元的方法。我认为特性比多重继承更好。实现基于特性的框架留作读者练习。
动机
多重继承是一个热门讨论的话题。多重继承的支持者声称它可以使代码更短,更容易阅读,而反对者则声称它会使代码更耦合,更难以理解。我过去花了一些时间面对Python中多重继承的复杂性,我曾经是其支持者之一;然而,自从那时起,我一直在使用大量使用多重继承的框架(我的意思是Zope 2),如今我已成为反对者之一。因此我对替代方案感兴趣。
近年来,在少数圈子中,"特性"的方法逐渐受到关注,我决定编写一个用于在Python中实现特性的库,以供实验之用。这个库是为框架构建者、那些在考虑基于多重继承(通常是通过公共混入方法)编写框架,但尚未确信这是最佳解决方案,并希望尝试替代方案的人设计的。这个库也是为那些对混入基框架不满意并希望将其框架转换为特性的人设计的。
特性是否比多重继承和混入更好?从理论上讲,我认为是这样的,否则我不会编写这个库,但在实践中(像往常一样),事情可能有所不同。使用特性和使用混入在实践上可能没有太大区别,或者范式转换可能不值得努力;或者相反也可能是真的。唯一的方法是尝试,基于特性构建软件,看看它在大型系统中的扩展性。在小规模上,几乎所有方法都可以正常工作:只有通过在大规模编程中,你才能看到差异。
这就是我以自由许可发布这个库的原因,让人们可以尝试并看看它的工作方式。这个库旨在与现有框架良好配合(在可能的情况下)。例如,我将在此展示如何将Tkinter类重写为使用特性而不是混入。当然,我并不提倡重写Tkinter:这将是愚蠢且无意义的;但重写你自己的框架使用特性可能是有意义(或不)的,也许是一个尚未发布的内部使用的框架。
为Python实现特性的人不止我一个;在完成我的实现后,我做了一些研究,发现了一些其他实现。我还发现了Enthought Traits框架,然而它似乎是用这个名字来指代完全不同的东西(即一种类型检查)。我的实现没有依赖项,代码简洁,我承诺未来也会保持其简洁性,遵循少即是多的原则。
这个模块背后还有一个隐藏的目标:普及Python对象模型的一些不太为人所知的高级特性。《strait》模块实际上是对Python元编程能力的致敬:这些特性通常与有着强大学术传统的语言相关联——如Smalltalk、Scheme、Lisp——但实际上Python对象模型并不逊色。例如,将对象系统从多重继承转换为基于特性的,可以在基本对象系统内部完成。这是因为Guido用来实现对象系统的特性(特殊方法钩子、描述符、元类)都是现成的,可供最终用户构建自己的对象系统。
此类特性在Python社区中通常使用较少,原因有很多:大多数人认为对象系统已经足够好,没有必要改变它;此外,对改变语言存在强烈的反对,因为Python程序员相信统一性和使用常见的惯例;最后,应用程序员很难找到一个这些特性有用的领域。一个例外是对象关系映射器领域,而Python语言经常被拉伸来模拟SQL语言,这一趋势的一个著名例子是SQLAlchemy)。然而,我从未见过像在strait模块中实现的那样扭曲的对象模型,所以我想要成为第一个进行这种滥用的人 ;)
什么是特性?
“特性”这个词有很多含义;我将参照 Traits - Composable Units of Behavior这篇论文中的定义,该论文在Squeak/Smalltalk中实现了特性。这篇论文发表于2003年,但支撑特性的大多数想法至少已经流传了30年。还有一个为PLT Scheme的特性实现,在精神上(如果不是实践中)与我这里所倡导的相近。你正在阅读的库绝不是Smalltalk库的移植:我只是从那篇论文中借鉴了一些想法来实现一个Pythonic的mixins替代品,由于没有更好的名称,我决定将其称为特性。我对与Smalltalk库保持一致性没有丝毫义务。这样做是在遵循一个悠久传统,因为很多语言使用“特性”这个名称,其含义与Smalltalk完全不同。例如,Fortress和Scala语言使用“特性”这个名称,但含义不同(Scala的特性与多重继承非常接近)。对我来说,一个特性是一组具有以下特性的方法和属性:
特性中的方法和属性逻辑上相互关联;
如果特性增强了一个类,则所有子类也会得到增强;
如果特性与类有共同的方法,则类中定义的方法具有优先级;
特性的顺序不重要,即先使用特性T1增强一个类,然后使用特性T2,或者反之亦然,效果相同;
如果特性T1和T2有相同的名称,同时使用T1和T2增强一个类将引发错误;
如果特性与基类有共同的方法,则特性的方法具有优先级;
一个类可以被视为特性的组合,也可以被视为一个均匀的实体。
从4到7的特性是特性与多重继承和mixins区分的特性。特别是,由于4和5,与方法的解析顺序相关的问题都消失了,重写永远不会是隐式的。第6条特性主要是不寻常的:在Python中,通常基类具有比mixins类更高的优先级。第7条特性应该理解为特性实现必须提供元数据支持,以使类作为原子实体和组合实体之间的过渡无缝。
实际例子
让我先展示如何将Tkinter类重写为使用特性而不是mixins。考虑由基类BaseWidget和mixins类Tkinter.Grid、Tkinter.Pack和Tkinter.Place派生的Tkinter.Widget类。我想通过使用特性来重写它。《strait》模块提供了一个名为include的工厂函数来完成这项工作。只需要用以下语法替换多重继承语法
class Widget(BaseWidget, Grid, Pack, Place):
pass
为以下语法
class Widget(BaseWidget):
__metaclass__ = include(Pack, Place, Grid)
我说从混入(mixins)到特性(traits)的转换很简单:但实际上我骗了你们,因为如果你尝试执行我刚才写的代码,你会得到一个 覆盖错误(OverridingError)
>>> from Tkinter import *
>>> class Widget(BaseWidget):
... __metaclass__ = include(Pack, Place, Grid)
Traceback (most recent call last):
...
OverridingError: Pack overrides names in Place: {info, config, configure, slaves, forget}
错误的原因很清楚:两个 Pack 和 Place 都提供了名为 {info, config, configure, slaves, forget} 的方法,特性实现无法确定该使用哪一个。这是一个特性,因为它迫使你明确指定。在这种情况下,如果我们想与多重继承规则保持一致,我们希望来自第一个类(即 Pack)的方法优先。这可以通过直接将这些方法包含在类命名空间中并依赖于规则3来实现
class TOSWidget(BaseWidget):
__metaclass__ = include(Pack, Place, Grid)
info = Pack.info.im_func
config = Pack.config.im_func
configure = Pack.configure.im_func
slaves = Pack.slaves.im_func
forget = Pack.forget.im_func
propagate = Pack.propagate.im_func
请注意,我们还需要指定 propagate 方法,因为它在 Pack 和 Grid 中是共有方法。
你可以通过以下方式检查 TOSWidget 类是否工作,例如定义一个标签小部件如下(请记住,TOSWidget 从 BaseWidget 继承其签名)
>>> label = TOSWidget(master=None, widgetName='label',
... cnf=dict(text="hello"))
你可以通过调用 .pack 方法来可视化小部件
>>> label.pack()
这应该会打开一个小窗口,里面有一个名为“hello”的消息。
一些注意事项和警告
首先,让我指出,尽管表面上看起来如此,include 并不返回一个元类。相反,它返回一个签名为 name, bases, dict 的类生成器函数
>>> print include(Pack, Place, Grid)
<function include_Pack_Place_Grid at 0x...>
此函数将使用适当的元类来创建类
>>> type(TOSWidget)
<class 'strait.MetaTOS'>
在简单情况下,元类将是 MetaTOS,即特性对象系统的主要类,但在一般情况下,它可以是不同的,不继承自 MetaTOS。关于 include 如何确定正确类的确切规则将在稍后讨论。
在这里我想强调,根据规则6,特性具有优先级高于基类属性。考虑以下示例
>>> class Base(object):
... a = 1
>>> class ATrait(object):
... a = 2
>>> class Class(Base):
... __metaclass__ = include(ATrait)
>>> Class.a
2
在常规多重继承中,你会通过在 Base 之前包含 ATrait 来做同样的事情,即
>>> type('Class', (ATrait, Base), {}).a
2
你应该注意不要混淆顺序,否则你会得到不同的结果
>>> type('Class', (Base, ATrait), {}).a
1
因此,如果你依赖于顺序,用特性替换混入类可能会破坏你的代码。小心!
特性对象系统
strait 模块的目标是修改标准的Python对象模型,将其转变为特性对象系统(简称TOS):TOS类与常规类表现不同。特别是TOS类不支持多重继承。如果你尝试从TOS类和另一个类进行多重继承,你会得到一个 TypeError
>>> class M:
... "An empty class"
...
>>> class Widget2(TOSWidget, M):
... pass
...
Traceback (most recent call last):
...
TypeError: Multiple inheritance of bases (<class '__main__.TOSWidget'>, <class __main__.M at 0x...>) is forbidden for TOS classes
这种行为是有意为之的:通过这个限制,你可以模拟一个理想的世界,在这个世界里,Python不支持多重继承。假设你想声称支持多重继承是一个错误,Python如果没有它会更好(这是我现在倾向于持有的观点):你如何证明这一点?只需编写不使用多重继承的代码,并且这些代码比使用多重继承的代码更清晰、更易于维护。
我发布这个特性实现,希望你能帮助我证明(或者可能反驳)这个观点。你可以将特性视为一种没有命名冲突、没有方法解析复杂性和有限方法合作的限制形式的多重继承。此外,当前实现比常规继承略少动态。
继承的一个优点是,如果您有一个从类 M 继承的类 C,并且您在运行时修改了 M 中的一个方法,那么在 C 创建并实例化之后,所有 C 的实例会自动获得该方法的新版本,这在调试过程中非常有用。这里提供的特质实现中失去了这个特性。实际上,在之前的版本中,我的特质实现是完全动态的,如果更改混入,实例也会更改。然而,我从未在实际中使用这个特性,它使实现复杂化并减慢了属性访问速度,所以我移除了它。
我认为这些限制是可以接受的,因为它们在简单性方面带来了许多优势:例如,super 变得非常简单,因为每个类只有一个超类,而我们都知道 Python 中的当前 super 非常复杂。
include 的魔法
由于 TOS 类的基本属性必须在继承下保持不变(即 TOS 类的儿子也必须是 TOS 类),因此实现必然需要元类。到目前为止,TOS 类的唯一基本属性是禁止多重继承,因此通常(但不总是)TOS 类是元类 MetaTOS 的实例,它实现了单继承检查。如果您从现有的类构建 TOS 层次结构,您应该了解 include 如何确定元类:如果您的基类是旧式类或普通的新式类(即 type 元类的直接实例),那么 include 将将其更改为 MetaTOS
>>> type(TOSWidget)
<class 'strait.MetaTOS'>
通常,您可能需要在具有非平凡元类的现有类之上构建基于特质的框架,例如 Zope 类;在这种情况下,include 足够智能,可以确定正确的元类。以下是一个示例
class AddGreetings(type):
"A metaclass adding a 'greetings' attribute for exemplification purposes"
def __new__(mcl, name, bases, dic):
dic['greetings'] = 'hello!'
return super(AddGreetings, mcl).__new__(mcl, name, bases, dic)
class WidgetWithGreetings(BaseWidget, object):
__metaclass__ = AddGreetings
class PackWidget(WidgetWithGreetings):
__metaclass__ = include(Pack)
include 自动生成正确的元类作为 AddGreetings 的子类
>>> print type(PackWidget).__mro__
(<class 'strait._TOSAddGreetings'>, <class '__main__.AddGreetings'>, <type 'type'>, <type 'object'>)
顺便说一下,由于 TOS 类保证在直系层次结构中,include 能够优雅地避免可怕的 元类冲突。
重要的是,_TOSAddGreetings 提供了与 MetaTOS 相同的功能,即使它不是其子类;另一方面,_TOSMetaAddGreetings 是 AddGreetings 的子类,它调用 AddGreetings.__new__,因此不会丢失 AddGreetings 提供的功能;在这个例子中,您可以检查问候属性是否已正确设置
>>> PackWidget.greetings
'hello!'
生成的元类名称自动从基元类名称生成;此外,维护一个生成的元类注册表,以便在可能的情况下重用元类。如果您想了解详细信息,欢迎您查看实现,与在真正的多重继承情况下解决元类冲突的一般方法相比,实现非常简短且简单。
协作特质
乍一看,特质对象系统似乎缺少了普通 Python 对象系统实现的多重继承的一个重要特性,即协作方法。考虑以下类
class LogOnInitMI(object):
def __init__(self, *args, **kw):
print 'Initializing %s' % self
super(LogOnInitMI, self).__init__(*args, **kw)
class RegisterOnInitMI(object):
register = []
def __init__(self, *args, **kw):
print 'Registering %s' % self
self.register.append(self)
super(RegisterOnInitMI, self).__init__(*args, **kw)
在多重继承中,LogOnInitMI 可以与其他类混合使用,使子类具有在初始化时记录日志的能力;同样,RegisterOnInitMI 也一样,它使其子类能够填充实例注册表。多重继承系统的重要特性是 LogOnInitMI 和 RegisterOnInitMI 之间能够很好地协同工作:如果您从两者中继承,则可以获得这两个特性。
class C_MI(LogOnInitMI, RegisterOnInitMI):
pass
>>> c = C_MI()
Initializing <__main__.C_MI object at 0x...>
Registering <__main__.C_MI object at 0x...>
如果盲目地使用特性对象系统,则无法获得相同的行为。
>>> class C_MI(object):
... __metaclass__ = include(LogOnInitMI, RegisterOnInitMI)
...
Traceback (most recent call last):
...
OverridingError: LogOnInitMI overrides names in RegisterOnInitMI: {__init__}
当然,这是一个特性,因为特性对象系统是为了避免命名冲突而设计的。然而,情况比这更糟糕:即使您尝试混合单个类,您也会遇到麻烦。
>>> class C_MI(object):
... __metaclass__ = include(LogOnInitMI)
>>> c = C_MI()
Traceback (most recent call last):
...
TypeError: super(type, obj): obj must be an instance or subtype of type
这里发生了什么?如果您注意到这里的 super 调用实际上是 super(LogOnInitMI, c) 类型的调用,其中 c 是 C 的一个实例,而 C 并不是 LogOnInitMI 的子类,那么这种情况就变得清晰了。这解释了错误信息,但并没有解释如何解决这个问题。看起来 TOS 类无法使用 super 来实现方法间的协作。
实际上并非如此:单继承协作是可能的,并且正如我们将在下一分钟所展示的,这是足够的。但在此之前,让我指出,我认为协作方法并不一定是好主意。它们很脆弱,并且会使所有类严格耦合。如果可能的话,我的建议是您不应使用基于方法协作的设计。话虽如此,确实存在一些情况(非常罕见)您确实希望实现方法协作。 strait 模块通过 __super 属性为这些情况提供支持。
让我解释它是如何工作的。当您将特性 T 混合到类 C 中时,include 会在 C 中添加一个属性 _T__super,这是一个将调度到 C 的超类属性的 super 对象。需要记住的重要一点是存在一个定义良好的超类,因为特性对象系统只使用单继承。由于层次结构是直接的,协作机制比多重继承中的协作机制更容易理解。以下是一个例子。首先,让我将 LogOnInit 和 RegisterOnInit 重写为使用 __super 而不是 super
class LogOnInit(object):
def __init__(self, *args, **kw):
print 'Initializing %s' % self
self.__super.__init__(*args, **kw)
class RegisterOnInit(object):
register = []
def __init__(self, *args, **kw):
print 'Registering %s' % self
self.register.append(self)
self.__super.__init__(*args, **kw)
现在您可以按照以下方式包含 RegisterOnInit 功能
class C_Register(object):
__metaclass__ = include(RegisterOnInit)
>>> _ = C_Register()
Registering <__main__.C_Register object at 0x...>
一切正常,因为 include 添加了正确的属性
>>> C_Register._RegisterOnInit__super
<super: <class 'C_Register'>, <C_Register object>>
此外,您还可以包含 LogOnInit 功能
class C_LogAndRegister(C_Register):
__metaclass__ = include(LogOnInit)
>>> _ = C_LogAndRegister()
Initializing <__main__.C_LogAndRegister object at 0x...>
Registering <__main__.C_LogAndRegister object at 0x...>
如您所见,合作机制运行得很好。我将这种旨在包含在其他类中使用并利用 __super 伎俩的类称为 合作特性。直接使用常规 super 的类不能用作合作特性,因为它必须满足继承约束。然而,将其转换为使用 __super 还是相当容易的。毕竟,strait 模块是为框架编写者设计的,所以它假设如果您想的话,可以更改框架的源代码。另一方面,如果您试图重用来自第三方框架的混合类并使用 super,您将不得不重新编写它的部分。这是不幸的,但我无法创造奇迹。
您可以将 __super 视为一种巧妙的方法来间接使用 super。请注意,由于层次结构是直接的,因此在核心语言级别有优化空间。纯 Python 中实现的 __super 伎俩利用了名称混淆机制,并紧密遵循著名的 autosuper 菜谱,并进行了一些改进。无论如何,如果您有两个具有相同名称的特性,您会遇到麻烦。为了解决此问题并获得更简洁的语法,需要语言提供更多支持,但 __super 伎俩对于原型设计来说已经足够好了,并且它有一个重大的优势,那就是它现在就可以为当前的 Python 工作。
元类级别的合作
在我的经验中,您需要多继承情况下方法合作的场景极其罕见,除非您是语言实现者或非常高级框架的设计师。在这样的领域,您需要合作方法;这并不是一个迫切的需求,从您可以没有它们而生存的意义上讲,但如果您关心优雅性和可扩展性,它们是一个很好的特性。例如,P. J. Eby 在这个 python-dev 线程 中指出
合作 super() 的一个主要用例是在元类的实现中。__init__ 和 __new__ 签名是固定的,多继承是可能的,合作是必需的(因为基类方法必须被调用)。我很难想到在过去五年或更长时间里我写的元类构造函数或初始化器中我没有使用 super() 来使其合作。在我看来,即使没有其他任何关于 super 需求的例子,这也是一个令人信服的用例。
我一直有同样的感觉。因此,尽管我多年来对多重继承不满意,但我不能完全摒弃它,因为担心这个用例。只有在我发现合作特性后,我才有足够的信心用它们取代多重继承,而不会失去我关心的任何东西。
当您戴着语言实现者的帽子时,元类级别的多重继承会再次出现。例如,如果您尝试基于特性实现一个对象系统,您将不得不在元类级别上这样做,并且方法合作有其位置。特别是,如果您查看 strait 模块的源代码——大约 100 行,这是对 Python 力量的致敬——您将看到 MetaTOS 元类被实现为一个合作特性,以便它可以与其他元类混合,在这种情况下,您正在与具有非平凡元对象协议的框架进行互操作。这是通过 include 内部完成的。
元类协作旨在让用户的生活更轻松。假设你们中的一些人,即strait
模块的用户,想使用来自第三方框架的另一个元类来增强include
机制,因此不继承自MetaTOS
class ThirdPartyMeta(type):
def __new__(mcl, name, bases, dic):
print 'Using ThirdPartyMeta to create %s' % name
return super(ThirdPartyMeta, mcl).__new__(mcl, name, bases, dic)
方法很简单。首先,你应该将MetaTOS
混合到第三方类中
class EnhancedMetaTOS(ThirdPartyMeta):
__metaclass__ = include(MetaTOS)
然后,你可以定义自己的增强型include
如下
def enhanced_include(*traits):
return include(MetaTOS=EnhancedMetaTOS, *traits)
在简单情况下,直接使用ThirdPartyMeta
可能有效,但我强烈建议即使在ThirdPartyMeta
中也用__super
替换super调用,以使协作更加稳健。
一些设计决策和未来工作的讨论
拥有不是MetaTOS
实例的TOS类的决策需要一些思考。那是我strait
版本0.1中的原始想法;然而,在版本0.2中,我想看看如果我使所有TOS类成为MetaTOS
的实例会发生什么。这意味着如果您的原始类有一个非平凡的元类,那么TOS类必须从原始元类和MetaTOS
继承,即需要在元类级别上实现多重继承和方法协作。
我不喜欢这个,因为我主张你可以不使用多重继承就做任何事情;此外,在元类级别上使用多重继承意味着必须以通用的方式解决元类冲突。我通过使用自己的烹饪配方做到了这一点,并且所有的测试都通过了。
然而,最终,在版本0.3中,我决定回到原始设计。元类冲突配方太复杂了,我认为这是一个代码恶臭 - 如果实现难以解释,那它就是一个坏主意 - 另一个表明多重继承是坏主意的迹象。在原始设计中,可以通过使用单一继承来避免冲突,将MetaTOS
的功能添加到原始元类中。
为此付出的代价是,现在TOS类不再是MetaTOS
的实例,但这不是问题:重要的是TOS类将按照MetaTOS
的规定在其特性上进行调度。此外,从Python 2.6开始,多亏了抽象基类,即使obj
不是cls
的实例,也可以通过注册一个合适的基类来满足isinstance(obj, cls)
检查(同样适用于issubclass
)。在我们的情况下,这意味着只需要将MetaTOS
注册为原始元类的基类。
版本0.4比当前版本复杂得多(尽管仍然很短,只有不到300行纯Python代码),因为它有一个更雄心勃勃的目标,那就是解决命名空间污染问题。我已经在其他地方讨论了这个问题elsewhere:如果你继续向类中注入方法(无论是直接还是通过继承),你最终可能会在相同级别上扁平化数百个方法。
一图胜千言,如果你想了解我想要避免的特质带来的恐怖,请看看PloneSite层次结构(该图片显示了每个类定义的非特殊属性的数量,方括号内):在Plone Site层次结构中,有38个类,88个重写名称,42个特殊名称,648个非特殊属性和方法。这是一个噩梦。
最初,我想阻止这种滥用,但这让我的实现更加复杂,而我的主要目标是保持实现简单。因此,这个版本假设了这样一个平凡的态度,即无论如何都无法阻止程序员进行不良设计,所以他们如果想走Zope的路,他们可以。
在之前的版本中,我确实为 include 提供了一些语法糖,以便可以编写如下内容(使用在此讨论的技巧)
class C(Base):
include(Trait1, Trait2)
在版本0.5中,我决定删除这个功能。现在,管道(即 __metaclass__ 钩子)对用户开放,一些魔法已经被移除,如果用户想要编写自己的 include 工厂,这将更容易。
从这里该走向何方?目前,我对未来没有明确的想法。特质的小型语言实现提供了方法重命名功能。Python实现在此方面没有提供任何功能。未来我可能会决定提供一些重命名支持,也可能不会。目前,您可以手动重命名方法。此外,未来我可能会决定添加某种适应机制,也可能不会:毕竟,这个实现的初衷是简洁性,我不想让它充斥着太多功能。
我非常欢迎反馈和批评:我发布这个模块,希望它在现实生活中得到应用,以便收集关于特质概念的经验。显然,我并不建议Python应该移除多重继承以支持特质:向后兼容性的考虑将使这个提议从一开始就遭到淘汰。我只是寻找一些愿意尝试特质的冒险者;如果实验成功,并且人们开始比现在更少地使用(多重)继承,我将感到很高兴。
趣事
strait 官方代表简单特质对象系统,然而这个名称也暗含了对“straight”(直的)这个字的玩笑,因为多重继承层次结构与TOS层次结构的不同之处在于TOS层次结构是直的。此外,没有人会阻止你认为 s 也代表Simionato ;)
项目详情
strait-0.5.3.tar.gz 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 8599dec6db3c2fda5ab2bb9ba844ae00ed2f3222e3f94f8140329c82f0eadf42 |
|
MD5 | 051a2446f0776a900d8675f0a61801bb |
|
BLAKE2b-256 | 5734232f0507738c5415b2d3add0ea396a534e7846410331096b92e50968f96f |