在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 模块 。