《Effective Python》的读书笔记~
Pythonic Thinking
- Python开发者用Pythonic这个形容词来形容那种符合特定风格的代码。
- 用import this 可以以查看python的编程之禅
PEP8风格指南
针对Python代码格式编订的风格指南
空白Whitespace
- 使用空格而不是制表符来缩进
- 和语法相关的为4个空格
- 每行字符不超过79
- 对于多行的长表达式,除首行外其余各行都应该在通常缩进级别之上再加4个空格
- 文件中的函数与类之间应该用两个空行隔开
- 在同一个类中,各方法之间用一个空行隔开
- 对于下表取值、函数调用、给关键字参数赋值不要在两旁写空格
- 变量赋值时赋值符号各自写一个空格
命名
- 函数、变量和属性用小写+下划线的形式
- 受保护的实例属性用单下划线开头_leading_underscore
- 私有的实例属性用两个下划线开头__double_leading_underscore
- 类和异常每个单词首字母均大写 CapitalizedWord
- 模块级别的常量全部大写+下划线:ALL_CAPS
- 类中的实例方法首个参数命名为self,表示对象本身
- 类方法class method首个参数应该用cls,表示该类自身
表达式和语句
- 使用(if a is not b)而不是if not a is b
- 不要通过长度检测 if len(somelist) == 0 来判断是否为空,直接用if not somelist这种方式来判断,空值默认判断为False
- 检测somelist是否为[1]或'hi'等非空值,也应该直接用if somelist,会把非空为True
- 避免单行的if 、for、while循环以及except复合语句,应该拆分为多行使得更加清晰
- import 语句总是放在文件的开头
- 引入模块应该才用绝对的路径而不要用相对的路径,如引入bar中的foo,import bar import foo,而不是import foo
- 如果一定要相对名称,就用明确的写法:from . import foo
- import语句应该按顺序分为三部分:标准库模块、第三方模块、自用模块。每一部分中,各import语句按模块的字母顺序排列
其它
在同一个切片操作内,不要同时使用start、end、stride,如果确实需要执行这样的语句,那就考虑将其拆解为两条赋值语句,其中一条作范围切割,另一条做步进切割。如果对时间或者内存要求严格,可以考虑内置itertools中的islice
a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
- a[::2] # ['a', 'c', 'e', 'g']
- a[::-2] # ['h', 'f', 'd', 'b']
下面的会让人困惑
- a[2::2] # ['c', 'e', 'g']
- a[-2::-2] # ['g', 'e', 'c', 'a']
- a[-2:2:-2] # ['g', 'e'],从-2开始取到下标为2,且步长为2
- a[2:2:-2] # []
不要使用含有两个以上的列表推导,因为难以理解
使用生成器表达式来改写数据量较大的列表推导
把实现列表推导的[]改为()就变成了生成器表达式
数据量大用列表推导占用太多内存(内存不一定够)
串在一起的生成器表达式执行速度很快
- it = (len(x) for x in open('/tmp/my_file.txt'))
- roots = ((x, x**0.5) for x in it)
尽量用enurmate取代range
用zip来平行的遍历多个迭代器
- python3中zip相当于生成器。
- 如果迭代器长度不等,那么zip会提前自动终止。
- 使用zip_longest可以平行遍历多个迭代器
不要在for 和 while后写else块
- 容易让人误解
- 只有当整个循环都没有break的时候,循环后面的else才会执行
合理利用try/except/else/finally中的每个代码块
- 无论try块是否发生异常,都可以用finally来执行清理工作
- else 可以用来缩减try块中的代码量,并把没有发生异常时所要执行的语句和try/except代码块隔开
函数
在闭包中使用外围作用域的变量
- 若是当前作用域没有这个变量,python会把这次赋值视为对变量的定义
- 使用nonlocal来修改外围作用域中同名变量,但不能延伸到模块级别,防止污染全局作用域
- nonlocal与global互为补充,会直接修改模块作用域里的那个变量
生成器
- 生成器是使用yield的函数,调用生成器函数时,并不会真的执行,而是会返回生成器。每个在这个迭代器上面调用next函数时,迭代器把生成器推进到下一个yield表达式那里
当函数参数若是迭代器…
需要注意的时,迭代器只能返回一轮结果,在跑出过StopIteration异常的迭代器或生成器上继续迭代第二轮是不会有结果的。因此,如果将迭代器作为参数并在函数中想要遍历两次,那么代码不能按我们期望的方式进行。
解决的办法是新编一种实现迭代器协议的容器类。
python在for循环及相关表达式中遍历某种容器的内容时,就要依靠这个迭代器协议。在执行类似for x in foo这样的语句时,python实际上会调用iter(foo),iter又会调用foo.__iter__这个方法。这个方法返回迭代器对象,而那个迭代器本身,实现了
__next__
特殊方法,此后for循环在迭代器上反复调用next函数,直到耗尽并产生stopIteration异常。在使用类时,只需要令自己的类把__iter__方法实现为生成器就可以实现上面的要求
1
2
3
4
5
6
7class ReadVisits(object):
def __init__(self, data_path):
self.data_path = data_path
def __iter__(self):
with open(self.data_path) as f:
for line in f:
yield int(line)想判断某个值是容器还是迭代器,可以拿该值为参数,两次调用iter函数。若结果,相同,则为迭代器
if iter(numbers) is iter(numbers)
变长参数
- 变长参数在传给函数时,总是要先转化为元祖,因此,若对生成器使用*操作符,就会将生成器完整迭代一轮,并把结果每个值都放入元祖中,这可能会消耗大量内存,导致程序崩溃。
- 使用
*arg
参数的第二个问题是将来要给函数添加新的位置参数,就必须修改原来调用函数的代码。为了避免此情况,我们应该使用只以关键字形式指定的参数来扩展接受*args
的函数
关键字参数
位置参数必须出现在关键字参数之前
- 如这样是错的:
remainder(number=20, 7)
- 如这样是错的:
动态默认值的参数应该把形式上的默认值写成None,然后在函数中初始化。因为参数的默认值会在每个模块加载进来的时候求出,而很多模块都在程序启动时加载。模块一旦加载进来,参数默认固定值就不变。
如下代码的when不会变
1 | def log(message, when=datetime.now()): |
- **参数列表里的*号,标志着位置参数就此终结,之后的参数只能以关键字形式来指定**(only py3)
1 | def safe_division_c(number, divisor, *, |
类和继承
namedtuple
- 如果容器中包含简单又不可变的数据,那么先用namedtuple来表示,稍后有需要的时候再修改为完整的类。
- 不过无法指定默认参数
- 使用例子:
1 | Point = collections.namedtuple('Point', ['x', 'y']) |
简单的接口应该接受函数而非类的实例
对于连接各种Python的简单接口,通常应该给其直接传入函数,而不是先定义某个类,然后再传入这个类的实例。
python中的函数和方法都可以像一级类那样引用。
python中的函数之所以能充当挂钩,原因在于函数是一级对象,可以像语言中其它值那样传递和引用。
- nums.sort(key=lambda x:len(x))
通过名为
__call__
的方法,可以使类的实例能像普通的python函数那样得到调用。如果要用函数来保存状态,那么就定义新的类,并实现
__call__
方法,而不是定义带状态的闭包。
@classmethod来多态构建对象
python只允许__init__的构造器方法,可以使用@classmethod的多态。
1 | def create_workers(input_list): |
上面的例子中, LineCountWorker为Worker的子类。
假如此时需要处理别的work的话,那就得重写这个函数。
因此可以这样:
1 | class GenericWorker(object): |
然后让具体的子类继承GenericWorker即可。
用super初始化父类
钻石形继承体系:
1 | class MyBaseClass(object): |
用super的话,钻石顶部的MyBaseClass类中的__init__方法只会运行一次。而其它超类初始化顺序,则与这些超类在class语句中出现的顺序相同。
1 | class MyBaseClass(object): |
可以用mro类方法查询程序的运行顺序,调用GoodWay(5),首先调用TimesFive.__init__,然后TimesFive.__init__调用PlusTwo.__init__,然后PlusTwo.__init__调用MyBaseClass.__init__,到达顶部后,先设为5,然后PlusTwo.__init__加2,然后TimesFive.__init__乘以5,得到了35。
只在使用Mix-in组件制作工具类时进行多重继承
Mix-in可以认为是工具类,继承的子类便具有了这些功能,子类可以复写方法来改进。能用Mix-in组件实现的效果,就不要用多重继承来做。
1 | class ToDictMixin(object): |
多用public属性,少用private属性
子类无法访问private属性,原因在于变换后的属性名和待访问的属性名称不相符:
1 | class MyParentObject(object): |
调用MyChildObject.get_private_field,它将translates __private_field
变换为_MyChildObject__private_field
,然后进行访问。而__private_field只在MyParentObject.__init__
做了定义,因此这个私有属性的名称是_MyParentObject__private_field
。
因此,上面的代码可以直接用print(baz._MyParentObject__private_field)#71
访问私有属性。
Python编译器无法严格保证private字段的私密性。宁可叫子类更多地取访问超类的protected属性,也不要设置为private。应当在文档中说明每个protected字段的含义,解释哪些字段是可供子类使用的内部API,哪些字段是完全不应该触碰的数据。
只有当子类不受自己控制的时候,才可以考虑用private属性来避免名称冲突。
继承collecions.abc实现自定义容器类型
- 如果要定制的子类比较简单,可以直接从Python的容器类型继承(如List或dict)
- 编写自制的容器类型,可以从collections.abc模块的抽象基类中继承,那些基类能确保我们子类具备适当的接口及行为。如继承Sequence的话,要求实现
__getitem__
以及__len__
方法
元类及属性
元类这个词只是模糊的描述了一种高于类又超乎类的概念。就是我们可以把python的class语句转译为元类,并令其在每次定义具体的类时,都提供独特的行为。
python还可以动态的定义对属性的访问操作。
用@property取代get和set方法
- 如果访问对象的某个属性,需要表现出特殊的行为(如修改电压同时修改电流),可以用@property来修饰方法
- setter和getter的名称必须要相关属性相符
- 可以在setter的时候设置相关的属性,或者进行数值验证
- @property方法需要执行得迅速一点,缓慢或复杂的工作应该放在普通方法里
1 | class Resistor(object): |
用@property代替属性重构
@property可以把一个原有的属性变为新的。
如下面的例子中,原来的属性有quota,现在改成了max_quota和quota_consumed:
1 | class Bucket(object): |
如果@property用得太过频繁,那么就应该考虑彻底重构该类并修改相关的调用代码。
用 getattr、 getattribute、和__setattr__ 实现按需生成的属性
如果某个类定义了__getattr__
,系统在该类对象实例的字典中又找不到待查询的属性,那么系统就会调用这个方法。适合实现按需访问,初次执行__getattr__
把相关属性加载,以后在访问该属性时,只需从现有的结果中获取即可。
1 | class LazyDB(object): |
-
__getattribute__
方法:每次访问对象属性时,都会调用这个方法(即使属性字典有也会) __setattr__
:赋值操作时均会触发(无论是内置的setattr函数还是直接赋值)- 如果要在
__getattribute__
和__setattr__
中访问实例属性,那么应该直接通过super()来避免无限递归:
1 | class BrokenDictionaryDB(object): |
这样才是对的
1 | class BrokenDictionaryDB(object): |
用元类验证子类
- 定义元类的时候,要从type中继承。
- 对于使用该元类的其他类,python会把那些类的class语句体中所含的相关内容,发送给元类的
__new__
方法。于是我们可以在系统构建出那个类之前,先修改类的信息。
1 | class Meta(type): |
通过元类,我们可以在生成子类对象之前,先验证子类的定义是否合乎规范。
python把子类的整个class语句处理完后,就调用其元类的__new__
方法。
如下面的例子中,多边形至少三条边,而Line设置一条边就不行:
1 | class ValidatePolygon(type): |
用元类注解类的属性
用下面的代码将数据库的行和列建立对应关系:
1 | class Field(object): |
但是Field还需要指定字段名称如first_name = Field('first_name'),比较繁琐,可以用元类来做
1 | class Field(object): |
并发及并行
用subprocess来管理子进程
1 | roc = subprocess.Popen(['ping', 'baidu.com'], stdout=subprocess.PIPE) |
可以一边定期查询子进程状态,一遍处理其它事务
1 | roc = subprocess.Popen(['ping', 'baidu.com'], stdout=subprocess.PIPE) |
还可以给 communicate传入timeout参数,避免子进程死锁或挂起
python线程并非并行
标准的python实现叫做CPython,CPython分两步来运行Python程序。首先把文本形式的源代码解析并编译成字节码。然后,用一种基于栈的解释器来运行这份字节码。执行python程序时,字节码解释器必须保持协调一致的状态。Python才用GIL(global interpreter lock,全局解释器锁)来确保这种协调性。
GIL为一把互斥锁,用以防止Cpython收到抢占式多线程切换的干扰。由于收到GIL保护,同一时刻只有一条线程向前执行。(虽然同一时刻只有一条线程,但仍需锁等机制来防止数据竞争)
可以用concurrent.futures中的ProcessPoolExecutor来执行真正的并行。
- 子进程的GIL是独立的
- 原理:将数据通过pickle来执行序列化,变为二进制形式,然后用local socket发给子解释器,子进程用pickle反序列化操作,然后执行。将结果进行序列化操作,然后用socket发送回主进程。主进程反序列化得到结果。主进程和子进程之间,必须进行序列化和反序列化操作,开销较大。
- 官方注释如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18|======================= In-process =====================|== Out-of-process ==|
+----------+ +----------+ +--------+ +-----------+ +---------+
| | => | Work Ids | => | | => | Call Q | => | |
| | +----------+ | | +-----------+ | |
| | | ... | | | | ... | | |
| | | 6 | | | | 5, call() | | |
| | | 7 | | | | ... | | |
| Process | | ... | | Local | +-----------+ | Process |
| Pool | +----------+ | Worker | | #1..n |
| Executor | | Thread | | |
| | +----------- + | | +-----------+ | |
| | <=> | Work Items | <=> | | <= | Result Q | <= | |
| | +------------+ | | +-----------+ | |
| | | 6: call() | | | | ... | | |
| | | future | | | | 4, result | | |
| | | ... | | | | 3, except | | |
+----------+ +------------+ +--------+ +-----------+ +---------+Queue(queue中) 为线程安全
- 具备阻塞式队列操作
- 指定缓冲区尺寸
- join等
协程
线程有三个显著的缺点:
- 需要特殊的工具来保证数据的安全。于是多线程的代码更加难懂,不便于扩展维护。
- 线程需占用大量内存,每个线程大约需要8MB。如果程序中运行成千上万个函数并且想要用线程来模拟出同时运行的效果,那就会出现问题。
- 线程启动开销比较大。
Python中的协程可以解决上述的问题。协程,又称微线程,纤程。英文名Coroutine。协程可以理解为用户级线程,协程和线程的区别是:线程是抢占式的调度,而协程是协同式的调度,协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。
Python协程的工作原理是:通过send给生成器传值,生成器将yield作为表达式的执行结果。执行完当前的yield,生成器推进到下一个yield表达式那里,并将那个yield关键字右侧的内容,当成send方法的返回值,返回给外界。
生成器通过这个输出值,来推进其他的生成器函数,使得那些生成器函数也执行到它们各自的下一条yield表达式处。接连推进多个独立的生成器,可以模拟出python线程的并发行为,令程序真正看上去好像是在同时执行多个函数。(用yield from 推进其他的生成器)
下面的代码中统计当前的最小值统计当前的最小值:
1 | def minimize(): |
内置模块
装饰器
contextlib和with语句改写try/finally语句
- with open('./data') as f: 比使用try 打开文件然后finally中关闭好得多
- 一个简单的函数,只需要经过contextlib中的contextmanager修饰,就可以用在with语句中。
- 由于py的默认信息级别是WARNING,因此不会打印debug的(只会打印不小于当前级别的)。下面用一个经过contextmanager修饰的函数来临时提升信息的级别,执行完毕后,再恢复原有级别。yield表达式所在的地方,就是with语句中要执行的地方。
1 | def my_function(): |
如果yield返回一个值,那么此值会赋值给由as关键字所指定的变量。
用copyreg实现可靠的pickle操作
pickle处理之后的数据,不一种不安全的格式。如果混入了恶意信息,那么python程序对其进行反序列化操作的时候,这些恶意信息可能对程序照成伤害。
json模块产生的则是一种安全的格式。
可以把内置的copyreg和pickle结合起来使用,以便为旧数据添加缺失的属性值、进行类的版本管理,并给序列化后的数据提供固定的引入路径。
用datetime模块来处理本地时间,而非time模块
time模块需要依赖操作系统而运作。不要用time模块在不同的时区之间进行转换。
如果要在不同时区之间,进行可靠的转换,应该把内置的datetime模块与开发者社区提供的pytz模块搭配起来使用。
先把时间转换成UTC格式,然后执行各种转换操作,最后再转换回本地时间。
使用内置的算法和数据结构
deque 双向队列
- append
- popleft
- pop
OrderedDict 有序字典
heapq 堆
bisect_left 二分查找
和迭代器有关的:
把迭代器连接起来:
- chain: 将多个迭代器按顺序连成一个迭代器
- cycle无限重复某个迭代器中各个元素
- tee 把一个迭代器拆分成多个平行的迭代器
- zip_longest:和zip类似,但可以应对长度不同的迭代器
能够从迭代器中过滤元素的函数
- islice:不进行赋值的前提下,根据索引值来切割迭代器
- takewhile:再判定函数为True的时候,从迭代器中逐个返回元素
- dropwhile:从判定函数为False的地方开始,逐个返回元素
- filterfalse:和filter相反,从迭代器中返回令判定函数为False的所有元素
把迭代器元素组合起来的函数
- product:根据迭代器中的元素计算笛卡儿积,并将其返回。可以用product来改写深度嵌套的列表推导操作
- permutation:排列
- combination:组合
在重视精度的场合用decimal
如下面的代码,按照每分钟rate收费,但是第一个输出我们期望的是0.01,第二个是5.37:
1 | def do_cost(rate, second): |
Decimal类中非常适合用在那种对精度要求很高,且对舍入行为要求很严的场合,如涉及货币计算:
1 | def do_cost(rate, second): |
协作开发
编写文档
- Python将文档视为第一等级(first-class)对象
- 通过
__doc__
来访问文档 - 文档应该用三重双引号"""
为模块编写文档
- 每个模块都应有顶级的doctring,用来介绍当前这个模块以及模块中的内容
- 文档第一行为一句话,介绍本模块的用途
- 它下面的那段话,应该包含一些细节信息,把与本模块的操作有关内容,告诉模块使用者。可以强调本模块中比较重要的类和函数,使得开发者能据此了解该模块的用法。
1 | #!/usr/bin/env python3 |
为类编写文档
- 每个类都应该有类级别的doctring。
- 头一行也是一句话介绍该类用途
- 类中比较重要的public属性及方法,也应该再这个docstring里面加以强调
1 | class Player(object): |
为函数编写文档
- 每个函数和方法也应该有docstring
- 第一行为一句话描述本函数的功能
- 接下来为一段话用来描述具体的行为和参数。(如果函数没有参数,且有且仅有一个简单的返回绘制,那么只需要一句话来描述该函数就够了)
- 若有返回值,则应该再docstring中写明。如果没有返回值就不要写。
- 如果可能抛出某些调用者必须处理的异常,而这些异常又是函数接口的一部分,那么docstring应该对其做出解释。同样的,没有异常就不要写
- 如果函数接受数量可变的位置参数或数量可变的关键字参数,那么就应该再文档的参数列表中,使用
*args
和**kwargs
来描述它们的用途 - 如果函数的参数有默认值,那么应该指出这些默认值
- 如果函数是个生成器,那么应该描述该生成器在迭代时产生的内容
- 如果函数时个协程,那么应该描述协程所产生的返回值,以及这个协程希望通过yield表达式来接纳的值,同时还要说明该协程何时会停止迭代
1 | def find_anagrams(word, dictionary): |
用包来安排模块
可以编写__all_
_的特殊属性,减少其暴露给外围API使用者的信息。 __all__
时一个列表,其中每个名称都将作为本模块的一条公共API,导出给外部代码。 如果外部用户以from foo import *
形式使用foo模块,那么只有在__all__
里的那些属性才会从foo引入。如果foo没有提供__all__
,那么只会引入public属性
1 | # __init__.py |
自定义异常
好处:
- 调用者在使用API的时候,通过捕获根异常,可以知道他们使用的调用代码是否正确
- 调用者可以捕获python的Exception基类,帮助模块的研发者寻找API实现中的bug
用适当的方式打破循环依赖关系
下面的代码会出异常(AttributeError: ‘module’ object has no attribute ‘prefs’)
1 | # dialog.py |
引入模块的时候,python按照深度优先的顺序执行下列操作:
- 在sys.path所制定的路径中,搜寻待引入的模块
- 从模块中加载代码,并保证这段代码能够正确编译
- 创建与该模块相对应的空对象
- 把这个空的模块对象添加到sys.modules里
- 运行模块对象中的代码,定义其内容
因为某些属性必须等系统把对用的代码执行完毕之后(第5步),才具备完整的定义。因为app模块在未定义任何内容的时候就引入了dialog模块,然后dialog又引入了app模块。而app模块尚未定义完整个引入的过程,还处在引入dialog的状态之中。按照上面第4步的规则,此时的app模块只是个空壳而已。而dialog模块却需要这个prefs,就抛出了AtrributeError异常。
方法一调整引入顺序
在app模块中移动到底部。当dialog模块反向引用app时,第五步几乎执行完 了,于是dialog能找到app.prefs的定义。 但是该方法和PEP 8 风格不符(import 应该在顶部)
1 | # app.py |
方法二 先引入、在配置、最后运行
只在模块中给出函数、类和常量的定义,而不要在引入的时候真正去运行那些函数。每个模块都将提供configure函数,等其他模块都引入完毕之后,我们在该模块上面调用一次configure,而这个函数访问其他模块的属性,以便将本模块的状态准备好。
1 | # dialog.py |
调用时:
1 | # main.py |
这个方案在很多情况下都很适合,而且方便开发者实现依赖注入等模式。但是有时候很难从代码中提取configure步骤。
另外模块内部划分不同阶段,会令代码不易理解(因为把对象的定义和配置分开了)
方法三 动态引入
1 | # dialog.py |
而app模块和最开始的一样。
一般来说,尽量不要使用这种动态引入的方案,因为import语句的执行开销,还是不小的。折中动态引入方案,还可能会在程序运行时导致非常奇怪的错误,如程序在运行很久后突然抛出SyntaxError异常。
不过这是最简单的方案,因为即可以缩减重构所花的精力,又可以尽量降低代码的复杂度。
配置虚拟环境
- 用pip show xxx可以看依赖那些包
- 比如Sphinx和flask都依赖jinja2的包,但是这个包如果发生重大变化,一个需要新版一个需要旧版那系统就没法运行了。
- 虚拟环境工具pyvenv工具(python3.4自带)早期python需要 pip install virtualenv,并在命令行通过virtualenv来使用
- 用pyvenv命令来新建名为myproject的虚拟环境,每一套虚拟环境都必须位于各自独立的目录之中(使用虚拟化环境的时候也不要去移动环境的目录)。该目录下面会产生响应的目录树与文件:
1 | $ pyvenv /tmp/myproject |
- 用source来运行bin/actiave脚本,该脚本修改所有环境变量,使之与虚拟环境相匹配。它还会更新命令提示符,把虚拟环境的名称包含进来,使得开发者可以明确知道自己所处的环境:
1 | $ source bin/activate |
- 在这个环境中,除了pip和steuptools是没有安装任何软件包的。外围系统的包这里不可用。可以用pip把包安装在当前虚拟环境
- 使用完虚拟环境后,通过deactivate命令回到默认的系统
1 | (myproject)$ deactivate |
- 用pip freeze可以把开发环境对软件包的依赖关系,保存到文件之中。按照管理,文件名为requirements.txt.
1 | (myproject)$ pip3 freeze > requirements.txt |
新的环境要安装只需要
- (otherproject)$ pip3 install -r /tmp/myproject/requirements.txt
部署
通过repr来输出调试信息
- 对内置的Python类型调用pring函数,会根据该值打印出一条易于阅读的字符串,这个字符串隐藏了类型信息。而repr函数,会根据该值返回一条可供打印的字符串。把这个repr传给内置的eval函数,就可以将其还原为初始的那个值
- 在格式字符串使用%s就类似str函数返回的,使用%r就和repr相符
- 类中可以定义
__repr__
方法 - 在任意对象上查询
__dict__
属性,观察其内部信息
用unitest来测试全部代码
- 要想确信Python程序能正常运行,唯一的办法就是编写测试。
- 内置的unittest是编写测试最简单的方法
- 如以下的代码:
1 | # utils.py |
- 测试是以TestCase的形式来组织的。每个以test开头的方法,都是一项测试。如果测试方法在运行过程中没有抛出任何Exception,也没有因assert语句而导致AssertionError,那么测试就算顺利通过。
- TestCase类提供了一些辅助方法,以供开发者在编写测试的时候做出各种断言。如assertEqual判断两值是否相等,assertTrue判断表达式是否为真,assertRaises验证程序是否能在适当的时候抛出相关的异常。
- 在TestCase子类中,可以定义一些辅助方法来令测试代码更加便于阅读,只是要注意,这些辅助方法不能以test开头。
- 有时候运行测试方法需要在TestCase类中把测试环境配置好。于是我们就覆写setUp和tearDown方法。系统在执行每个测试之前,都会调用一次setUp方法,在执行每个测试之后,执行一次tearDown方法,这样保证各项测试独立运行。
- 通常把一组相关的测试放在一个TestCase中(一个模块内的所有函数),如果某函数有很多边界状况,那就针对这个函数专门编写一个TestCase子类,次外也会针对每个类来编写TestCase,来测试该类及类中的所有方法。
用pdb实现交互测试
通常写在一行,使得不用能够方便的注释 import pdb; pdb.set_trace()
先分析性能在优化
Python提供了内置的性能分析工具,可以计算出程序某个部分的执行时间在总体的执行时间中所占的比率。
采用内置的cProfile模块比profile模块好,因为对受测代码的效率只会产生很小的影响。
下面的代码测试插入排序的:
1 | def insert_value(array, value): |
结果如下:
1 | 20003 function calls in 2.288 seconds |
说明如下:
- ncalls: The number of calls to the function during the profiling period.
- tottime: The number of seconds spent executing the function, excluding time spent executing other functions it calls.
- tottime percall: The average number of seconds spent in the function each time it was called, excluding time spent executing other functions it calls. This is tottime divided by ncalls.
- cumtime: The cumulative number of seconds spent executing the function, including time spent in all other functions it calls.
- cumtime percall: The average number of seconds spent in the function each time it was called, including time spent in all other functions it calls. This is cumtime divided by ncalls.
可以用stats.print_callers()查看该函数所消耗的执行时间究竟是哪些调用者分别引发的。
使用tracemalloc来掌握内存的使用及泄漏情况
调用内存的使用情况的第一种是内置的gc模块,让它列出垃圾收集器当前所知的每个对象。
1 | # using_gc.py |
但是gc模块不能告诉我们这些对象是如何分配出来的。可以用tracemalloc解决(Python3.4及之后的才有)
下面的打印导致内存增大的前三个对象,可以立即看出导致内存变大的主要因素以及分配那些对象的语句在源代码中的位置。
1 | import tracemalloc |
tracemalloc模块也可以打印出py在执行每一个分配内存操作时,具备的完整的堆栈信息.下面找到程中最消耗内存的那个内存分配操作,并将该操作的堆栈信息打印出来.
1 | § stats = time2.compare_to(time1, 'traceback') |