魔术方法全览
官方的《Python语言参考手册》中,数据模型章节列了80多个魔术方法。其中多半是为了实现算数运算符、按位运算符和比较运算符。可以参阅下表。
表1-1列出了除了二元操作符和算术运算符以外的魔术方法。本书用到了其中的大多数,包括Python新版本中的新功能,例如在3.5版中添加的异步魔术方法__anext__,3.6版本中添加的类自定义钩子__init_subclass__(此魔术方法在类被继承时调用)。
类别 | 魔术方法 |
---|---|
字符串/byte形式输出 | __repr__、__str__、__format__、__bytes__、__fspath__ |
类型转换 | __bool__、__complex__、__int__、__float__、__hash__、__index__ |
集合操作 | __len__、__getitem__、__setitem__、__delitem__、__contains__ |
迭代 | __iter__、__aiter__、__next__、__anext__、__reversed__ |
作为函数调用/并行 | __call__、__await__ |
上下文管理 | __enter__、__exit__、__aexit__、__aenter__ |
构造和析构 | __init__、__del__ |
属性访问 | __getattr__、__getattribute__、__setattr__、__delattr__、__dir__ |
属性描述符(常用于数据代理) | __get__、__set__、__delete__、__set_name__ |
抽象基类 | __instancecheck__、__subclasscheck__ |
元类相关 | __prepare__、__init_subclass__、__class_getitem__、__mro_entries__ |
表1-2是二元运算符和算术运算符及对应的方法。3.5版中增加的 matmul , rmatmul , __imatmul__用于支持矩阵乘法的“@”运算符,16章中详细介绍。
类别 | 运算符 | 对应的魔术方法 |
---|---|---|
一元算术运算 | -、+、abs() | __neg__,__pos__,__abs__ |
比较运算 | <、<=、==、!=、>、>= | __lt__,__le__,__eq__,__ne__,__gt__,__ge__ |
算术运算 | +、-、*、/、//、%、@、divmod()、round()、**、pow() | __add__,__sub__,__mul__,__truediv__,__floordiv__,__mod__,__matmul__,__div__,__mod__,__round__,__pow__ |
逆向运算 | 右操作数的算术运算符 | __radd__,__rsub__,__rmul__,__rtrue__,__div__,__rfloordiv__,__rmod__,__rmat__,__mul__,__rdivmod__,__rpow__ |
复合赋值 | +=、-=、*=、/=、//=、%=、@=、**= | __iadd__,__isub__,__imul__,__itrue__,__div__,__ifloordiv__,__imod__,__imat__,__mul__,__ipow__ |
逆向位运算 | 右操作数的位运算符 | __rand__,__ror__,__rxor__,__rlshift__,__rrshift__ |
位运算复合赋值 | &=、|=、^=、<<=、>>= | __iand__,__ior__,__ixor__,__ilshift__,__irshift__ |
在二元运算语句中,当第一个操作数(左操作数)不识别运算符时(此运算符没有对应的魔术方法),Python将会在第二个操作数(右操作数)上调用其逆向运算符对应的魔术方法。
例如 执行 a + b时:
- 如果 a 有 __add__ 方法, 而且返回值不是 NotImplemented, 调用a.__add__(b), 返回结果。
- 如果 a 没有 __add__ 方法, 或者其__add__ 方法返回NotImplemented时,检查 b 有没有 __radd__ 方法, 如果有, 而且没有返回 NotImplemented, 调用b.__radd__(a), 返回结果。
- 如果 b 没有 __radd__ 方法, 或者调用 __radd__ 方法返回NotImplemented,此语句最终抛出 TypeError, 并在错误消息中指明操作数类型不支持。
复合赋值运算符是运算和赋值两步合成一步的快捷方式,例如a+=b,先计算a+b的结果,然后把结果赋值给a。16章中详细讲述逆向运算符和复合赋值运算符。
len为什么不是方法
此标题的意思指的是:在Python中,类中不需具有被公开调用的获取长度的类方法,而统一使用内置函数len()(但是类内仍然需要实现支持len的魔术方法)。函数和方法的定义和区别应当属于OOP里的基本知识。
关于这个问题,Python的核心开发者Raymond Hettinger在2013年引用了《Python之禅》说:实用性比纯粹性更重要。“如何使用魔术方法”一节说过在内置类型上使用len(x)要快的多。CPython解释器运行len时不会调用任何方法,而是获取了底层C结构体的字段值。在str、list、memoryview等类型中查询元素个数是非常常见的操作,必须要高效。
另一方面,len不作为方法的原因是在Python数据模型中做了特殊处理。但是在自定义类型中,可以借助__len__魔术方法来支持len操作。这达到了内置对象访问的高效性和语法一致性的平衡,《Python之禅》还说过,特殊的情况也要照顾到规则。
如果把len或abs看作一元运算符或许更容易理解它的写法和用法。ABC语言是Python语言的始祖,在ABC语言中,有个类似len的运算符“#”,使用方法是“#s”(一元运算)或“x#s”(二元运算,返回x在s中出现的次数)。
章节回顾
通过魔术方法,自定义类型也可以像内置类型那样,更加Pythonic(Python风格)。
Python对象的基本规范之一是类应当提供两种字符串输出形式,一个用于调试和日志 ,另一个是给程序的使用用户看,所以有两个魔术方法来实现这个功能:__repr__、__str__。
如FrenchDeck例子中那样,模拟序列类型是用到魔术方法的典型案例。另外,数据库查询中返回的记录集,也类似序列类型。第2章介绍了内置的序列类型,12章实现了自定义的序列类型,将二维的Vector类扩展成多维。
通过运算符重载,Python提供了大量的准数值型类型,从内置类型到decimal.Decimal和fractions.Fraction,它们都支持二元算术运算符。Numpy是个经典的数学库,提供了矩阵和张量(Tensor,即广义的多维向量,机器学习中通常用到这个概念)。在16章增强Vector类的例子中会介绍逆向、复合赋值运算符等各种运算符重载。
后文中会陆续用到其他的魔术方法。
附 言
数据模型和对象模型?
Python文档中的“Python数据模型”,有很多人称为“对象模型”。Martelli、 Ravenscroft,、Holden著的《Python in a Nutshell》第三版和David Beazley著的《Python Essential Reference》第四版精彩地介绍了Python数据模型,但名称都使用了“对象模型”。维基百科上,对象模型的概念是:某种计算机语言中对象属性的集合。这也是Python数据模型的定义。此处采纳Python数据模型的说法,因为这也是Python官方参考文档对应章节的说法。
普通方法
《The Original Hacker’s Dictionary 》(黑客宝典)一书中“魔法”的定义是:无人理解,难以理解或仅为少数智者知晓。
Ruby社区也有魔术方法的说法。Python社区中很多人也用此术语。其实这些特殊的方法与魔法的本意正相反。Python和Ruby 为他们的用户提供了丰富的元对象协议,该协议有完整的文档记录,使像你我这样的麻瓜(《哈利波特》中对不懂魔法的普通人的称呼)能够像核心开发人员一样使用和扩展许多功能。
相比之下, Go语言中也内置了一些具有特殊功能的对象,但是无法在自定义类型中实现它们。例如,Go语言的数组、字符串和map类型支持使用 “[ ]” 下标访问元素,如 a[i]。但是,全新定义的自定义类型(即不是继承了上述内置类型)没有办法实现下标访问。更糟糕的是,Go 没有用户自定义可迭代接口或迭代器对象的概念,因此它的 for/range 语法仅限于支持五种内置了“魔法方法”的内置类型,包括数组、字符串和映射。
也许在未来,Go 的设计者会增强它的元对象协议。但目前,功能上比 Python 或 Ruby要有限得多。
元对象
元对象对于理解Python数据模型非常有用,其他语言也有类似概念。《The Art of the Metaobject Protocol (AMOP)》,即《元对象协议的艺术》一书可以帮助学习元对象,“元对象”一章说元对象是语言本身的一部分。而“协议” 相当于计算机学科常说的接口。因此,元对象协议就是对象模型的同义词,他们都指的是构成语言核心体系的API。
在Java、C#里常说的反射、自省,在Python里面实现起来异常简单,这是动态语言赋予的先天优势,其基础原理就是元编程。所谓元编程,可以理解为编程语言给程序员提供了一种机制,可以对类和实例(即对象)内部的属性和方法在运行时进行创建、修改或销毁。
丰富的元对象协议可以扩展语言,支持新的编程规范。Gregor Kiczales 是 《元对象协议的艺术》的第一作者,后来成为面向切面编程的先驱,也是 AspectJ 的最初作者,AspectJ 是实现面向切面编程规范的 Java 扩展。在像 Python 这样的动态语言中,面向切面编程要容易得多,一些框架可以做到这一点。zope.interface是其中的重要一员,它是构建 Plone内容管理系统(一个开源的内容管理系统应用程序)的一部分。