流畅的Python(第二版)伴读之 1.1 Python数据模型[2/3]

魔术方法的用法

关于魔法方法,要首先理解它是由Python解释器调用而不应当被手动调用。例如,不能使用my_object.__len__(),而是使用len(my_object)让Python自动调用__len__。

然而解释器为内置的某些类型提供了一些快捷方式——例如list、str、bytearray,及他们的扩展如NumPy数组。Python的变长集合在底层的C代码中体现为PyVarObject结构体,它有个ob_size字段,储存了集合的长度。所以内置集合类型的 len(my_object) 直接取了ob_size的值,而不是遍历计数,这样会快的多。

多数情况下,魔术函数都是隐式调用。例如:for i in x: 这个语句实际上调用iter(x),iter(x)再调用x.__iter__(),如果x.__iter__()不存在,就会调用x.__getitem__(),在FrenchDeck例子中就是这种情况。

代码中通常不应当有很多魔术方法的直接调用,除非使用了大量的元编程。实务中,经常用到的魔术方法是__init__(),在这个方法内部可以调用父类的初始化方法或做一些对象初始化工作。

在用到了魔术方法的场景中,使用内置函数(len, iter, str等等)会更好。这些内置方法更完善,对于内置类型来说,这些函数运行更快。可参考17章中“在可调用对象上使用iter”一节的示例。

下一节展示魔术方法的如下重要用途:

  • 模拟数值类型
  • 对象的字符串表示
  • 对象的布尔值
  • 实现集合
模拟数值类型

一些魔术方法能让自定义类型使用类似“+”这样的操作符。在16章将会详细阐述,在此先管窥一斑。

我们定义一个数学和物理中常用的二维向量Vector(即欧几里得向量),可以实现一些数学运算(见图1-1)。

Tip:内置的复数类型(complex)也可以用作二维向量,但我们在此自定义的类可以扩展到N维,如17章所做的那样。

1-1 二维向量加法:Vector(2, 4) + Vector(2, 1)= Vector(4, 5)

我们为向量类设计的API将在交互式控制台上测试,实现如下功能,图1-1是其图形演示:

>>> v1 = Vector(2,  4)
>>> v2 = Vector(2,  1)
>>> v1 + v2
Vector(4, 5)

注意两个向量使用加号相加,其结果在控制台上输出的是一个高可读性的字符串。

内置的abs函数返回一个整型、浮点型或复数大小的绝对值,因此我们的API也要使用abs返回二维向量vector的长度大小(数学上也叫向量的模):

>>> v = Vector(3, 4)
>>> abs(v) 
5.0

还可以实现 “*” 操作符以进行点积运算(例如,向量和一个数字相乘等于另外一个向量,结果与原向量方向一致,长度等于原向量与数字的乘积)

>>> v * 3 
Vector(9, 12)
>>> abs(v * 3) 
15.0

例1-2是一个 Vector 类,通过使用魔术方法__repr__,__abs__,__add__和__mul__ 来实现以上描述的操作。

例1-2 简单的二维向量vector类

"""
vector2d.py
实现了一些魔术方法的二维向量vector类
本例仅做演示之用,未进行异常处理,尤其是__add__,__mul__方法,正式应用时应当进行异常处理。
本例在后文中将不断完善。

加法:
>>> v1 = Vector(2, 4)
>>> v2 = Vector(2, 1)
>>> v1 + v2
Vector(4, 5) 

绝对值:
>>> v = Vector(3, 4)
>>> abs(v)
5.0

点积:
>>> v * 3 
Vector(9, 12)
>>> abs(v * 3) 
15.0

"""
import math class Vector:

def __init__(self,  x=0,  y=0):
    self.x = x
    self.y  = y

def __repr__(self):
    return f'Vector({self.x!r}, {self.y!r})'

def __abs__(self):
    return math.hypot(self.x, self.y)

def __bool__(self):
    return bool(abs(self))

def __add__(self, other): 
    x = self.x +  other.x 
    y = self.y + other.y 
    return Vector(x, y)

def __mul__(self, scalar):
    return Vector(self.x * scalar, self.y * scalar)

除了__init__,还定义了5个魔术方法。但他们都没有在测试代码中直接调用。如前文所述,Python解释器才是魔术方法的使用者。

例1-2 定义了两个操作符 “+” 和 “*” 的实现方法__add__和__mul__。它们都返回一个新的vector对象,对原操作数不产生影响。这符合对数学运算的期望。在16章将继续聊这个问题。

警告:例1-2中允许Vector对象乘以一个数字,但是不允许数字乘以Vector对象,这不符合通常的乘法交换律,我们将在16章的__rmul__中解决这个问题。

字符串形式

魔术方法__repr__被内置的repr方法调用,返回一个对象的字符串表达形式。如果不自定义__repr__,Python控制台将Vector对象打印为 “ <Vector object at 0x10e100070>”。

交互式控制台和调试工具输出对象时也调用__repr__,就像字符串格式化中用 “%r”占位符的输出,或者在新式的f-string语法中str.format使用“!r”修饰符转换的内容。

注意__repr__方法体中,f-string语法使用了“!r”来得到各属性的字符串表现形式。这是有必要的,因为这能区分Vector(1, 2) 和 Vector('1', '2')——在本例中没能体现,因为x和y属性都是数字型。

f-string是最有用的语法之一,Python开发人员最起码要通过看文档或教程学习其常规用法。!r修饰符的作用是能显示出原始形态,例如 f'{ 1!r }' 和 f'{ ”1”!r }' 是不同的。

编写__repr__方法时,应该明确表达出对象的特征内容,一看就能辨别。最好是像本例那样,输出的字符串形式类似构造函数,能看出对象是如何构建的。

有时候不需要自己编写__repr__方法,继承自object的这个方法就可以用。例5-2演示了一些自定义__repr__方法的例子。

TIP:有其他语言经验的程序员可能经常使用toString方法,从而倾向于实现__str__而不是__repr__,但是在Python中,请首选实现__repr__。关于二者的不同,在StackOverflow论坛上,两位Python大佬Alex Martelli 和Martijn Pieters有精彩的说法。

自定义类型的布尔值

虽然有专门的布尔类型,但是其他类型的数据也可以当作布尔值来判断,例如用在 if 或 while 语句的判断条件中,还可以用and、or、not连接多个值。如果仅想检测x是真值还是假值,可以用bool(x),它返回True或False。

默认情况下,用户自定义类对象视为真值,但是可以通过__bool__或__len__魔术函数来改变。bool(x)在底层调用了x.__bool__()方法,如果__bool__没有定义,会尝试调用x.__len__(),如果返回0,就判断为False,否则判断为True。

Python中的一些类型转换为布尔值时与其他语言不同,例如list、tuple等集合,只要集合中没有元素,就返回False;而Java、C#、JavaScript等的语言中,认为空列表、空数组也是一个类对象,就会返回true。

我们实现的__bool__非常简单,vector的两个维度数值都是0(即向量长度是0),则返回False,否则返回True。__bool__方法需要返回一个布尔值,因此我们用bool(abs(self))将向量长度转换成布尔值。实际上在编程代码中可以直接把一个值用在需要判断真假值的语境中,所以很少用bool()转换函数。

更方便的实现代码可写成:

def __bool__(self):
    return bool(self.x  or self.y)

这种写法运行时省略了abs、平方、开方等运算。不过这样语义并不明确。代码中用bool的明确转换操作是必须的,因为__bool__方法需要一个布尔值作为返回值,如果不用bool函数转,它会返回对象的x属性值或y属性值。

运算符 or 和 and 表达式的最终结果并不一定返回布尔值,比如说:

x = 0 or 2,x的值是2,而不是True,它的逻辑是这样的:先看or的左边的操作数,如果是真值(注意True、非零实数、非空字符串等等都是真值),则返回左操作数,如果是假值(False、0、空字符串、None等都是假值),则返回右边操作数,注意是直接返回左操作数或右操作数,而不是把他们变成布尔值。这种效果在某些时候很有用。

 集合的API

图1-2显示了Python中基本集合类型的API。图中所有的类都是抽象基类(ABC——abstract base classes)。13章中介绍这些抽象基类和collections.abc内置模块。此处只从宏观上了解Python最重要的集合接口,即他们的魔术方法。

1-2 基础集合类型的UML图。图中斜体方法名是抽象方法,必须由其子类实现。其余常规字体的方法已经有实现代码,子类可以继承,也可以重载。

图中最上方的三个抽象基类都有一个魔术方法,抽象基类Collection(3.6版新增)中融合了这三个接口,应当在其子类中实现:

  • Iterable用来支持for语法、解包、以及其他迭代用途。
  • Sized用于支持内置的len方法
  • Container用于支持in操作符

Python不需要在类定义中明确继承抽象基类,便可以成为其子类。所有实现了__len__方法的类,都视为满足Sized接口。

这是Python和其他语言的重大区别之一,即“鸭子类型”:对于某接口,只要一个类是实现了接口规定的一些魔术方法,就相当于实现了接口,而不需要在类定义中明确继承关系,例如:

class S:   # 并没有使用 class S(Sized) 明确继承sized
    def __len__():
        return 0

s_obj = S()
isinstance (s_obj, Sized)  # 将会返回True

Java或C#程序员对此可能会感觉很惊讶,但是这就是鸭子类型:当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。一个对象能像使用一个类的方式处理数据,它就属于这个类型。

三个非常重要的Collection类是:

  • Sequence,统一了list和str等内置类型的接口
  • Mapping,作为dict、collection.defaultdict等类型的父类
  • Set,内置类型set和frozenset的接口类

只有Sequence类是有序的,它支持对子元素排序,而mapping和set不可以。

从3.7版本开始,dict也是有序的,但只保留了key插入的顺序,并不能重新排序。

在抽象基类Set中,魔术方法实现了二元运算符。例如__and__方法令语句 a&b 返回集合a和b的交集。

下两章介绍基础库sequences、mappings 和sets的详情。

下一篇对Python数据模型中的魔术方法进行分类整理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值