《Effective Python 59》 是非常值得一读的Python进阶书籍,它阐述了Python语言中一些鲜为人知的微妙特性,并给出了能够改善代码功能及运行效率的习惯用法。相比其他Python书籍,这本书有以下几个特点:
- 从工程实践出发、以场景为主导阐述如何编写高质量、可维护的代码,并配套的代码范例。
- 内容不拖沓,省去很多基础语法。
- 不厚,只有二百多页(这一点至关重要)。
这是我看完之后做的一份读书笔记,列出来以供以后不时查阅。(还没写完,持续更新…)
用Pythonic方式来思考
bytes、str与unicode的区别
很多时候项目从Python2.x迁移到Python3.x会遇到字符编码的问题,原因是Pyhton2.x和Python3.x的字符编码不统一,具体如下:
Python3:有两种表示字符序列的类型:bytes
和str
:
bytes
:8个二进制位str
:unicode
字符
Python2:有两种表示字符序列的类型:str
和unicode
:
str
:8个二进制位unicode
:unicode
字符
unicode
$\rightarrow$ 二进制: 常见的编码方式是utf-8
,使用encode
方法
二进制$\rightarrow$unicode
: 使用decode
方法
在编程的时候,编解码放在接口外面来做。程序核心使用unicode字符,且不要对字符编码做任何假设。
1 | def to_str(bytes_or_str): |
在Python3中,涉及到文件处理的操作(使用内置的open函数)会默认的以UTF-8进行编码。而在Python2中默认采用二进制形式来编码。这也是导致很多意外事故发生的根源,特别是对于那些更习惯使用Python2的程序员而言。
比方说,将几个随机的二进制数据写入到一个文件中。在Python2中,下面的这段代码可以正常的工作,但是在Python3中却会报错并退出。
1 | import os |
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-4-71b20f96b6df> in <module>()
1 import os
2 with open('random.bin', 'w') as f:
----> 3 f.write(os.urandom(10))
TypeError: write() argument must be str, not bytes
导致这个异常发生的原因是在Python3中对于open函数又新增了一个名为encoding的参数。此参数默认为UTF-8。这样在文件句柄上进行read和write操作时,必须传入Unicode字符串的str实例,而不是包含了二进制数据的bytes实例。
用生成器表达式来改写数据量较大的列表推导
- 当输入的数据量较大时,列表推导可能因为占用太多内存而出问题。
- 使用圆括号构成生成器表达式,由生成器表达式所返回的迭代器,可以逐次产生输出值,从而避免内存占用问题。
- 把某个生成器表达式所返回的迭代器,放在另一个生成器表达式的
for
子表达式中,即可将二者组合起来。 - 串在一起的生成器表达式执行速度很快,果要把多种手法组合起来,以操作大批量数据,最好是用生成器表达式实现。
1 | # 这种类表推导只适合处理少量的输入值,对于大量数据,最好考虑生成器表达式而不是列表生成式 |
value: <generator object <genexpr> at 0x000001A6AC0D1308>
next value: 7
next value: 1
next roots: (6, 60)
尽量用enumerate代替range
enumerate
提供了一种精简的写法,可以在遍历迭代器时获知每个元素的索引。- 尽量用
enumerate
来改写那种将range
与下标访问相结合的序列遍历代码。 - 可以个
enumerate
提供第二个参数,以指定开始计数时所用的值(默认值为0)。
1 | # 第一种写法。不推荐 |
range写法,不推荐
1: vanilla
2: chocolate
3: pecan
4: strawberry
enumerate方法
1: vanilla
2: chocolate
3: pecan
4: strawberry
指定enumerate函数开始计数时所用的值
1: vanilla
2: chocolate
3: pecan
4: strawberry
用zip函数同时遍历两个迭代器
- 内置的
zip
可以平行地遍历多个迭代器。 - Python3中的
zip
相当于生成器,会在遍历过程中逐次产生元组,而Python2中直接把这些元组完全生成好,并一次性返回整份列表。 - 如果提供的迭代器长度不等,
zip
会提前终止。
1 | names = ['LiMing', 'LiLi', 'ZhangMei'] |
ZhangMei 8
1 | longest_name = None |
ZhangMei 8
不要在for和while循环后面写else块
- Python有种特殊写法,可在
for
和while
循环的内部语句块之后紧跟一个else
块。 - 但这种写法既不直观,又容易让人误解,应该避免这种写法。
合理利用 try/except/else/finally 结构中的每个代码块
try...finally...
这种结构简单的说是在try
下的全部操作如果某项失败的话就终止并执行finally
下定义的语句。如果全部操作都没有报错,那么最后也执行finally
下定义的语句,经常用于既要向上传播异常,又要在异常发生时执行某些清理操作。try...except...else...
- 可以混合使用。
1 | # try...finally... |
close handle
函数
尽量用异常来表示特殊情况,而不要返回None
- 返回
None
的来作为特殊的含义很出错,因为None
和其他的变量(例如zero
,空字符串)在条件表达式的判断下是等价的。 - 函数在遇到特殊情况时,应该抛出异常,而不是返回None。这样调用者就能够合理地按照函数中的说明文档来处理由此而引发的异常了。
用None
表示特殊情况如下:
1 | def divide(a, b): |
1.0
用异常表示特殊情况如下:
1 | def divide(a, b): |
Invlid inputs
第一种方法问题在于,Python程序员习惯用-
表示用不到的变量,那很有可能调用者会轻易的跳过元组的第一部。第二种方法让程序员不得不处理异常情况。
了解如何在闭包里使用外围作用域中的变量
假如有一份数字列表,要对其排序,但在排序时,要把出现在某个组群中的数字,放在组群外的那些数字之前,简单的实现如下:
1 | def sort_priority(values, group): |
[2, 3, 5, 7, -1, 1, 4, 6, 8]
上面程序能正常工作的原因是以下三个方面:
- Python支持闭包:闭包是一种定义在某个作用域中的函数,这种函数引用了那个作用域中的变量
- Python的函数是一级对象,也就是说,我们可以直接引用函数、把函数赋给变量、把函数当成参数
- Python使用特殊规则比较两个元祖,首先比较下标为0的对应元素,如果相等,再比较下标为1的对应元素,以此类推
若果增加一个功能,如果在数字出现在了组群中,返回一个标志,先试试下面这种写法:
1 | def sort_priority2(numbers, group): |
False
[2, 3, 5, 7, -1, 1, 4, 6, 8]
排序的结果是对的,但标志不对。解释如下:
当在表达式中引用变量的时候,Python解释器会按如下顺序遍历各作用域:
- 当前函数的作用域。
- 任何外围作用域(比如其他的包含着的函数)。
- 包含当前代码的模块域(也称之为全局作用域)。
- 内置域(包含了像len,str等函数的域)。
如果上述地方都没找到,就抛出异常。
当给变量赋值时,如果变量在当前作用域内已经被定义过,那么该变量会具备新值,如果当前作用域没有这个变量,Python会把这次的赋值行为视为对变量的定义
在Python3中有一种特殊的写法,能够获取闭包内的数据,我们可以用nonlocal
语句表明这样的意图。
1 | def sort_priority2(numbers, group): |
True
[2, 3, 5, 7, -1, 1, 4, 6, 8]
nonlocal
清楚的表明:如果在闭包内给该变量赋值,那么修改的其实是闭包外的那个作用域中的变量,nonlocal
不能延伸到模块级别- Python2中不支持
nonlocal
nonlocal
可能也会像全局变量一样遭到滥用,建议只在及其简单的函数中使用这种机制- 如果使用
nonlocal
的那些代码,已经写的越来越复杂了,那就应该将相关的状态封装成辅助类
下面定义的类与nonlocal
的功能相同
1 | class Sorter(object): |
True
[2, 3, 5, 7, -1, 1, 4, 6, 8]
考虑用生成器来改写直接返回列表的函数
如果函数要产生一系列结果,最简单的做法是返回一份列表。例如,想知道一个字符串中每个单词的首字母在句子中的位置。代码如下:
1 | def index_words(text): |
[0, 5, 11, 15, 21, 27]
这段代码的问题如下:
- 代码杂乱拥挤。每次都要调用
append
方法,而且要初始化result
,返回result
。 - 在返回前,要把所有的结果放在列表中,在输入量大的情况下,有内存崩溃的风险
下面这个生成器函数,返回一个迭代器,与之前的代码功能相同。
1 | def index_words(text): |
<generator object index_words at 0x000001A6AC0FA9E8>
[0, 5, 11, 15, 21, 27]
在参数上迭代时,要多加小心
- Python的迭代器协议,描述了容器和迭代器应该如何与
iter
和next
内置函数、for
循环函数及相关表达式相互配合。 - 把
__iter__
方法实现为生成器,即可定义自己的容器类型。 - 想判断某个值是迭代器还是容器,可以拿该值为参数,两次调用
iter
函数,若结果相同,则是迭代器。
当一个函数接收的参数是一个对象列表,那么很有可能要在这个列表上迭代。如下代码计算个体占总体的百分比。
1 | def normalize(numbers): |
[11.538461538461538, 26.923076923076923, 61.53846153846154]
如果将个体数据放在一份文件里,然后从文件中读取,我们定义read_vists
函数,然后定义一个生成器以便将数据应用到更大数据集上。
1 | def read_vists(path): |
[]
奇怪的是,以生成器返回的迭代器为参数,来调用normalize
,没有产生任何结果。原因是迭代器只能产生一轮结果,在sum
函数那里已经用完了。但之后在已经用完的迭代器上继续迭代时,没有报错。
一种解决方法是通过参数来接受另外一个函数,那个函数每次调用后,都能返回新的迭代器。代码如下。
1 | def normalize_func(numbers): |
这种方法显得生硬,很不Pythnic。另一种解决方法是新编一种实现迭代器协议的容器类。
实际上,当Python执行类似
for x in foo
这样的表达式的时候,它就会调用iter(foo)
。内置的iter
函数然后会调用foo.__iter__
方法。该方法返回一个迭代器对象,而那个迭代器对象,则实现了__next__
方法。然后循环语句会在迭代器对象上反复调用next
方法,直到产生StopIteration异常。
只需自己的类把__iter__
方法实现为生成器就满足上述要求。
1 | class ReadVisits(object): |
<generator object ReadVisits.__iter__ at 0x000001A6AC0D1C50>
normalize
的sum
方法会调用ReadVisits.__iter__
方法,之后的for
循环也会调用ReadVisits.__iter__
方法。
迭代器协议有这样的规定:如果把迭代器对象传给内置的
iter
函数,那么此函数会把该迭代器返回,反之,如果传给iter
的是个容器类型的对象,那么iter
函数则每次回返回新的迭代器对象,我们可以根据这种行为来判断输入值是不是迭代器对象本身,如果是,就抛出错误
下面是对第一个函数的完善。
1 | def normalize_defensive(numbers): |
[7.6923076923076925,
28.205128205128204,
33.97435897435897,
9.615384615384615,
20.512820512820515]
1 | it = read_vists('data.txt') |
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-32-df5cf9d8e72a> in <module>()
1 it = read_vists('data.txt')
----> 2 normalize_defensive(it)
<ipython-input-29-ce5cccbce26d> in normalize_defensive(numbers)
1 def normalize_defensive(numbers):
2 if iter(numbers) == iter(numbers):
----> 3 raise TypeError('Must supply a container')
4 total = sum(numbers)
5 result = []
TypeError: Must supply a container
用数量可变的位置参数减少视觉杂讯
令函数接受可选的位置参数(*args
),能够使代码更加清晰,并能减少视觉杂讯。
例如: 你想打印一些调试信息。假如该函数的参数个数固定不变,那它就必须接受一段信息及一份含有待打印值的列表。
1 | def log(message, values): |
My Number are: 11,22,33
但是如果没有values要打印的时候也必须传递一个空列表,这样使得代码既麻烦又杂乱。
我们在位置参数前加一个*
来实现可变参数
1 | def log(message, *values): |
My Number are: 12,33,44
My Number are: 12,33,44
My Number are
有两个应该注意的问题:
- 第一个是可变参数在被传递给函数的时候要转变成元组。这意味着如果调用者对生成器使用了
*
操作符,来调用这种函数,程序将会先把生成器迭代一轮,并把生成器锁生成的每一个值,都放在元组中,如果数据量巨大,可能消耗大量内存,并导致程序崩溃。 - 使用
*args
参数的话,如果以后再新增其他的位置参数,就必须修改原来调用该函数的那些旧代码,如果不更新调用代码,则会产生难以调试的错误。
用关键字参数表达可选的行为
- Python函数中的所有的位置参数都可以通过关键字来传值,关键字参数的顺序不限,只要把函数所要求的全部位置参数都指定好即可。
- 还可以混合使用关键字参数和位置参数来调用函数。
- 位置参数必须出现在关键字参数之前。
- 只使用位置参数来调用函数,可能导致这些参数值的含义不够明确,而关键字参数则能够阐明每个参数的意图。
- 给函数添加新的行为时,可以使用带默认值的关键字参数,以便于原有的调用代码保持兼容,关键字参数提供了一种扩充函数参数的有效方式。
- 可先的关键字参数,总是应该以关键字形式来指定,而不应该以位置参数的形式来指定。
1 | def remainder(number, divisor): |
6
1 | remainder(number=20, 7) |
File "<ipython-input-15-fa871e527313>", line 1
remainder(number=20, 7)
^
SyntaxError: positional argument follows keyword argument
用None和文档字符串来描述具有动态默认值的参数
- 参数的默认值,只会在程序加载模块并读到本函数的定义时评估一次,对于
{}
、[]
等动态的值,者可能导致奇怪的行为。 - 对于以动态值作为实际默认值的关键字参数来说,应该把形式上的默认值写为
None
,并在函数的文档字符串里面描述该默认值所对应的实际行为。
例如打印日志时想要加上打印的时间。
先看如下的写法:
1 | from datetime import datetime |
1 | log('Hi there!') |
2018-05-30 11:37:23.481681: Hi there!
2018-05-30 11:37:23.481681: Hi again!
从上面打印的信息来看,时间戳是相同的,这是因为datetime.now
仅仅被执行了一次,也及时它只在函数定义的时候执行了一次。
参数的默认值,仅仅在模块被加载进来的时候执行一次,而这通常发生在程序开始运行的时候。当模块已经加载完这段代码后,参数的默认值就不会被改变了。
在Python中如果想真正实现动态默认值,习惯上把默认值设为None
,并且在文档字符串中记录详细的行为和使用方法。当代码发现一个值为None
的参数的时候,就可以为其分配默认值了。
修改打印日志的函数,产生不同的时间戳,代码如下。
1 | def log(message, when=None): |
2018-05-30 11:43:57.224250: hi there!
2018-05-30 11:43:58.224661: hi again!
如果参数的实际默认值是可变类型(mutable),那就一定要记得用None
作为形式上的默认值。
例如实现一个功能:加载一个被编码为JSON的数据值,如果解码的时候失败了,你想默认返回一个空字典。代码如下:
1 | import json |
1 | foo = decode('bad data') |
{'stuff': 5, 'meep': 1}
{'stuff': 5, 'meep': 1}
我们本以为foo
和bar
会表示两份不同的字典,每份字典都有一个键值对,但以上代码的结果却是,修改一个的话很明显也会改变另一个。错误的根本原因是:foo
和bar
其实都等同于卸载default
参数默认值中的那个字典,它们表示的都是同一个字典对象。
由于default参数的默认值只在模块加载时执行一次,所以凡是以默认的空字典调用这个函数的代码,都将共享一份字典
1 | assert foo is bar |
解决办法就是对关键字参数设置默认值None并且记录在该函数的说明文档中。
1 | def decode(data, default=None): |
Foo: {'stuff': 5}
Bar: {'meep': 1}
用只能以关键字形式指定的参数来确保代码明确清晰
对于函数的某些关键变量,希望调用者明确改参数的值,使代码清晰,减少逻辑错误。此时,可以使用命名关键字参数。示例如下:
1 | def safe_division_c(number, division, *, ignore_overflow=False, |
ignore_overflow
和ignore_overflow
只能通过关键字参数赋值的形式被使用,而不是通过位置参数赋值的方式,也不能像默然参数那样省略。
类与继承
尽量用辅助类来维护程序的状态,而不是字典和元组
- 不要使用包含其他字典的字典,也不要使用过长的元组(元组里的元素超过两个,就应该考虑用其他办法实现)。
- 如果容器中包含简单又不可变的数据,那么可以先使用
namedtuple
来表示,待稍后有需要时,再修改为完整的类。 - 保存内部状态的字典如果变得复杂,那就应该吧这些代码拆解为多个辅助类。
namedtuple
是继承自tuple
的子类。namedtuple
创建一个和tuple
类似的对象,而且对象拥有可访问的属性。由于这种具名元组的属性都带有名称,所以当需求发生变化,以致要给简单的数据容器添加新的行为时,很容易就能从namedtuple
迁移到自己定义的类。
尽管namedtuple
在很多场合很有用,但它在有些场合使用反而不好。
namedtuple
类无法指定各参数的默认值。对于可选属性比较多的数据来说,namedtuple
用起来很不方便。namedtuple
实例的各项属性,依然可以通过下标及迭代访问。这可能导致其他人以不符合设计者意图的方式使用这些元组,从而使以后很难迁移成真正的类。
1 | from collections import namedtuple |
age: 21
sex: male
user1[0]: kongxx
简单的接口应该接受函数,而不是类的实例
- 对于连接各种Python组件的简单接口来说,通常应该给其直接传入函数,而不是先定义某个类,然后再传入该类的实例。
- Python中的函数和方法都可以像一级对象那样引用,因此,它们和其他类型的对象一样,也能够放在表达式里。
- 通过名为
__call__
的特殊方法,可以使类的实例能够像普通的Python函数那样得到调用。 - 如果要用函数来保存状态,那就应该定义新的类,并令其实现
__call__
方法,而不要定义带状态的闭包。__call__
方法强烈地暗示了该类的用途,它告诉我们,这个类的功能就相当于一个带有状态的闭包。
例如:Python内置的defaultdict
类,这个数据结构允许调用者提供一个函数,在查询本字典时,如果字典中没有待查询的键时,此函数返回一个默认值。而且为字典中的这个缺省键来返回一个默认值。提供像这样的函数接口,可以使得API更容易被构建和测试,因为它能够把附带的效果和确定的行为分开。
例如:现在我们要给defaultdict
传入一个产生默认值的挂钩函数,并令其统计出该字典一共遇到了多少个缺失的键。
第一种方式:带状态的闭包。
1 | from collections import defaultdict |
尽管defaultdict
并不知道missing
挂钩函数里保存了状态,但是运行上面的代码,依旧会产生预期的结果。
但上述方法读起来不够直观。
第二种方法:定义一个小的类,把想追踪的状态信息封装起来。
1 | class CountMissing(object): |
这种方法却是比increment_with_report
函数更加清晰,但是,单看这个类,依然不太容易理解CounMissing
的意图,CountMissing
对象由谁构造?missing
方法谁来调用?该类以后是否需要添加新的方法?直到你看到了使用它的defaultdict函数,你才会明白这些问题。
为了厘清这些问题,我们可以在Python代码中定义__call__
这个特殊的方法。该方法使对象能够像函数一样被调用。此外,如果把这样的实例传给内置的callable
函数,那么callable
会返回True
。
第三种方法:类中实现__call__
方法。
1 | class BetterCountMissing(object): |
用@classmethod形式的多态去通用地构建对象
- Python的每个类只能有一个构造器,也就是一个
__init__
方法。 - 使用
@classmethod
可以用一种与构造器相仿的方式构造类的对象。
参考链接:
https://stackoverflow.com/questions/12179271/meaning-of-classmethod-and-staticmethod-for-beginner
例子如下。首先我们有一个处理时间的类:
1 | class Date(object): |
如果我们要通过字符串创建Date
实例,此时我们要做如下操作:
- 将
str
的日期转为int
。 - 通过
int
的日期构建Date
1 | day, month, year = map(int, '11-09-2012'.split('-')) |
如果我们经常要使用字符串创建Date
实例,很显然如果能实现重载就更加方便,C++
有重载的方法,但Python没有重载,每个类只有一个构造器,只有一个__init__
方法,因此@classmethod
方法应运而生。
1 | class Date(object): |
cls
表示这个类本身
@staticmethod
与@classmethod
的区别是不构建类的实例,也不访问、依赖和改变类,只是一个函数。和普通的非class的method作用是一样的,只不过是命名空间是在类里面。一般使用场景就是和类相关的操作。
加入我们要验证日期的合法性,我们可以使用@staticmethod
,如下。
1 | class Date(object): |
用super
初始化父类
- Python采用MRO解决超类初始化次序以及菱形继承问题。
- 总是应该使用super来初始化父类。
初始化父类的传统方式是在子类中直接调用父类的__init__
方法。
1 | class MyBaseClass(object): |
这种方法对于单继承体系是可行的,但是如果子类收到多继承的影响,由于调用父类初始化方法的顺序并不固定,可能产生无法预知的行为(尤其在菱形继承体系下)。
super
函数定义了方法解析顺序(MRO
)以解决这一问题,MRO
以标准的流程来安排超类之间的初始化顺序:深度优先、从左至右,也保证了菱形继承中超类的初始化方法只执行一次。这个MRO
的顺序可以通过名为mro
的类方法类查询。
1 | from pprint import pprint |
Should be 5 * (5 + 2) = 35 and is 35
[<class '__main__.GoodWay'>,
<class '__main__.TimesFiveCorrect'>,
<class '__main__.PlusTwoCorrect'>,
<class '__main__.MyBaseClass'>,
<class 'object'>]
调用GoodWay(5)
的时候,它会调用TimesFiveCorrect.__init__
,而TimesFiveCorrect.__init__
又会调用PlusTwoCorrect.__init__
,PlusTwoCorrect.__init__
会调用MyBaseClass.__init__
。到达钻石顶部之后,所有的初始化过程会按照相反的顺序进行。
在Python3中将super
方法写法简化了,具体示例如下。
1 | class Implicit(MyBaseClass): |
只在使用Mix-in
组件制作工具类时进行多重继承
- 尽量避免多继承,能用
mix-in
组件实现的效果,就不要用多继承来做。
1 | class ToDictMixin(object): |
{'value': 10, 'left': {'value': 7, 'left': None, 'right': {'value': 9, 'left': None, 'right': None}}, 'right': {'value': 13, 'left': {'value': 11, 'left': None, 'right': None}, 'right': None}}
多用public
属性,少用private
属性
- 对Python来说,其属性的可见度只有两种,
public
和private
。以两个下划线__
开头的属性,是private
字段,本类的方法可以访问它们,类外直接访问会引发异常。 - 子类无法访问父类的
private
字段。 - Python会对私有属性做一些简单的变换,以保证
private
字段的私密性,例如:在MyObject
中定义self.__privated_file
,Python会以变换后的_MyObject__privated_file
保存,以保证私密性。所以如果我们以这种方式在类外访问私有属性,也可以访问到。Python为什么不从语法上对于私有属性严格保证呢?最简单的原因就是”We are all consenting adults here”。Python程序员相信开放要比封闭好。 - 为了尽量减少无意间访问内部属性所带来的意外,Python程序员应该遵守《Python风格指南》中建议,用一种习惯性的命名方式来表示这种字段:以单下划线开头的字段应该视为
protected
字段,本类之外的那些代码在使用这种字段的时候要多加小心。 - 由于在开发中,以后的代码可能需要从这个类上继承子类,并在子类中添加新的行为,假设超类使用了private属性,那么在覆写子类的时候就会遇到麻烦。虽然此时仍然可以通过第3条中所述的方式访问超类私有属性,但如果继承体系发生变化,
private
字段很可能失效,导致子类出现错误。 - 一般来说,宁可叫子类更多地访问超类的
protected
属性,也别把这些属性设为private
,我们应该在文档中说明每个protected
字段的含义,解释哪些字段是可供子类使用的内部api,哪些是不应该完全触碰的数据。
继承collections.abc
以实现自定义的容器类
- 如果要定制一个实现
list
等功能的简单子类,那就可以直接从Python的容器类型(如list
或dict
)继承。 - 想正确实现自定义的容器类型,可能需要编写大量的特殊方法。
- 编写自制的容器类型时,可以从
collections.abc
模块的抽象基类中继承,那些基类能够确保我们的子类具备适当的接口及行为。
如果要设计比较简单的序列,我们自然会想到继承Python的内置List
类型,如下我们要定义一种自定义的列表类型,并提供统计各元素出现频率的功能。
1 | class FrequencyList(list): |
Length is: 6
after pop: ['a', 'b', 'c', 'b', 'c']
{'a': 1, 'b': 2, 'c': 2}
现在要编写一个本身不属于list
子类,但是可以像list
一样通过下标来访问,并且可以使用len
来获取长度。
- Python用下标访问序列中的元素时,会把访问代码转译为:
xx.__getitem__(index)
,所以我们要想一个类可以用下标访问,只需在类中实现特殊方法__getitem__
方法即可。 - 想要是内置的
len
函数正常工作,就必须在自己定制的序列类型中实现一个名叫__len__
的特殊方法……
例如要令表示二叉树节点的类,也能通过下标访问节点,并且用len
能够得到长度信息。
1 | class BinaryNode(object): |
如果还要实现其他功能,那会是一件很麻烦的事。为了避免这些麻烦,我们可以使用内置的collections.abc
模块,该模块定义了一系列的抽象基类,它们提供了每一种容器所应当具备的常用方法。从这样的基类继承子类之后,如果忘记实现某个方法,那么collections.abc
模块就会指出这个错误。如果子类实现了抽象基类所要求的每个方法,那么基类就会自动提供剩下的那些方法。
1 | from collections.abc import Sequence |
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-10-997f61651b50> in <module>()
4 pass
5
----> 6 foo = BadType()
TypeError: Can't instantiate abstract class BadType with abstract methods __getitem__, __len__
1 | class IndexableNode(BinaryNode, Sequence): |