Python Cookbook
这本书和一般的语言书在结构上有非常大的不同,因为这本书是摘录了论坛里面精华的内容,所以书上的内容相对割裂,在查找的时候比较困难。这一篇博客主要是结合自己的日常开发,做一个常用技巧的收录,方便直接查找要用的工具。
工具
快速实现小根堆
一般来说堆这种数据结构会应用于优先队列,在python和java中都有对应的工具类,python中的是:queue.PriorityQueue,这是一个线程安全的优先队列。
另外,python标准库还提供了额外的库来直接使用数组实现一个堆,应对K最小、K最大问题时会非常有效。
1
2
3
4
5
6
7
8
9
import heapq
import random
nums = [random.randint(-100, 100) for x in range(20)]
heapq.heapify(nums)
print([heapq.heappop(nums) for x in range(20)])
output:
[-95, -87, -82, -79, -50, -48, -45, -45, -44, -27, -22, -1, 0, 1, 6, 9, 72, 85, 95, 96]
详细的内容可以参考官方文档:heapq — Heap queue algorithm。文档中还特别写明了heapify的时间复杂度是线性的,未来有兴趣的时候可以看一下是如何实现的。
使用zip连接多个数组
如果想要将多个数组里面的内容一一对应地组合起来时,可以使用zip()函数。
1
2
3
4
5
6
7
8
9
10
x = [1, 2, 3]
y = [4, 5, 6]
z = [7, 8, 9]
for a in zip(x, y, z):
print(a)
output:
(1, 4, 7)
(2, 5, 8)
(3, 6, 9)
使用Counter统计每个元素的出现次数
很多时候需要统计一个列表中的单词或者元素出现次数,一般会用for语句来写,使用迭代器似乎没有很好的写法,这里推荐系统自带的collections.Counter容器。
1
2
3
4
5
6
7
8
9
import random
from collections import Counter
x = [random.randint(0, 5) for _ in range(50)]
counter = Counter(x)
print(counter)
output:
Counter({0: 13, 3: 12, 4: 8, 1: 6, 2: 6, 5: 5})
详细的内容可以参考官方文档:collections — Container datatypes,同时该容器还提供了most_common用于返回最常见的N个对象。
正则字符串替换
最简单的字符串替换方式是使用replace函数来进行替换,这无需多言。相对复杂的方式是使用正则来进行替换,正则替换对应的函数是re.sub,该函数包含了三个参数,分别是:
- 匹配的模式字符串,和其他正则一样,用()来表示一个组
- 替换模式字符串,其中用反斜杠数字的表示第几组,比如\1\2
- 待替换内容的字符串
这样解释起来依旧抽象,我写一个日期转换的例子,把2000-05-03转化成2000年05月03日:
1
2
3
4
5
6
7
8
import re
date_str = '2000-05-03'
date_str = re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\1年\2月\3日', date_str)
print(date_str)
output:
2000年05月03日
在使用时别忘了正则字符串要加上r开头作为识别标签,另外如果需要重复使用匹配的模式字符串,可以使用re.compile提前编译来提升效率,更多用法可以参考官方文档:re — Regular expression operations — re.sub。
在python中表达正负无穷和非数字浮点数
在python中可以直接用float来创建相关数值:
1
2
3
4
5
6
7
8
x = float("-inf")
y = float("inf")
z = float("nan")
print(x, y, z, x < y)
output:
-inf inf nan True
使用yield from拍平嵌套数组
yield from是一个生成器语法,其实日常开发中很少用到,但是在某些特定的情况下会让代码显得非常清晰,比如希望拍平一个多层嵌套的数组:[1, 2, 3, [4, 5], [6, [7, [8]]]],希望能获得:[1,2,3,4,5,6,7,8],可以使用如下方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
def flat(nums):
if isinstance(nums, list):
for x in nums:
yield from flat(x)
else:
yield nums
array = [1, 2, 3, [4, 5], [6, [7, [8]]]]
print(list(flat(array)))
output:
[1, 2, 3, 4, 5, 6, 7, 8]
使用csv系统库读取csv文件
一般处理csv文件的时候,我们会习惯用逗号作为分隔符来处理,但是系统其实提供了一种更易用的csv库来专门处理这一项内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import csv
from pprint import pprint
from collections import namedtuple
from tempfile import TemporaryFile
with TemporaryFile("w+t") as f:
f.write("年份,人数,报课数,单价,总收入\n")
f.write("1,250,300,4500,1350000\n")
f.write("2,350,400,4500,1800000\n")
f.seek(0)
f_csv = csv.reader(f)
headers = next(f_csv)
Row = namedtuple("Row", headers)
rows = [Row(*r) for r in f_csv]
pprint(rows)
output:
[Row(年份='1', 人数='250', 报课数='300', 单价='4500', 总收入='1350000'),
Row(年份='2', 人数='350', 报课数='400', 单价='4500', 总收入='1800000')]
上面的例子中另外用到的几个小工具:
- TemporaryFile:用于生成一个临时文件
- namedtuple:用于自定义一个带参数名的元组类
- pprint:在打印内容比较复杂的情况下代替print函数,可以获得一个比较好的打印效果
####
设计
用re模块实现一个文本解析器
在做文本处理的时候,可能要在一堆有特定格式的字符串中,进行模式匹配,找到对应的操作信息,并根据信息做出响应,比如说,我们需要将这个字符串:
1
foo = 23 + 43 * 10
解析成一个操作令牌的列表:
1
[('name', 'foo'), ('eq', '='), ('num', '23'), ('plus', '+'), ('num', '43'), ('times', '*'), ('num', '10')]
这时可以使用一些带有命名的正则捕获组来定义所有可能的令牌,并且使用re.finditer方法来匹配每一种令牌,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import re
from collections import namedtuple
NAME = r"(?P<name>[a-zA-Z_][a-zA-Z_0-9]+)"
NUM = r'(?P<num>\d+)'
PLUS = r'(?P<plus>\+)'
TIMES = r'(?P<times>\*)'
EQ = r'(?P<eq>=)'
WS = r'(?P<ws>\s+)'
pattern = re.compile('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS]))
op_str = 'foo = 23 + 43 * 10'
Token = namedtuple('Token', ['type', 'value'])
tokens = (Token(x.lastgroup, x.group()) for x in pattern.finditer(op_str))
print([x for x in tokens if x.type != 'ws'])
output:
[Token(type='name', value='foo'), Token(type='eq', value='='), Token(type='num', value='23'), Token(type='plus', value='+'), Token(type='num', value='43'), Token(type='times', value='*'), Token(type='num', value='10')]
文本解析是一个很大的主题,也是编译原理基础的一部分,这部分内容在官方文档中也有相应介绍:re — Regular expression operations — writing a tokenizer。如果说只需要解析一个运算符和函数的表达式的话,还有一种专用算法:wiki — 调度场算法。
使用cached_property使类属性自动缓存
使用系统自带的property装饰器可以将一个成员方法转化为属性,但是这么做的话每次使用这个属性都会调用相应的方法,如果这个方法本身比较耗时,那么计算一次以后自动缓存会是一个好方法。
在python的3.8版本以前,并没有官方库的支持。如果项目中使用了django的话,可以用django.utils.functional下面的cached_property装饰器,但是在python3.8以后,cached_property已经被合入到官方库,我们可以直接使用官方提供的装饰器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import time
from functools import cached_property
from threading import Thread
class Foo:
def __init__(self):
self.count = 0
@cached_property
def bar(self):
time.sleep(0.5)
self.count += 1
return self.count
f = Foo()
threads = []
for x in range(10):
t = Thread(target=lambda: f.bar)
t.start()
threads.append(t)
for t in threads:
t.join()
print(f.bar)
output:
1
上面这段代码还检查了cached_property是否是线程安全的,如果将cached_property的引用改为:
1
from django.utils.functional import cached_property
那么我们会得到的输出是10,这说明django的cached_property并不是线程安全的,其实很多第三方库的cached_property都不是线程安全的,因此在多线程情况下尽量使用官方库中的cached_property,以免出现意外bug。
使用元类构建注册工厂
注册工厂是一个比较常见的设计模式,尤其在快速迭代的项目中,如果已经实现设计好一个抽象模型,在添加新功能时往往会用到。在python中我们可以使用元类让新功能自动注册到工厂中去,这样我们在添加新功能时,只需要继承某一个基类就可以完成自动加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Factory:
workers = {}
@classmethod
def build(cls, name):
return cls.workers[name]().build()
class WorkerMeta(type):
def __new__(mcs, what, bases=None, attrs=None):
new_class = super().__new__(mcs, what, bases, attrs)
if what != "Worker":
Factory.workers[what.replace("Worker", "").lower()] = new_class
return new_class
class Worker(metaclass=WorkerMeta):
pass
class CarWorker(Worker):
def build(self):
return "super car"
class PlaneWorker(Worker):
def build(self):
return "super plane"
print(Factory.build("car"))
print(Factory.build("plane"))
output:
super car
super plane
在上面的例子中,如果需要给现有工厂新增功能,仅需要写一个新的类继承Worker即可,不会对现有代码造成任何入侵。
另外,使用元类还可以对新类进行各种操作,这在实现类库或者中间件的时候尤为方便,在未来实现中间件的时候,可以参考一下Django类库中models的继承结构:
1
2
3
4
5
6
7
8
9
10
class ModelBase(type):
"""Metaclass for all models."""
def __new__(cls, name, bases, attrs, **kwargs):
super_new = super().__new__
...
class Model(metaclass=ModelBase):
def __init__(self, *args, **kwargs):
...