Effective Python --编写高质量Python代码的59个有效方法 (读书笔记 每日更新一条 Updated 2019.1.28)
好记性不如烂笔头,读读写写才能记得牢固些
第 1 章 用 Pythonic 方式来思考 1
第 1 条:确认自己所用的 Python 版本 1
2to3和six等工具可以吧Python2代码适配到Python3版本上。
- 有两个版本的 Python 处于活跃状态,它们是: Python 2 与 Python 3。
- 有很多种流行的 Python 运行时环境,例如, CPython、 Jython、 IronPython 以及
PyPy 等。 - 在操作系统的命令行中运行 Python 时,请确保该 Python 的版本与你想使用的
Python 版本相符。 - 由于 Python 社区把开发重点放在 Python 3 上,所以在开发后续项目时,应该优先考虑采用 Python 3。
第 2 条:遵循 PEP 8 风格指南 3
PEP 8
-
空白:
- 不要使用 tab 缩进,使用空格来缩进
- 使用四个空格缩进,使用四个空格对长表达式换行缩进
- 每行的字符数不应该超过 79
- class和funciton之间用两个空行,class的method之间用一个空行
- list索引和函数调用,关键字参数赋值不要在两旁加空格
- 变量赋值前后都用一个空格
-
命名
- 函数,变量以及属性应该使用小写,如果有多个单词推荐使用下划线进行连接,如lowercase_underscore
- 被保护 的属性应该使用 单个 前导下划线来声明。
- 私有 的属性应该使用 两个 前导下划线来进行声明。
- 类以及异常信息 应该使用单词 首字母大写 形式,也就是我们经常使用的驼峰命名法,如CapitalizedWord。
- 模块级 别的常量应该使用 全部大写 的形式, 如ALL_CAPS。
- 类内部的实例方法的应该将self作为其第一个参数。且self也是对当前类对象的引用。
- 类方法应该使用cls来作为其第一个参数。且self引用自当前类。
-
表达式和语句( Python之禅: 每件事都应该有直白的做法,而且最好只有一种 )
- 使用内联否定(如 if a is not b) 而不是显示的表达式(如if not a is b)。
- 不要简单地通过变量的长度(if len(somelist) == 0)来判断空值。使用隐式的方式如来假设空值的情况(如if not somelist 与 False来进行比较)。
- 上面的第二条也适用于非空值(如[1],或者’hi’)。对这些非空值而言 if somelist默认包含隐式的True。
- 避免将if , for, while, except等包含多个语块的表达式写在一行内,应该分割成多行。
- 总是把import语句写在Python文件的顶部。
- 当引用一个模块的时候使用绝对的模块名称,而不是与当前模块路径相关的名称。例如要想引入bar包下面的foo模块,应该使用from bar import foo而不是import foo。
- 如果非要相对的引用,应该使用明确的语法from . import foo。
- 按照以下规则引入模块:标准库,第三方库,你自己的库。每一个部分内部也应该按照字母顺序来引入。
第 3 条:了解 bytes、 str 与 unicode 的区别 5
- Python3 两种字符串类型:bytes和str,bytes表示8-bit的二进制值,str表示unicode字符。开发者不能用 > 或 + 混同操作bytes和str实例
- Python2 两种字符串类型:str和unicode,str表示8-bit的二进制值,unicode表示unicode字符。如果str只包含7位ASCII字符,那么通过相关的操作符才同时使用str与unicode
- 在对输入的数据进行操作之前,使用辅助函数来保证字符的序列
- 从文件中读取或者写入二进制数据时,总应该使用 ‘rb’ 或 ‘wb’ 等二进制模式来开启文件
第 4 条:用辅助函数来取代复杂的表达式 8
- 开发者很容易过度使用Python的语法特效,从而写出那种特别复杂并且难以理解的单行表达式
- 请把复杂的表达式移入辅助函数中,如果要反复使用相同的逻辑,那就更应该这么做
- 使用 if/else 表达式,要比使用 or 或者 and 这样的 Booolean 操作符更加清晰
第 5 条:了解切割序列的办法 10
- 分片机制自动处理越界问题,但是最好在表达边界大小范围是更加的清晰。(如a[:20] 或者a[-20:])
- list,str,bytes和实现
__getitem__
和__setitem__
这两个特殊方法的类都支持slice操作 - 基本形式是:somelist[start:end],不包括end,可以使用负数,-1 表示最后一个,默认正向选取,下标0可以省略,最后一个下标也可以省略
- slice list是shadow copy,somelist[0:]会复制原list,切割之后对新得到的列表进行修改不会影响原来的列表
- slice赋值会修改slice list,即使长度不一致(增删改)
第 6 条:在单次切片操作内,不要同时指定 start、 end 和 stride 13
- 既有start和end, 又有stride的切割操作,会让人感到困惑。
- 尽量使用stride为正数,且不带start或者end索引的切割操作。尽可能的避免在分片中使用负数值。
- 避免在分片中同时使用start,end,stride;如果非要使用,考虑两次赋值(一个分片,一个调幅),或者使用内置模块itertoolsde 的 islice方法来进行处理。
第 7 条:用列表推导来取代 map 和 filter 15
a = [1,2,3,4,5,6,7,8,9,10]
squares = [x*x for x in a]
squares = map(lambda x: x **2 ,a)
even_squares = [x**2 for x in a if x%2==0]
alt = map(lambda x: x**2, filter(lambda x: x%2==0,a))
assert even_squares== list(alt)
chile_rank = {'ghost':1,'habanero':2,'cayenne':3}
rank_dict = {rank:name for name,rank in child_rank.items()}
chile_len_set = {len(name) for name in rank_dict.values()}
- 列表表达式比内置的
map
,filter
更加清晰,因为map
,filter
需要额外的lambda
表达式的支持。 - 列表表达式允许你很容易的跳过某些输入值,而一个
map
没有filter
帮助的话就不能完成这一个功能。 - 字典和集合也都支持列表表达式
第 8 条:不要使用含有两个以上表达式的列表推导 16
matrix = [[1, 2, 3],[4, 5, 6],[7, 8, 9]]
squared = [[ x**2 for x in row] for row in matrix]
flat = [x for row in matrix for x in row]
my_lists = [
[[1, 2, 3],[4, 5, 6]],
# ...
]
# not good
flat = [ x for sublist in my_lists
for sublist2 in sublist
for x in sublist2]
# prefer
flat = []
for sublist in my_lists:
for sublist2 in sublist:
flat.append(sublist2)
- 列表表达式支持多层的循环和条件语句,以及每层循环内部的条件语句。
- 当列表表达式内部多余两个表达式的时候就会变得难于阅读,这种写法应该避免使用。
第 9 条:用生成器表达式来改写数据量较大的列表推导 18
列表生成式的缺点是,在推倒过程中,对输入列表中的每一个值,可能都要创建只包含一个元素的新列表。这对于小的输入序列可能是很好用的,但是大的输入序列而言就很有可能导致你的程序崩溃。
ython提供了一个generator expression(生成器表达式),在程序运行的过程中,生成其表达式不实现整个输出序列,相反,生成其表达式仅仅是对从表达式中产生一个项目的迭代器进行计算,说白了就是每次仅仅处理一个迭代项,而不是整个序列。
生成器表达式通过使用类似于列表表达式的语法(在()之间而不是[]之间,仅此区别)来创建。
it = ( len(x) for x in open('/tmp/my_file.txt'))
print(next(it))
roots = ((x,x**0.5) for x in it)
print(next(roots))
- 当遇到大输入事件的时候,使用列表表达式可能导致一些问题。
- 生成器表达式通过迭代的方式来处理每一个列表项,可以防止出现内存危机。
- 当生成器表达式 处于链式状态时,会执行的很迅速。
第 10 条:尽量用 enumerate 取代 range 20
# good
for i, flavor in enumerate(flavor_list):
print(‘%d: %s’ % (i + 1, flavor))
# not good
for i in range(len(flavor_list)):
flavor = flavor_list[i]
print(‘%d: %s’ % (i + 1, flavor))
-
enumerate
提供了简洁的语法,再循环迭代一个迭代器的同时既能获取下标,也能获取当前值。 - 可以添加第二个参数来指定索引开始的序号,默认为
0
第 11 条:用 zip 函数同时遍历两个迭代器 21
names = ['Cecilia','Lise','Marie']
letters = [len(n) for n in names]
for name, count in zip(names, letters):
if count > max_letters:
longest_name = name
max_letters = count
- 内置的zip函数可以并行的对多个迭代器进行处理。
- 在Python3中,zip 相当于生成器,会在遍历过程中逐次产生元组。而在Python2中,zip返回的是一个包含了其处理好的所有元祖的一个集合。
- 如果所处理的迭代器的长度不一致时,zip会默认截断输出,使得长度为最先到达尾部的那个长度。
- 内置模块itertools中的zip_longest函数可以并行地处理多个迭代器,而可以无视长度不一致的问题。
第 12 条:不要在 for 和 while 循环后面写 else 块 23
for i in range(2):
print('Loop %d' % i)
else:
print('Else block')
>>>
Loop 0
Loop 1
Else block
-
Python
有用特殊的语法能够让else
语块在循环体结束的时候立刻得到执行。 - 循环体后的
else
语块只有在循环体没有触发break
语句的时候才会执行。 - 避免在循环体的后面使用
else
语块,因为这样的表达不直观,而且容易误导读者。
第 13 条:合理利用 try/except/else/f inally 结构中的每个代码块 25
UNDEFINED = object()
def divide_json(path):
handle = open(path, 'r+') # May raise IOError
try:
data = handle.read() # May raise UnicodeDecodeError
op = json.loads(data) # May raise ValueError
value = (op['numerator'] / op['denominator']) # May raise ZeroDivisionError
except ZeroDivisionError as e:
return UNDEFINED
else:
op[‘result’] = value
result = json.dumps(op)
handle.seek(0)
handle.write(result) # May raise IOError
return value
finally:
handle.close() # Always runs
- try/finally组合语句可以使得你的代码变得很整洁而无视try块中是否发生异常。
- else块可以最大限度的减少try块中的代码的长度,并且可以可视化地辨别try/except成功运行的部分。
- else块经常会被用于在try块成功运行后添加额外的行为,但是要确保代码会在finally块之前得到运行。
finally 块: 总是会执行,可以用来关闭文件句柄之类的
else 块 : try 块没有发生异常则执行 else 块,有了 else 块,我们可以尽量减少 try 块的代码量
第 2 章 函数 28
第 14 条:尽量用异常来表示特殊情况,而不要返回 None 28
- 返回
None
的函数来作为特殊的含义是容易出错的,因为None
和其他的变量(例如zero
,空字符串)在条件表达式的判断情景下是等价的。 - 通过触发一个异常而不是直接的返回
None
是比较常用的一个方法。这样调用方就能够合理地按照函数中的说明文档来处理由此而引发的异常了。
def divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
raise ValueError('Invalid inputs') from e
# use like this:
x, y = 5, 2
try:
result = divide(x, y)
except ValueError:
print("Invalid inputs")
else:
print("Result is %.1f"% result)
第 15 条:了解如何在闭包里使用外围作用域中的变量 30
Python支持闭包。闭包是一种定义在某个作用域中的函数,这种函数引用了作用域中的变量。
Python的函数是一级对象,我们可以直接引用函数,把函数赋值给变量、当成参数传递给其它函数等等。。
Python编译器变量查找域的顺序:
- 当前函数的作用域
- 任何其他的封闭域(比如其他的包含着的函数)。
- 包含该段代码的模块域(也称之为全局域)
- 内置域(包含了像len,str等函数的域)
# 优先排序
def sort_priority2(values, group):
found = False # 作用域:sort_priority2
def helper(x):
if x in group:
found = True # 作用域: helper
return (0, x)
return (1, x) # found在helper的作用域就会由helper转至sort_priority2函数
values.sort(key=helper)
return found
-
获取闭包中的数据-Python3
def srt_priority3(numbers, group): found = False def helper(x): nonlocal found # 表明found是闭包外数据 if x in group: found = True return (0, x) return (1, x) numbers.sort(key=helper) return found
当数据在闭包外将被赋值到另一个域时,nonlocal 语句使得这个过程变得很清晰。它也是对global语句的一个补充,可以明确的表明变量的赋值应该被直接放置到模块域中。
然而,像这样的反模式,对使用在那些简单函数之外的其他的任何地方。nonlocal引起的副作用是难以追踪的,而在那些包含着nonlocal语句和赋值语句交叉联系的大段代码的函数的内部则尤为明显。
当你感觉自己的nonlocal语句开始变的复杂的时候,我非常建议你重构一下代码,写成一个工具类。这里,我定义了一个实现了与上面的那个函数功能相一致的工具类。虽然有点长,但是代码却变得更加的清晰了(详见第23项:对于简单接口使用函数而不是类里面的
__call__
方法)class Sorter(object): def __init__(self, group): self.group = group self.found = False def __call__(self, x): if x in self.group: self.found = True return (0, x) return (1, x) sorter = Sorter(group) numbers.sort(key=sorter) assert sorter is True
-
Python2中的作用域
Python2
是不支持nonlocal
关键字的。为了实现相似的功能,你需要广泛的借助于Python
的作用与域规则。虽然这个方法并不是完美的,但是这是Python
中比较常用的一种做法。# Python2 def sort_priority(numbers, group): found = [False] def helper(x): if x in group: found[0] = True return (0, x) return (1, x) numbers.sort(sort=helper) return found[0]
就像上面解释的那样,Python 将会横向查找该变量所在的域来分析其当前值。技巧就是发现的值是一个易变的列表。这意味着一旦检索,闭包就可以修改found的状态值,并且把内部数据的改变发送到外部,这也就打破了闭包引发的局部变量作用域无法被改变的难题。其根本还是在于列表本身元素值可以被改变,这才是此函数可以正常工作的关键。
当found为一个dictionary类型的时候,也是可以正常工作的,原理与上文所言一致。此外,found还可以是一个集合,一个你自定义的类等等。
要点:
- 闭包函数可以从变量被定义的作用域内引用变量。
- 默认地,闭包不能通过赋值来影响其检索域。
- 在Python3中,可以使用nonlocal关键字来突破闭包的限制,进而在其检索域内改变其值。(global 关键字用于使用全局变量,nonlocal 关键字用于使用局部变量(函数内))
- Python2中没有nonlocal关键字,替代方案就是使用一个单元素(如列表,字典,集合等等)来实现与nonlocal一致的功能。
- 除了简单的函数,在其他任何地方都应该尽力的避免使用nonlocal关键字。
第 16 条:考虑用生成器来改写直接返回列表的函数 35
- 相较于返回一个列表的情况,替代方案中使用生成器可以使得代码变得更加的清晰。
- 生成器返回的迭代器,是在其生成器内部一个把值传递给了
yield
变量的集合。 - 生成器可以处理很大的输出序列就是因为它在处理的时候不会完全的包含所有的数据。
#list
def index_words(text):
result = []
if text:
result.append(0)
for index, letter in enumerate(text):
if letter == ' ':
result.append(index + 1)
return result
address = 'Four score and seven years ago...'
result = index_words(address)
print(result[:3]) # [0, 5, 11]
#generator
def index_words_iter(text):
if text:
yield 0
for index, letter in enumerate(text):
if letter == ' ':
yield index + 1
result = list(index_words_iter(address))
第 17 条:在参数上面迭代时,要多加小心 37
- 多次遍历输入参数的时候应该多加小心。如果参数是迭代器的话你可能看到奇怪的现象或者缺少值现象的发生。
- Python的iterator协议定义了容器和迭代器在iter和next下对于循环和相关表达式的关系。
只要实现了__iter__
方法,你就可以很容易的定义一个可迭代的容器类。 - 通过连续调用两次iter方法,你就可以预先检测一个值是不是迭代器而不是容器。两次结果一致那就是迭代器,否则就是容器了。调用内置的next函数,可以令迭代器前进一步。
#generator不能重用的例子
def read_visits(data_path):
with open(data_path,'r') as f:
for line in f:
yield int(line)
it = read_visits('tmp/my_numbers.txt')
print(list(it))
print(list(it)) # 这里其实已经执行到头了
>>>
[15, 35, 80]
[]
# 如何解决?每次调用都创建iterator避免上面list分配内存
def normalize_func(get_iter): # get_iter 是函数
total = sum(get_iter()) # New iterator
result = []
for value in get_iter(): # New iterator
percent = 100 * value / total
result.append(percent)
return result
percentages = normalize_func(lambda: read_visits(path))
for循环会调用内置iter函数,进而调用对象的__iter__
方法,__iter__
会返回iterator对象(实现__next__
方法)。用iter函数检测iterator:
class ReadVists(object):
def __int__(self, data_path):
self.data_path = data_path
# 在自己的类中把__iter__实现为生成器,就可以实现一个可以迭代的容器类
def __iter__(self):
with open(self.data_path) as f:
for line in f:
yield int(line)
def normalize_defensive(numbers):
if iter(numbers) is iter(numbers): # 是个迭代器
raise TypeError('Must supply a container')
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)
return result
visits = [15, 35, 80]
normalize_defensive(visits) # no error
visits = ReadVIsitors(path)
normalize_defensive(visits) # no error
# 但是如果输入值不是一个容器类的话,就会引发异常了
it = iter(visits)
normalize_defensive(it)
>>>
TypeError: Must supply a container
第 18 条:用数量可变的位置参数减少视觉杂讯 41
- 通过使用
*args
定义语句,函数可以接收可变数量的位置参数。 - 你可以通过
*
操作符来将序列中的元素作为位置变量。 - 带有
*
操作符的生成器变量可能会引起程序的内存溢出,或者机器宕机。 - 为可以接受
*args
的函数添加新的位置参数可以产生难于发现的问题,应该谨慎使用。
def log(message, *values):
if not values:
print(message)
else:
values_str = ', '.join(str(x) for x in values)
print('%s: %s' % (message, values_str))
log('My numbers are', 1, 2)
log('Hi there')
第 19 条:用关键字参数来表达可选的行为 43
- 函数的参数值即可以通过位置被指定,也可以通过关键字来指定。
- 相较于使用位置参数赋值,使用关键字来赋值会让你的赋值语句逻辑变得更加的清晰。
- 带有默认参数的关键字参数函数可以很容易的添加新的行为,尤其适合向后兼容。
- 可选的关键字参数应该优于位置参数被考虑使用。
关键字参数的好处:
- 代码可读性的提高
- 以在定义的时候初始化一个默认值
- 在前面的调用方式不变的情况下可以很好的拓展函数的参数,不用修改太多的代码
# before
def flow_rate(weight_diff, time_diff, period=1):
return (weight_diff / time_diff) * period
# after
def flow_rate(weight_diff, time_diff, period=1, units_per_kg=1):
return ((weight_diff / units_per_kg) / time_diff) * period
flow_per_second = flow_rate(weight_diff, time_diff)
flow_per_hour = flow_rate(weight_diff, time_diff, period=3600)
pounds_per_hour = flow_rate(weight_diff, time_diff, period=3600, units_per_kg=2.2)
pounds_per_hour = flow_rate(weight_diff, time_diff, 3600, 2.2) # 不推荐
第 20 条:用 None 和文档字符串来描述具有动态默认值的参数 46
- 默认参数只会被赋值一次:在其所在模块被加载的过程中,这有可能导致一些奇怪的现象。
- 使用
None
作为关键字参数的默认值会有一个动态值。要在该函数的说明文档中详细的记录一下。
def log(message, when=datetime.now()):
print(‘%s: %s’ % (when, message))
log(‘Hi there!’)
sleep(0.1)
log(‘Hi again!’)
>>>
2019-1-14 22:10:10.371432: Hi there!
2019-1-14 22:10:10.371432: Hi again! # 时间并没有变化。
# 使用None作为默认值,文档里要有说明
def log(message, when=None):
"""Log a message with a timestamp.
Args:
message: Message to print
when: datetime of when the message occurred.
Default to the present time.
"""
when = datetime.now() if when is None else when
print("%s: %s" %(when, message))
# another example
def decode(data, default=None):
"""Load JSON data from string.
Args:
data: JSON data to be decoded.
default: Value to return if decoding fails.
Defaults to an empty dictionary.
"""
if default is None:
default = {}
try:
return json.loads(data)
except ValueError:
return default
第 21 条:用只能以关键字形式指定的参数来确保代码明晰 49
- 关键字参数使得函数调用的意图更加的清晰,明显。
- 使用keyword-only参数可以强迫函数调用者提供关键字来赋值,这样对于容易使人疑惑的函数参数很有效,尤其适用于接收多个布尔变量的情况。
- Python3中有明确的keyword-only函数语法。
- Python2中可以通过**kwargs模拟实现keyword-only函数语法,并且人工的触发TypeError异常。
- keyword-only在函数参数列表中的位置很重要,这点大家尤其应该明白!
def safe_division(number, divisor, ignore_overflow,
ignore_zero_division):
# 省略了实现.......
result = safe_division(1, 10**500, True, False)
result = safe_division(1, 0, False, True)
# 上述函数使用上不方便,因为容易忘记 ignore_overflow 和 ignore_zero_division 的顺序
# 用 keyword 引数可解決此问题,在 Python 3 可以宣告强制接收 keyword-only 参数。
def safe_division_c(number, divisor, *,
ignore_overflow=False,
ignore_zero_division=False):
#....
safe_division_c(1, 10**500, True, False)
>>>
TypeError: safe_division_c() takes 2 positional arguments but 4 were given
safe_division(1, 0, ignore_zero_division=True) # OK
Python 2 虽然没有这种语法,但可以用 **
操作符模拟
注:*
操作符接收可变数量的位置参数,**
接受任意数量的关键字参数
# Python 2
def safe_division(number, divisor, **kwargs):
ignore_overflow = kwargs.pop('ignore_overflow', False)
ignore_zero_division = kwargs.pop('ignore_zero_division', False)
if kwargs:
raise TypeError("Unexpected **kwargs: %r"%kwargs)
# ···
# test
safe_division(1, 10)
safe_division(1, 0, ignore_zero_division=True)
safe_division(1, 10**500, ignore_overflow=True)
# 而想通过位置参数赋值,就不会正常的运行了
safe_division(1, 0, False, True)
>>>
TypeError:safe_division() takes 2 positional arguments but 4 were given.
第 3 章 类与继承 53
第 22 条:尽量用辅助类来维护程序的状态,而不要用字典和元组 53
- 避免字典中嵌套字典,或者长度较大的元组。
- 在一个整类(类似于前面第一个复杂类那样)之前考虑使用
namedtuple
制作轻量,不易发生变化的容器。 - 当内部的字典关系变得复杂的时候将代码重构到多个工具类中。
dictionaries 以及 tuples 拿來存简单的资料很方便,但是当资料越来越复杂时,例如多层 dictionaries 或是 n-tuples,程式的可读性就下降了。你可以从依赖树的底端开始,将其划分成多个类。这就是代码的设计问题了。
第 23 条:简单的接口应该接受函数,而不是类的实例 58
- 在Python中,不需要定义或实现什么类,对于简单接口组件而言,函数就足够了。
- Python中引用函数和方法的原因就在于它们是first-class,可以直接的被运用在表达式中。
特殊方法__call__
允许你像调用函数一样调用一个对象实例。 - 当你需要一个函数来维护状态信息的时候,考虑一个定义了
__call__
方法的状态闭包类哦(详见第15项:了解闭包是怎样与变量作用域的联系)
Python中的许多内置的API都允许你通过向函数传递参数来自定义行为。这些被API使用的hooks将会在它们运行的时候回调给你的代码。例如:list类型的排序方法中有一个可选的key 参数来决定排序过程中每个下标的值。这里,我使用一个lambda表达式作为这个键钩子,根据名字中字符的长度来为这个集合排序。
names = ['Socrates', 'Archimedes', 'Plato', 'Aristotle']
names.sort(key=lambda x: len(x))
函数可以作为钩子来工作是因为Python
有first-class
函数:在编程的时候函数,方法可以像其他的变量值一样被引用,或者被传递给其他的函数。Python
允许类来定义__call__
这个特殊的方法。它允许一个对象像被函数一样来被调用。这样的一个实例也引起了callable
这个内True
的事实。
current = {'green': 12, 'blue': 3}
incremetns = [
('red', 5),
('blue', 17),
('orange', 9)
]
class BetterCountMissing(object):
def __init__(self):
self.added = 0
def __call__(self):
self.added += 1
return 0
counter = BetterCountMissing()
counter()
assert callable(counter)
# 这里我使用一个BetterCountMissing实例作为defaultdict函数的默认的hook值来追踪缺省值被添加的次数。
counter = BetterCountMissing()
result = defaultdict(counter, current)
for key, amount in increments:
result[key] += amount
assert counter.added == 2
第 24 条:以 @classmethod 形式的多态去通用地构建对象 62
-
Python
的每个类只支持单个的构造方法,__init__
- 使用
@classmethod
可以为你的类定义可替代构造方法的方法。 - 类的多态为具体子类的组合提供了一种更加通用的方式。
使用 @classmethod起到多态的效果:一个对于分层良好的类树中,不同类之间相同名称的方法却实现了不同的功能的体现。 下面的函数 generate_inputs() 不够一般化,只能使用 PathInputData ,如果想使用其它 InputData 的子类,必须改变函数。
class InputData(object):
def read(self):
raise NotImplementedError
class PathInputData(InputData):
def __init__(self, path):
super().__init__()
self.path = path
def read(self):
return open(self.path).read()
def generate_inputs(data_dir):
for name in os.listdir(data_dir):
yield PathInputData(os.path.join(data_dir, name))
问题在于建立 InputData
子类的物件不够一般化,如果你想要编写另一个 InputData
的子类就必须重写 read
方法幸好有 @classmethod
,可以达到一样的效果。
class GenericInputData(object):
def read(self):
raise NotImplementedError
@classmethod
def generate_inputs(cls, config):
raise NotImplementedError
class PathInputData(GenericInputData):
def __init__(self, path):
super().__init__()
self.path = path
def read(self):
return open(self.path).read()
@classmethod
def generate_inputs(cls, config):
data_dir = config['data_dir']
for name in os.listdir(data_dir):
yield cls(os.path.join(data_dir, name))
第 25 条:用 super 初始化父类 67
-
Python
的解决实例化次序问题的方法MRO
解决了菱形继承中超类多次被初始化的问题。 - 总是应该使用
super
来初始化父类。
应该避免棱形继承。如有会发生,说明设计的不够好。
第 26 条:只在使用 Mix-in 组件制作工具类时进行多重继承 71
- 如果可以使用
mix-in
实现相同的结果输出的话,就不要使用多继承了。 - 当
mix-in
类需要的时候,在实例级别上使用可插拔的行为可以为每一个自定义的类工作的更好。 - 从简单的行为出发,创建功能更为灵活的
mix-in
。
如果你发现自己渴望随继承的便利和封装,那么考虑mix-in吧。它是一个只定义了几个类必备功能方法的很小的类。Mix-in类不定义以自己的实例属性,也不需要它们的初始化方法init被调用。Mix-in可以被分层和组织成最小化的代码块,方便代码的重用。
mix-in 是可以替换的 class ,通常只定义 methods ,虽然本质上上还是通过继承的方式,但因为 mix-in 沒有自己的 state ,也就是说沒有定义 attributes ,使用上更有弹性。
import json
class ToDictMixin(object):
def to_dict(self):
return self._traverse_dict(self.__dict__)
def _traverse_dict(self, instance_dict):
output = {}
for key, value in instance_dict.items():
output[key] = self._traverse(key, value)
return output
# hasattr 函数动态访问属性,isinstance 函数动态检测对象类型
def _traverse(self, key, value):
if isinstance(value, ToDictMixin):
return value.to_dict()
elif isinstance(value, dict):
return self._traverse_dict(value)
elif isinstance(value, list):
return [self._traverse(key, i) for i in value]
elif hasattr(value, '__dict__'):
return self._traverse_dict(value.__dict__)
else:
return value
class BinaryTree(ToDIctMixin):
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
# 这下把大量的Python对象转换到一个字典中变得容易多了。
tree = BinaryTree(10, left=BinaryTree(7, right=BinaryTree(9)),
right=BinaryTree(13, left=BinaryTree(11)))
print(tree.to_dict())
>>>
{'left': {'left': None,
'right': {'left': None, 'right': None, 'value': 9},
'value': 7},
'right': {'left': {'left': None, 'right': None, 'value': 11},
'right': None,
'value': 13},
'value': 10
}
第 27 条:多用 public 属性,少用 private 属性 75
- Python 编译器无法严格保证 private 字段的私密性
- 不要盲目将属性设置为 private,而是应该从一开始就做好规划,并允子类更多地访问超类的内部的API
- 应该多用 protected 属性,并且在文档中把这些字段的合理用法告诉子类的开发者,而不要试图用 private 属性来限制子类的访问
- 只有当子类不受自己控制的时候,才可以考虑使用 private 属性来避免名称冲突
Python 里面沒有真正的 “private variable”,想存取都可以存取得到。一般来说 Python 惯例是在变数前加一个底线代表 protected variable ,作用在于提醒开发者使用上要注意。双底线的命名方式是为了避免父类和子类间的命名冲突,除此之外尽量避免使用这种命名。
第 28 条:继承 collections.abc 以实现自定义的容器类型 79
- 如果要定制的子类比较简单,那就可以直接从Python的容器类型(如list或dict)中继承
- 想正确实现自定义的容器类型,可能需要编写大量的特殊方法
- 编写自制的容器类型时,可以从collection.abc 模块的抽象类基类中继承,那些基类能确保我们的子类具备适当的接口及行为
ollections.abc
里面的 abstract classes 的作用是让开发者方便地开发自己的 container ,例如 list。一般情況下继承list 就ok了,但是当结构比较复杂的时候就需要自己自定义,例如 list 有许多 方法,要一一实现有点麻烦。
但是使用者可能想使用像 count()以及 index()等 list 的 方法 ,这时候可以使用 collections.abc的 Sequence 。子类只要实现 __getitem__
以及 __len__
, Sequence 以及提供count()以及 index()了,而且如果子类没有实现类似 Sequence 的抽象基类所要求的每个方法,collections.abc 模块就会指出这个错误。
第 4 章 元类及属性 84
第 29 条:用纯属性取代 get 和 set 方法 84
- 使用public属性避免set和get方法,@property定义一些特别的行为
- 如果访问对象的某个属性的时候,需要表现出特殊的行为,那就用@property来定义这种行为
- @property 方法应该遵循最小惊讶原则,而不应该产生奇怪的副作用
- 确保@property方法是快速的,如果是慢或者复杂的工作应该放在正常的方法里面
# 不要把 java 的那一套 getter 和 setter 带进来
# not like this:
lass OldResistor(object):
def __init__(self, ohms):
self._ohms = ohms
def get_ohms(self):
return self._ohms
def set_ohms(self, ohms):
self._ohms = ohms
# just like this:
class Resistor(object):
def __init__(self, ohms):
self.ohms = ohms
self.voltage = 0
self.current = 0
使用@property,来绑定一些特殊操作,但是不要产生奇怪的副作用,比如在getter里面做一些赋值的操作
class VoltageResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
self._voltage = 0
# 相当于 getter
@property
def voltage(self):
return self._voltage
# 相当于 setter
@voltage.setter
def voltage(self, voltage):
self._voltage = voltage
self.current = self._voltage / self.ohms
r2 = VoltageResistance(1e3)
print('Before: %5r amps' % r2.current)
# 会执行 setter 方法
r2.voltage = 10
print('After: %5r amps' % r2.current)
第 30 条:考虑用 @property 来代替属性重构 88
- 使用@property给已有属性扩展新需求
- 可以用 @property 来逐步完善数据模型
- 当@property太复杂了才考虑重构
@property可以把简单的数值属性迁移为实时计算,只定义 getter 不定义 setter 那么就是一个只读属性
class Bucket(object):
def __init__(self, period):
self.period_delta = timedelta(seconds=period)
self.reset_time = datetime.now()
self.max_quota = 0
self.quota_consumed = 0
def __repr__(self):
return ('Bucket(max_quota=%d, quota_consumed=%d)' %
(self.max_quota, self.quota_consumed))
@property
def quota(self):
return self.max_quota - self.quota_consumed
@quota.setter
def quota(self, amount):
delta = self.max_quota - amount
if amount == 0:
# Quota being reset for a new period
self.quota_consumed = 0
self.max_quota = 0
elif delta < 0:
# Quota being filled for the new period
assert self.quota_consumed = 0
self.max_quota = amount
else:
# Quota being consumed during the period
assert self.max_quota >= self,quota_consumed
self.quota_consumed += delta
这种写法的好处就在于:从前使用的Bucket.quota 的那些旧代码,既不需要做出修改,也不需要担心现在的Bucket类是如何实现的,可以轻松无痛扩展新功能。但是@property也不能滥用,而且@property的一个缺点就是无法被复用,同一套逻辑不能在不同的属性之间重复使用如果不停的编写@property方法,那就意味着当前这个类的代码写的确实很糟糕,此时应该重构了。
第 31 条:用描述符来改写需要复用的 @property 方法 92
- 如果想复用 @property 方法及其验证机制,那么可以自定义描述符类
- WeakKeyDictionary 可以保证描述符类不会泄露内存
- 通过描述符协议来实现属性的获取和设置操作时,不要纠结于
__getatttttribute__
的方法的具体运作细节
property
最大的问题是可能造成 duplicated code 这种 code smell. 可以使用 descriptor 解決,下面的程式将重复的逻辑封装在 Grade 里面。但是这个程式根本不能用 ,因为存取到的是 class attributes,例如 exam.writing_grade = 40
其实是Exam.__dict__['writing_grade'].__set__(exam, 40)
,这样所有 Exam 的 instances 都是存取到一样的东西 ( Grade()
)。
class Grade(object):
def __init__(self):
self._value = 0
def __get__(self, instance, instance_type):
return self._value
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError('Grade must be between 0 and 100')
self._value = value
class Exam(object):
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()
exam = Exam()
exam.writing_grade = 40
解決方式是用个 dictionary 存起來,这里使用 WeakKeyDictionary
避免 memory leak。
from weakref import WeakKeyDictionary
class Grade(object):
def __init__(self):
self._values = WeakKeyDictionary()
def __get__(self, instance, instance_type):
if instance is None: return self
return self._values.get(instance, 0)
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError('Grade must be between 0 and 100')
self._values[instance] = value
第 32 条:用 __getattr__
、 __getattribute__
和__setattr__
实现按需生成的属性 97
- 通过
__getttattr__
和__setattr__
,我们可以用惰性的方式来加载并保存对象的属性 - 要理解
__getattr__
和__getattribute__
的区别:前者只会在待访问的属性缺失时触发,而后者则会在每次访问属性的时候触发 - 如果要在
__getattributte__
和__setattr__
方法中访问实例属性,那么应该直接通过 super() 来做,以避免无限递归 - obj.name,getattr和hasattr都会调用getattribute方法,如果name不在obj.dict里面,还会调用getattr方法,如果没有自定义getattr方法会AttributeError异常
- 只要有赋值操作(=,setattr)都会调用setattr方法(包括a = A())
__getattr__
和 __getattribute__
都可以动态地存取 attributes ,不同点在于如果 __dict__
找不到才会呼叫 __getattr__
,而 __getattribute__
每次都会被呼叫到。
第 33 条:用元类来验证子类 102
- 通过元类,我们可以在生成子类对象之前,先验证子类的定义是否合乎规范
- Python2 和 Python3 指定元类的语法略有不同
- 使用元类对类型对象进行验证
- Python 系统把子类的整个 class 语句体处理完毕之后,就会调用其元类的
__new__
方法
第 34 条:用元类来注册子类 104
- 在构建模块化的 Python 程序时候,类的注册是一种很有用的模式
- 开发者每次从基类中继承子类的时,基类的元类都可以自动运行注册代码
- 通过元类来实现类的注册,可以确保所有子类都不会泄露,从而避免后续的错误
第 35 条:用元类来注解类的属性 108
- 借助元类,我们可以在某个类完全定义好之前,率先修改该类的属性
- 描述符与元类能够有效的组合起来,以便对某种行为做出修饰,或者在程序运行时探查相关信息
- 如果把元类与描述符相结合,那就可以在不使用 weakerf 模块的前提下避免内存泄露
第 5 章 并发及并行 112
第 36 条:用 subprocess 模块来管理子进程 113
- 使用 subprocess 模块运行子进程管理自己的输入和输出流
- subprocess 可以并行执行最大化CPU的使用
- communicate 的 timeout 参数避免死锁和被挂起的子进程
import subprocess
import os
proc = subprocess.Popen(
['echo', 'Hello from the child!'],
stdout=subprocess.PIPE)
out, err = proc.communicate()
print(out.decode('utf-8'))
def run_openssl(data):
env = os.environ.copy()
env['password'] = b'\xe24U\n\xd0Ql3S\x11'
proc = subprocess.Popen(
['openssl', 'enc', '-des3', '-pass', 'env:password'],
env=env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
proc.stdin.write(data)
proc.stdin.flush() # Ensure the child gets input
return proc
def run_md5(input_stdin):
proc = subprocess.Popen(
['md5'],
stdin=input_stdin,
stdout=subprocess.PIPE)
return proc
第 37 条:可以用线程来执行阻塞式 I/O,但不要用它做平行计算 117
- 因为GIL,Python thread并不能并行运行多段代码
- Python保留thread的两个原因:1.可以模拟多线程,2.多线程可以处理I/O阻塞的情况
- Python thread可以并行执行多个系统调用,这使得程序能够在执行阻塞式I/O操作的同时,执行一些并行计算
第 38 条:在线程中使用 Lock 来防止数据竞争 121
- 虽然Python thread不能同时执行,但是Python解释器还是会打断操作数据的两个字节码指令,所以还是需要锁
- thread模块的Lock类是Python的互斥锁实现
from threading import Thread
from threading import Lock
class LockingCounter(object):
def __init__(self):
self.lock = Lock()
self.count = 0
def increment(self, offset):
with self.lock:
self.count += offset
def worker(sensor_index, how_many, counter):
for _ in range(how_many):
# Read from the sensor
counter.increment(1)
def run_threads(func, how_many, counter):
threads = []
for i in range(5):
args = (i, how_many, counter)
thread = Thread(target=func, args=args)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
how_many = 10**5
counter = LockingCounter()
run_threads(worker, how_many, counter)
print('Counter should be %d, found %d' %
(5 * how_many, counter.count))
第 39 条:用 Queue 来协调各线程之间的工作 124
- 管线是一种优秀的任务处理方式,它可以把处理流程划分为若干阶段,并使用多条Python线程同时执行这些任务
- 构建并发式的管线时,要注意许多问题,包括:如何防止某个阶段陷入持续等待的状态之中、如何停止工作线程,以及如何防止内存膨胀等
- Queue类具备构建健壮并发管道的特性:阻塞操作,缓存大小和连接(join)
from queue import Queue
from threading import Thread
class ClosableQueue(Queue):
SENTINEL = object()
def close(self):
self.put(self.SENTINEL)
def __iter__(self):
while True:
item = self.get()
try:
if item is self.SENTINEL:
return # Cause the thread to exit
yield item
finally:
self.task_done()
class StoppableWorker(Thread):
def __init__(self, func, in_queue, out_queue):
super().__init__()
self.func = func
self.in_queue = in_queue
self.out_queue = out_queue
def run(self):
for item in self.in_queue:
result = self.func(item)
self.out_queue.put(result)
def download(item):
return item
def resize(item):
return item
def upload(item):
return item
download_queue = ClosableQueue()
resize_queue = ClosableQueue()
upload_queue = ClosableQueue()
done_queue = ClosableQueue()
threads = [
StoppableWorker(download, download_queue, resize_queue),
StoppableWorker(resize, resize_queue, upload_queue),
StoppableWorker(upload, upload_queue, done_queue),
]
for thread in threads:
thread.start()
for _ in range(1000):
download_queue.put(object())
download_queue.close()
download_queue.join()
resize_queue.close()
resize_queue.join()
upload_queue.close()
upload_queue.join()
print(done_queue.qsize(), 'items finished')
第 40 条:考虑用协程来并发地运行多个函数 131
- 线程有三个大问题:
- 需要特定工具去确定安全性
- 单个线程需要8M的内存
- 线程启动消耗
- coroutine只有1kb的内存消耗
- generator可以通过send方法把值传递给yield
def my_coroutine():
while True:
received = yield
print("Received:", received)
it = my_coroutine()
next(it)
it.send("First")
('Received:', 'First')
第 41 条:考虑用 concurrent.futures 来实现真正的平行计算 141
- CPU瓶颈模块使用C扩展
- concurrent.futures的multiprocessing可以并行处理一些任务,Python2没有这个模块
- multiprocessing 模块所提供的那些高级功能,都特别复杂,开发者尽量不要直接使用它们
使用 concurrent.futures
里面的 ProcessPoolExecutor 可以很简单地平行处理 CPU-bound 的程式,省得用 multiprocessing
自定义。
from concurrent.futures import ProcessPoolExecutor
start = time()
pool = ProcessPoolExecutor(max_workers=2) # The one change
results = list(pool.map(gcd, numbers))
end = time()
print('Took %.3f seconds' % (end - start))
第 6 章 内置模块 145
第 42 条:用 functools.wraps 定义函数修饰器 145
第 43 条:考虑以 contextlib 和 with 语句来改写可复用的 try/f inally 代码 148
第 44 条:用 copyreg 实现可靠的 pickle 操作 151
第 45 条:应该用 datetime 模块来处理本地时间,而不是用 time 模块 157
第 46 条:使用内置算法与数据结构 161
第 47 条:在重视精确度的场合,应该使用 decimal 166
第 48 条:学会安装由 Python 开发者社区所构建的模块 168
第 7 章 协作开发 170
第 49 条:为每个函数、类和模块编写文档字符串 170
第 50 条:用包来安排模块,并提供稳固的 API 174
第 51 条:为自编的模块定义根异常,以便将调用者与 API 相隔离 179
第 52 条:用适当的方式打破循环依赖关系 182
第 53 条:用虚拟环境隔离项目,并重建其依赖关系 187
第 8 章 部署 193
第 54 条:考虑用模块级别的代码来配置不同的部署环境 193
第 55 条:通过 repr 字符串来输出调试信息 195
第 56 条:用 unittest 来测试全部代码 198
第 57 条:考虑用 pdb 实现交互调试 201
第 58 条:先分析性能,然后再优化 203
第 59 条:用 tracemalloc 来掌握内存的使用及泄漏情况 208