Python宝典

如果说优雅也有缺点的话,那就是你需要艰巨的工作才能得到它,需要良好的教育才能欣赏它。
—— Edsger Wybe Dijkstra

笔者精心整理了许多实用的Python tricks,欢迎各位Pythonistia参考。
阅读本文前有两点要提醒大家:

  1. 请确保你的Python是最新版或者与之接近的版本
  2. 本文仅仅谈Python语言本身及其标准库,不会提到任何第三方的库

字符串

格式化字符串

在字符串前加f,就可以在里面用大括号嵌入变量了(可以代替format函数)

1
2
3
4
>>> a = 5
>>> b = 10
>>> f'Five plus ten is {a + b} and not {2 * (a + b)}.'
'Five plus ten is 15 and not 30.'

字符串拼接

1
2
3
>>> text = ['I', ' Love ', 'Python!']
>>> print(''.join(text))
I Love Python!

字符串的contains

1
2
>>> 'ov' in 'love'
True

函数

生成器

生成器能实现惰性计算,一次返回一个结果,也就是说它不会一次性返回所有结果,这在处理大数据时颇为有用。
生成器同样支持推导式。

1
2
3
4
5
>>> squares = (x ** 2 for x in range(5))
>>> squares
<generator object <genexpr> at 0x000001B679B31570>
>>> list(squares)
[0, 1, 4, 9, 16]

如果是函数的话就得用yield关键词来表示生成器函数

1
2
3
4
5
6
7
>>> def gen():
... yield from 'AB'
... yield from range(1, 3)
>>> gen()
<generator object gen at 0x000001B679BDE048>
>>> list(gen())
['A', 'B', 1, 2]

装饰器

装饰器的主要用途:打印日志、检测性能、数据库事务、URL路由
它本质上就是一个高阶函数,它接收一个函数作为参数,然后,返回一个新函数。
想理解装饰器,就得知道以下两点:

  1. 函数皆对象
  2. 闭包特性(内函数能捕捉到外函数的环境变量)

简单的日志函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from datetime import datetime
import functools
def log(f):
@functools.wraps(f)
def wr(*args, **kwargs):
print(f'call {f.__name__}() at {datetime.now()}')
return f(*args, **kwargs)
return wr
@log
def square(x):
return x ** 2
>>> square(2)
call square() at 2018-01-24 11:01:19.547516
4

注意到为了让@deco自适应任何参数定义的函数,我们将可变参数args, *kwargs作为了wr的参数

@functools.wraps(f)

为了防止wr的函数属性覆盖掉原函数的属性,我们必须利用@functools.wraps(f)来把原函数的所有属性复制到新函数里

1
2
3
4
5
6
# 不加@functools.wraps(f)的情况下
>>> square.__name__
'wr'
# 加了@functools.wraps(f)的情况下
>>> square.__name__
'square'

如果想给装饰器传递参数,那么你必须利用闭包特性再嵌套一层函数,不过这并不常用。

函数性能检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def perf(f):
@functools.wraps(f)
def wr(*args, **kwargs):
start = time.time()
r = f(*args, **kwargs)
end = time.time()
print(f'call {f.__name__}() in {end - start}')
return r
return wr
@perf
def test(x):
time.sleep(2)
return x
>>> test(5)
call test() in 2.0007083415985107
5

数据库事务

1
2
3
4
5
6
7
8
9
def link_mysql(fun):
def wr(*args, **kwargs):
with pymysql.connect(host=host, port=port, user=user, passwd=passwd, db=dbname, charset=charset) as cur:
fun(cur, *args, **kwargs)
return wr
@link_mysql
def insert_data(cur, ...):
# execute your sql here.

上下文管理器

文件操作(超常用)、进程互斥锁和支持上下文的其他对象
目的是为了代替try语句和简化语法
以文件操作为例:利用它,文件会自动打开和关闭

1
2
with open('/path/to/file', 'r') as f:
handle_f

上下文语句支持嵌套(nested)
例如将a文件的源数据写入b文件里:

1
2
with open('a.txt') as i, open('b.txt') as o:
o.write(i.read())

偏函数

partial()用于把一个函数的某些参数给固定住(也就是设置默认值),并返回一个新的函数。

1
2
def int2(x, base=2):
return int(x, base)

相当于:

1
2
import functools
int2 = functools.partial(int, base=2)

函数注解

有的时候你希望他人了解函数参数的变量类型,这时就得用到函数注解了,既方便生成文档,也使得代码便于维护。
注解分为两种:形参和返回值。前者只要在相应的形参后加冒号和类型就可以了,后者是箭头加类型

1
2
3
4
5
6
7
8
9
10
11
def hash(block: dict) -> str:
"""Creates a SHA-256 hash of a Block
Args:
block (dict): Block
Returns:
str
"""
block_string = json.dumps(block, sort_keys=True).encode()
return hashlib.sha256(block_string).hexdigest()

以上函数是区块链里的hash函数,通过注解我们便能对它的用途一目了然,并且它的文档字符串也是根据注解自动生成的。

数据结构

元组

元组是一个immutable对象,有以下重要性:

  • 性能优化
  • 线程安全
  • 可以作为dict的key(hashable)
  • 拆包特性

元组拆包

a, b = b, a
两个数字交换的原理就是它。
以下是利用它来获取文件名及其扩展名

1
2
3
4
5
6
>>> import os
>>> filename, ext = os.path.splitext('patch.exe')
>>> filename
'patch'
>>> ext
'exe'

利用*/号运算符,我们能够获取剩余元素
比如获取某文件的第一个和最后一个数据

1
2
3
with open('text.txt', 'r') as f:
first, *middle, last = f.read()
print(first, last)

列表

切片

如果想要获取列表的多个元素,就得用到切片

1
list[start:stop:step]

你甚至可以给切片命名,增强复用性和可读性:

1
2
s = slice(start,stop,step)
list[s]

列表推导式

这是Python最强大的几个特征之一。
格式也很简单,其中if条件可按需添加,for循环也可以有多个

1
[i for i in iterable if condition]

构造url

写爬虫时,我们首先要根据url规律来构造爬取队列tasklist
比如konachan的前3页可以这么构造

1
2
3
4
5
6
>>> domain = 'http://konachan.net'
>>> tasklist = [f'{domain}/post?page={n}' for n in range(1, 4)]
>>> pprint(tasklist)
['http://konachan.net/post?page=1',
'http://konachan.net/post?page=2',
'http://konachan.net/post?page=3']

过滤非法字符串

利用if,列表推导式还能起到过滤的效果

1
2
3
4
def rectify(name):
illegal_chars = {'?', '<', '>', '|', '*', '"', ":"}
name = ''.join([c for c in name if c not in illegal_chars])
return name

以上函数实现了过滤文件非法字符串的功能

索引迭代

enumerate()可以把一个list变成索引-元素对。
比如写爬虫的log时能同时把item以及它的索引给打印出来

1
2
for i, item in enumerate(items):
print(f'[{i}]: {item}')

zip

zip()可以将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,并返回一个迭代器,常用于同时遍历两个可迭代对象。

1
2
3
4
5
6
>>> li1 = ['Python' ,'JavaScript', 'Java']
>>> li2 = [1, 2, 3]
>>> nl = zip(li1, li2)
<zip object at memory>
>>> list(nl)
[('Python', 1), ('JavaScript', 2), ('Java', 3)]

配合dict可以生成字典

1
2
3
4
>>> l1 = ['A', 'B', 'C']
>>> l2 = [1, 2, 3]
>>> dict(zip(l1, l2))
{'A': 1, 'B': 2, 'C': 3}

append和extend

1
2
3
4
5
6
7
>>> x = [1, 2, 3]
>>> x.extend([4, 5])
>>> x
[1, 2, 3, 4, 5]
>>> x.append([6, 7])
>>> x
[1, 2, 3, 4, 5, [6, 7]]

字典

本质是键值对哈希表

字典推导式

类似的,字典和列表一样具有推导式,格式如下

1
{k: v for keys, values in dict().items()}

以下是将pandas的DataFrame转化为numpy的array

1
2
3
4
import pandas as pd
data = pd.read_csv(...)
x = data['features']
x = {k: np.array(v) for k, v in dict(x).items()}

get

用来获取某个键的值,不存在的话就用设置的default(默认为None)

1
2
3
4
5
6
>>> d = dict(a=1, b=2)
>>> d.get('a')
1
>>> d.get('c')
>>> d.get('d', 2)
2

合并

1
2
3
4
5
>>> d1 = {'a':1, 'b':2}
>>> d2 = {'c':3, 'd':4}
>>> nd = {**d1, **d2}
>>> nd
{'a': 1, 'b': 2, 'c': 3, 'd': 4}

类似的,列表也可以这样合并

1
2
3
4
5
>>> l1 = [1, 2]
>>> l2 = [3, 4]
>>> l3 = [*l1, *l2]
>>> l3
[1, 2, 3, 4]

switch的实现

利用表驱动实现

1
2
3
4
5
6
7
8
9
10
def get_contry_abbr(contry):
contry_list = {
'China': 'CHN',
'America': 'USA',
'Japan': 'JPN'
}
return contry_list.get(contry, '')
>>> get_contry_abbr('America')
'USA'

键值排序

1
2
3
4
5
>>> rows = [{k1: v1, k2: v2 ...}, ...]
>>> from operator import itemgetter
# 根据k1排序
>>> sorted(rows, key=itemgetter(k1))
# 类似的,对于class的对象可以用attrgetter进行排序

键值对翻转

1
2
3
4
>>> kv = {'a': 6, 'b': 2, 'end': inf}
>>> vk = dict(zip(kv.values(), kv.keys()))
>>> vk
{6: 'a', 2: 'b', inf: 'end'}

集合

集合是一个无序不重复元素的集,同样支持推导式

1
2
3
4
5
6
7
8
>>> charset = {'a', 'b', 'c', 'a', 'b', 'd'}
>>> charset
{'d', 'b', 'c', 'a'}
>>> 'd' in charset
True
>>> a = {c for c in 'abracadabra' if c not in 'abc'}
>>> a
{'r', 'd'}

找出不同元素

set.difference可以找出两个集合间不同的元素。
例如,找出数组中中断的元素

1
2
3
4
5
>>> nums = [4,3,2,7,8,2,3,1]
>>> broken_nums = set(nums)
>>> continuous_nums = set(range(1, len(nums)+1))
>>> list(continuous_nums.difference(broken_nums))
[5, 6]

OOP

魔术方法

魔术方法可以用来定制类的功能。
比如__repr__用来调试时打印类的字符串

1
2
3
4
5
6
7
8
9
10
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f'<Person {self.name} age: {self.age}>'
>>> p = Person('alphardex', 21)
>>> p
<Person alphardex age: 21>

更多的比如__getitem__能够实现序列协议(切片)
想了解更多魔术方法请参见官方文档

只读属性

可以通过在变量名前加__来使其变成私有变量,外部无法直接访问,但可以通过类定义的方法来访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person(object):
def __init__(self, name):
self.__name = name
def get_name(self):
return self.__name
def set_name(self, name):
self.__name = name
>>> p = Person('alphardex')
>>> p.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'name'
>>> p.get_name()
'alphardex'
>>> p.set_name('wang')
>>> p.get_name()
'wang'

@property

肯定有的人不习惯通过方法来访问私有变量,那么如何用属性来访问私有变量呢?这时就要用到@property了,它可以把一个方法变成属性调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person(object):
def __init__(self, name):
self.__name = name
@property
def name(self):
return self.__name
@name.setter
def name(self, value):
self.__name = value
>>> p = Person('alphardex')
>>> p.name
'alphardex'
>>> p.name = 'wang'
>>> p.name
'wang'

__slots__

当我们定义了一个class并用其创建了一个实例后,可以动态地给其绑定属性,如果要限制这一点,可以利用__slots__

1
2
3
4
5
6
7
8
9
10
class Person(object):
__slots__ = ('name', 'age')
>>> p = Person('wang')
>>> p.name = 'wang'
>>> p.age = 21
>>> p.skill = 'Python'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'skill'

标准库

collections

用于实现其他数据结构,本文仅列举最常用的三种

双向队列

1
2
3
4
5
6
>>> from collections import deque
>>> q = deque([1,2,3])
>>> q.popleft()
1
>>> q
deque([2, 3])

计数器

1
2
3
4
5
6
>>> from collections import Counter
>>> c = Counter('hello world')
>>> c
Counter({'l': 3, 'o': 2, ' ': 1, 'e': 1, 'd': 1, 'h': 1, 'r': 1, 'w': 1})
>>> c.most_common(2)
[('l', 3), ('o', 2)]

1
2
3
4
5
6
7
8
9
10
>>> import heapq
>>> a = [1, 2, 3]
>>> b = [4, 5, 6]
>>> c = list(heapq.merge(a, b))
>>> c
[1, 2, 3, 4, 5, 6]
>>> heapq.nlargest(3, c)
[6, 5, 4]
>>> heapq.nsmallest(3, c)
[1, 2, 3]

linecache

用来获取文件的行,一定程度上可代替with-open语句

1
2
3
>>> import linecache
>>> linecache.getline('...', 8) # 获取文件的第八行
>>> linecache.getlines('...') # 获取文件的所有行,返回一列表

threading

首先,让我们写一个单线程爬虫
konachan.py

1
2
3
4
5
6
7
8
9
10
11
12
13
import looter as lt
domain = 'https://konachan.net'
def crawl(url):
tree = lt.fetch(url)
imgs = tree.cssselect('a.directlink')
lt.save_imgs(imgs)
if __name__ == '__main__':
tasklist = [f'{domain}/post?page={n}' for n in range(1, 3)]
result = [crawl(task) for task in tasklist]
1
2
3
4
$ time python konachan.py
real 0m56.992s
user 0m0.000s
sys 0m0.015s

运行后,发现它的速度很慢,如何提高呢?试试看threading吧。
threading是一个线程模块,利用它能实现多线程。
konachan_threading.py

1
2
3
4
5
6
7
8
9
10
import threading
from konachan import *
if __name__ == '__main__':
tasklist = [f'{domain}/post?page={n}' for n in range(1, 3)]
threads = [threading.Thread(target=crawl, args=(task,)) for task in tasklist]
for t in threads:
t.start()
for t in threads:
t.join()
  • threads利用列表推导式创建了3个线程,其中target为线程指向的函数,args为函数参数
  • 创建完线程后通过调用它们的start方法让它们开始运行
  • join起到了线程同步的作用,即主线程结束后会进入阻塞状态,等待其他的子线程结束后它才会终止
1
2
3
4
$ time python konachan_threading.py
real 0m32.543s
user 0m0.000s
sys 0m0.000s

类似threading的还有multiprocessing、concurrent.futures等模块,读者可自行去挖掘。

其他

加载内置模块

利用-m参数,我们可以直接加载Python的模块

1
2
3
4
5
6
7
8
9
10
# 搭建http服务器
$ python -m http.server
# 创建虚拟环境
$ python -m venv <name>
# 性能测试
$ python -m cProfile <file.py>
# 文档测试
$ python -m doctest <file.py>
# 查看JSON
$ cat <file.json> | python -m json.tool

编程技巧

  • 善用IDE或vscode编辑器的代码跳转功能(F12键或者Ctrl+鼠标左键),在使用一个复杂的函数前必须看其源码
  • 善用自省(尤其是dir和help函数),能使你不用去死记函数的功能(因为函数一般都会有完善的文档)
  • 推荐一个能自动补全代码的神器:ptpython
  • 面向google和stackoverflow编程,拒绝伸手党
  • 如果出现重复的代码,那么它肯定能被重构
致力于干货分享,您的支持是我最大的动力!