在GitHub上看到一个内容很赞的Python读物《Python 工匠:善用变量来改善代码质量》,通读一遍学到不少新知识,因此决定把这些有用的Tricks记下来,方便日后查阅。
所有内容摘自https://github.com/piglei/one-python-craftsman,作者piglei
Table of Contents
- 1 当Class不包含方法时,可以用NamedTuple代替
- 2 自定义对象的True/False
- 3 在条件判断中使用 all() / any()
- 4 使用 try/while/for 中 else 分支
- 5 以r开头的内建字符串函数
- 6 无穷大float("inf")和负无穷大float("-inf")
- 7 当需要频繁在list前端插入元素时,考虑用collections.deque
- 8 用collcetions.defaultdict统计次数
- 9 字典操作
- 10 next()函数
- 11 使用partial构造新函数
- 12 使用 product扁平化多层嵌套循环
- 13 使用 islice实现循环内隔行处理
- 14 使用 takewhile替代break语句
- 15 尝试用类来实现装饰器
- 16 使用 wrapt 模块编写更扁平的装饰器
- 17 使用 pathlib 模块改写代码
1 当Class不包含方法时,可以用NamedTuple代替
| 1 | from collections import namedtuple | 
2 自定义对象的True/False
在Python内,对象的布尔值由__bool__和__len__返回,如果未定义__bool__则返回__len__ != 0的值。
因此可以通过定义__bool__和__len__来直接定义对象的True/False。
| 1 | class UserCollection: | 
3 在条件判断中使用 all() / any()
all() 和 any() 两个函数非常适合在条件判断中使用。这两个函数接受一个可迭代对象,返回一个布尔值,其中:
- all(seq):仅当- seq中所有对象都为布尔真时返回- True,否则返回- False
- any(seq):只要- seq中任何一个对象为布尔真就返回- True,否则返回- False
假如我们有下面这段代码:
| 1 | def all_numbers_gt_10(numbers): | 
如果使用 all() 内建函数,再配合一个简单的生成器表达式,上面的代码可以写成这样:
| 1 | def all_numbers_gt_10_2(numbers): | 
4 使用 try/while/for 中 else 分支
让我们看看这个函数:
| 1 | def do_stuff(): | 
在函数 do_stuff 中,我们希望只有当 do_the_first_thing() 成功调用后(也就是不抛出任何异常),才继续做第二个函数调用。为了做到这一点,我们需要定义一个额外的变量 first_thing_successed 来作为标记。
其实,我们可以用更简单的方法达到同样的效果:
| 1 | def do_stuff(): | 
在 try 语句块最后追加上 else 分支后,分支下的do_the_second_thing() 便只会在 try 下面的所有语句正常执行(也就是没有异常,没有 return、break 等)完成后执行。
类似的,Python 里的 for/while 循环也支持添加 else 分支,它们表示:当循环使用的迭代对象被正常耗尽、或 while 循环使用的条件变量变为 False 后才执行 else 分支下的代码。
5 以r开头的内建字符串函数
Python 的字符串有着非常多实用的内建方法,最常用的有 .strip()、.split() 等。这些内建方法里的大多数,处理起来的顺序都是从左往右。但是其中也包含了部分以 r 打头的从右至左处理的镜像方法。在处理特定逻辑时,使用它们可以让你事半功倍。
假设我们需要解析一些访问日志,日志格式为:”{user_agent}” {content_length}:
| 1 | >>> log_line = '"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36" 47632' | 
如果使用 .split() 将日志拆分为 (user_agent, content_length),我们需要这么写:
| 1 | >>> l = log_line.split() | 
但是如果使用 .rsplit() 的话,处理逻辑就更直接了:
| 1 | >>> log_line.rsplit(None, 1) | 
6 无穷大float("inf")和负无穷大float("-inf")
7 当需要频繁在list前端插入元素时,考虑用collections.deque
8 用collcetions.defaultdict统计次数
| 1 | from collections import defaultdict | 
9 字典操作
- 如果移除字典成员,不关心是否存在:- 调用 pop 函数时设置默认值,比如 dict.pop(key, None)
 
- 调用 pop 函数时设置默认值,比如 
- 在字典获取成员时指定默认值:dict.get(key, default_value)
- 对列表进行不存在的切片访问不会抛出 IndexError异常:["foo"][100:200]
10 next()函数
next() 是一个非常实用的内建函数,它接收一个迭代器作为参数,然后返回该迭代器的下一个元素。使用它配合生成器表达式,可以高效的实现“从列表中查找第一个满足条件的成员”之类的需求。
| 1 | numbers = [3, 7, 8, 2, 21] | 
11 使用partial构造新函数
假设这么一个场景,在你的代码里有一个参数很多的函数 A,适用性很强。而另一个函数 B 则是完全通过调用 A 来完成工作,是一种类似快捷方式的存在。
比方在这个例子里, double 函数就是完全通过 multiply 来完成计算的:
| 1 | def multiply(x, y): | 
对于上面这种场景,我们可以使用 functools 模块里的 partial() 函数来简化它。
partial(func, *args, **kwargs) 基于传入的函数与可变(位置/关键字)参数来构造一个新函数。所有对新函数的调用,都会在合并了当前调用参数与构造参数后,代理给原始函数处理。
利用 partial 函数,上面的 double 函数定义可以被修改为单行表达式,更简洁也更直接。
| 1 | import functools | 
建议阅读:partial 函数官方文档
12 使用 product扁平化多层嵌套循环
虽然我们都知道“扁平的代码比嵌套的好”。但有时针对某类需求,似乎一定得写多层嵌套循环才行。比如下面这段:
| 1 | def find_twelve(num_list1, num_list2, num_list3): | 
对于这种需要嵌套遍历多个对象的多层循环代码,我们可以使用 product() 函数来优化它。product() 可以接收多个可迭代对象,然后根据它们的笛卡尔积不断生成结果。
| 1 | from itertools import product | 
相比之前的代码,使用 product() 的函数只用了一层 for 循环就完成了任务,代码变得更精炼了。
13 使用 islice实现循环内隔行处理
有一份包含 Reddit 帖子标题的外部数据文件,里面的内容格式是这样的:
| 1 | python-guide: Python best practices guidebook, written for humans. | 
可能是为了美观,在这份文件里的每两个标题之间,都有一个 "---" 分隔符。现在,我们需要获取文件里所有的标题列表,所以在遍历文件内容的过程中,必须跳过这些无意义的分隔符。
参考之前对 enumerate() 函数的了解,我们可以通过在循环内加一段基于当前循环序号的 if 判断来做到这一点:
| 1 | def parse_titles(filename): | 
但对于这类在循环内进行隔行处理的需求来说,如果使用 itertools 里的 islice() 函数修饰被循环对象,可以让循环体代码变得更简单直接。
islice(seq, start, end, step) 函数和数组切片操作( list[start:stop:step] )有着几乎一模一样的参数。如果需要在循环内部进行隔行处理的话,只要设置第三个递进步长参数 step 值为 2 即可(默认为 1)。
| 1 | from itertools import islice | 
14 使用 takewhile 替代 break 语句
有时,我们需要在每次循环开始时,判断循环是否需要提前结束。比如下面这样:
| 1 | for user in users: | 
对于这类需要提前中断的循环,我们可以使用 takewhile() 函数来简化它。takewhile(predicate, iterable) 会在迭代 iterable 的过程中不断使用当前对象作为参数调用 predicate 函数并测试返回结果,如果函数返回值为真,则生成当前对象,循环继续。否则立即中断当前循环。
使用 takewhile 的代码样例:
| 1 | from itertools import takewhile | 
itertools 里面还有一些其他有意思的工具函数,他们都可以用来和循环搭配使用,比如使用 chain 函数扁平化双层嵌套循环、使用 zip_longest 函数一次同时循环多个对象等等。
itertools — Functions creating iterators for efficient looping
Infinite iterators:
| Iterator | Arguments | Results | Example | 
|---|---|---|---|
| count() | start, [step] | start, start+step, start+2*step, … | count(10) --> 10 11 12 13 14 ... | 
| cycle() | p | p0, p1, … plast, p0, p1, … | cycle('ABCD') --> A B C D A B C D ... | 
| repeat() | elem [,n] | elem, elem, elem, … endlessly or up to n times | repeat(10, 3) --> 10 10 10 | 
Iterators terminating on the shortest input sequence:
| Iterator | Arguments | Results | Example | 
|---|---|---|---|
| accumulate() | p [,func] | p0, p0+p1, p0+p1+p2, … | accumulate([1,2,3,4,5]) --> 1 3 6 10 15 | 
| chain() | p, q, … | p0, p1, … plast, q0, q1, … | chain('ABC', 'DEF') --> A B C D E F | 
| chain.from_iterable() | iterable | p0, p1, … plast, q0, q1, … | chain.from_iterable(['ABC', 'DEF']) --> A B C D E F | 
| compress() | data, selectors | (d[0] if s[0]), (d[1] if s[1]), … | compress('ABCDEF', [1,0,1,0,1,1]) --> A C E F | 
| dropwhile() | pred, seq | seq[n], seq[n+1], starting when pred fails | dropwhile(lambda x: x<5, [1,4,6,4,1]) --> 6 4 1 | 
| filterfalse() | pred, seq | elements of seq where pred(elem) is false | filterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8 | 
| groupby() | iterable[, key] | sub-iterators grouped by value of key(v) | |
| islice() | seq, [start,] stop [, step] | elements from seq[start:stop:step] | islice('ABCDEFG', 2, None) --> C D E F G | 
| starmap() | func, seq | func(seq[0]), func(seq[1]), … | starmap(pow, [(2,5), (3,2), (10,3)]) --> 32 9 1000 | 
| takewhile() | pred, seq | seq[0], seq[1], until pred fails | takewhile(lambda x: x<5, [1,4,6,4,1]) --> 1 4 | 
| tee() | it, n | it1, it2, … itn splits one iterator into n | |
| zip_longest() | p, q, … | (p[0], q[0]), (p[1], q[1]), … | zip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D- | 
Combinatoric iterators:
| Iterator | Arguments | Results | 
|---|---|---|
| product() | p, q, … [repeat=1] | cartesian product, equivalent to a nested for-loop | 
| permutations() | p[, r] | r-length tuples, all possible orderings, no repeated elements | 
| combinations() | p, r | r-length tuples, in sorted order, no repeated elements | 
| combinations_with_replacement() | p, r | r-length tuples, in sorted order, with repeated elements | 
| product('ABCD', repeat=2) | AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD | |
| permutations('ABCD', 2) | AB AC AD BA BC BD CA CB CD DA DB DC | |
| combinations('ABCD', 2) | AB AC AD BC BD CD | |
| combinations_with_replacement('ABCD', 2) | AA AB AC AD BB BC BD CC CD DD | 
15 尝试用类来实现装饰器
绝大多数装饰器都是基于函数和 闭包) 实现的,但这并非制造装饰器的唯一方式。事实上,Python 对某个对象是否能通过装饰器(@decorator)形式使用只有一个要求:decorator 必须是一个“可被调用(callable)的对象。
| 1 | # 使用 callable 可以检测某个对象是否“可被调用” | 
函数自然是“可被调用”的对象。但除了函数外,我们也可以让任何一个类(class)变得“可被调用”(callable)。办法很简单,只要自定义类的 __call__ 魔法方法即可。
| 1 | class Foo: | 
基于这个特性,我们可以很方便的使用类来实现装饰器。
下面这段代码,会定义一个名为 @delay(duration) 的装饰器,使用它装饰过的函数在每次执行前,都会等待额外的 duration 秒。同时,我们也希望为用户提供无需等待马上执行的 eager_call 接口。
| 1 | import time | 
如何使用装饰器的样例代码:
| 1 | @delay(duration=2) | 
@delay(duration) 就是一个基于类来实现的装饰器。当然,如果你非常熟悉 Python 里的函数和闭包,上面的 delay 装饰器其实也完全可以只用函数来实现。所以,为什么我们要用类来做这件事呢?
与纯函数相比,我觉得使用类实现的装饰器在特定场景下有几个优势:
- 实现有状态的装饰器时,操作类属性比操作闭包内变量更符合直觉、不易出错
- 实现为函数扩充接口的装饰器时,使用类包装函数,比直接为函数对象追加属性更易于维护
- 更容易实现一个同时兼容装饰器与上下文管理器协议的对象(参考 unitest.mock.patch)
16 使用 wrapt 模块编写更扁平的装饰器
在写装饰器的过程中,你有没有碰到过什么不爽的事情?不管你有没有,反正我有。我经常在写代码的时候,被下面两件事情搞得特别难受:
- 实现带参数的装饰器时,层层嵌套的函数代码特别难写、难读
- 因为函数和类方法的不同,为前者写的装饰器经常没法直接套用在后者上
比如,在下面的例子里,我实现了一个生成随机数并注入为函数参数的装饰器。
| 1 | import random | 
@provide_number 装饰器功能看上去很不错,但它有着我在前面提到的两个问题:嵌套层级深、无法在类方法上使用。如果直接用它去装饰类方法,会出现下面的情况:
| 1 | class Foo: | 
Foo 类实例中的 print_random_number 方法将会输出类实例 self ,而不是我们期望的随机数 num。
之所以会出现这个结果,是因为类方法(method)和函数(function)二者在工作机制上有着细微不同。如果要修复这个问题,provider_number 装饰器在修改类方法的位置参数时,必须聪明的跳过藏在 *args 里面的类实例 self 变量,才能正确的将 num 作为第一个参数注入。
这时,就应该是 wrapt 模块闪亮登场的时候了。wrapt 模块是一个专门帮助你编写装饰器的工具库。利用它,我们可以非常方便的改造 provide_number 装饰器,完美解决“嵌套层级深”和“无法通用”两个问题,
| 1 | import wrapt | 
使用 wrapt 模块编写的装饰器,相比原来拥有下面这些优势:
- 嵌套层级少:使用 `@wrapt.decorator` 可以将两层嵌套减少为一层
- 更简单:处理位置与关键字参数时,可以忽略类实例等特殊情况
- 更灵活:针对 instance值进行条件判断后,更容易让装饰器变得通用
17 使用 pathlib 模块改写代码
为了让文件处理变得更简单,Python 在 3.4 版本引入了一个新的标准库模块:pathlib。它基于面向对象思想设计,封装了非常多与文件操作相关的功能。如果使用它来改写上面的代码,结果会大不相同。
使用 pathlib 模块后的代码:
| 1 | from pathlib import Path | 
和旧代码相比,新函数只需要两行代码就完成了工作。而这两行代码主要做了这么几件事:
- 首先使用 Path(path) 将字符串路径转换为 Path对象
- 调用 .glob(‘*.txt’) 对路径下所有内容进行模式匹配并以生成器方式返回,结果仍然是 Path对象,所以我们可以接着做后面的操作
- 使用 .with_suffix(‘.csv’) 直接获取使用新后缀名的文件全路径
- 调用 .rename(target) 完成重命名
相比 os 和 os.path,引入 pathlib 模块后的代码明显更精简,也更有整体统一感。所有文件相关的操作都是一站式完成。
其他用法
除此之外,pathlib 模块还提供了很多有趣的用法。比如使用 / 运算符来组合文件路径:
| 1 | # 😑 旧朋友:使用 os.path 模块 | 
或者使用 .read_text() 来快速读取文件内容:
| 1 | # 标准做法,使用 with open(...) 打开文件 | 
除了我在文章里介绍的这些,pathlib 模块还提供了非常多有用的方法,强烈建议去 官方文档 详细了解一下。
如果上面这些都不足以让你动心,那么我再多给你一个使用 pathlib 的理由:PEP-519 里定义了一个专门用于“文件路径”的新对象协议,这意味着从该 PEP 生效后的 Python 3.6 版本起,pathlib 里的 Path 对象,可以和以前绝大多数只接受字符串路径的标准库函数兼容使用:
| 1 | >>> p = Path('/tmp') | 
所以,无需犹豫,赶紧把 pathlib 模块用起来吧。
Hint: 如果你使用的是更早的 Python 版本,可以尝试安装 pathlib2 模块 。

