什么是装饰器

在《Fluent Python》里说, A decorator is a callable that takes another function as argument。也就是装饰器就是一个可执行对象,它的参数是一个函数。

1
2
3
@decorate
def target():
print('running target()')

相当于

1
2
3
def target():
print('running target()')
target = decorate(target)

另一个重要的问题是当模块导入时,装饰器就执行了。

编写registration.py

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
registry = []


def register(func):
print('running register(%s)' % func)
registry.append(func)
return func


@register
def f1():
print('running f1()')


@register
def f2():
print('running f2()')


def f3():
print('running f3()')


def main():
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()

if __name__ == '__main__':
main()

之后导入模块,可以看到registration.registry的值

1
2
3
4
5
>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)
>>> registration.registry
[<function f1 at 0x10063b1e0>, <function f2 at 0x10063b268>]

编写一个装饰器

下面编写一个记录函数执行时间的装饰器,保存到clockdeco.py中

1
2
3
4
5
6
7
8
9
10
11
import time
def clock(func):
def clocked(*args): #
t0 = time.perf_counter()
result = func(*args) #
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked

之后使用这个装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import time
from clockdeco import clock
@clock
def snooze(seconds):
time.sleep(seconds)

@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)

if __name__=='__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))

可以看到结果

1
2
3
4
5
6
7
**************************************** Calling snooze(123) [0.12405610s] snooze(.123) -> None **************************************** Calling factorial(6) [0.00000191s] factorial(1) -> 1
[0.00004911s] factorial(2) -> 2
[0.00008488s] factorial(3) -> 6
[0.00013208s] factorial(4) -> 24
[0.00019193s] factorial(5) -> 120
[0.00026107s] factorial(6) -> 720
6!=720

使用内置装饰器

上面编写的clock装饰器存在一个问题是不支持关键字参数,可以使用标准库里的functools.wraps来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(', '.join(repr(arg) for arg in args))
if kwargs:
pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
arg_lst.append(', '.join(pairs))
arg_str = ', '.join(arg_lst)
print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
return result
return clocked

Python内置了三个常用的装饰器,property,classmethod和staticmethod,
标准库functools里还有两个有趣的装饰器lru_cache和singledispatch。

编写带参数的装饰器

要编写带参数的装饰器,一个解决的办法是编写一个装饰器工厂,然后根据参数不同,返回不同的装饰器。

拿上面的clock装饰器来说,如果要允许传入时间格式来输出不同的日期格式,可以如下编写装饰器工厂, 保存到clockdeco_param.py中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT):
def decorate(func):
def clocked(*_args):
t0 = time.time()
_result = func(*_args)
elapsed = time.time() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args)
result = repr(_result)
print(fmt.format(**locals()))
return _result
return clocked
return decorate

编写如下测试代码,可以看到不同的时间格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time
from clockdeco_param import clock
@clock()
def snooze(seconds):
time.sleep(seconds)

for i in range(3):
snooze(.123)


@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
time.sleep(seconds)


for i in range(3):
snooze(.123)

输出结果如下

1
2
3
4
5
6
[0.12581110s] snooze(0.123) -> None
[0.12463617s] snooze(0.123) -> None
[0.12825012s] snooze(0.123) -> None
snooze(0.123) dt=0.127s
snooze(0.123) dt=0.126s
snooze(0.123) dt=0.126s

《Fluent Python》里说,简单来说,闭包就是一个包含非全局变量且该变量不是在它自己函数体里声明的函数。它不关心函数是否匿名,它关心的是可以访问不在自己函数体里定义的非全局变量。

考虑一个avg平均值函数,如下

1
2
3
4
5
6
>>> avg(10) 
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

一个使用类的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Averager():
"""
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
"""

def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total/len(self.series)

一个函数的实现方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def make_averager():
"""
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
"""

series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager

这里需要注意的是series相对于averager是一个自由变量,也就是这个变量不属于局部变量。

1
2
>>> avg.__code__.co_varnames ('new_value', 'total')
>>> avg.__code__.co_freevars ('series',)

而每一个avg.__code__.co_freevars里的变量的值都保存在avg.__closure__中,其值在avg.__closure__[0].cell_contents

1
2
3
4
5
6
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

所以,闭包就是一个可以从自由变量里获取变量的函数。

下面看看为什么会引入nonlocal这个关键字,以及它的作用。

如果我们想办法消除series变量,可以编写如下函数。

1
2
3
4
5
6
7
8
9
def make_averager():

count = 0
total = 0
def averager(new_value):
count += 1
total += new_value
return total / count
return averager

但函数执行时会报错

1
2
3
4
5
6
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
>>>

这是因为变量作用域的原因。count += 1相当于count = count + 1, 此时count会被当做局部变量,而不是引用外面一层的count, 而averager函数里并没有声明count变量,于是报错。可以参看Python之dis模块

在Python3里添加了nonlocal来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def make_averager():
"""
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
"""

count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager

dis, 即Disassembler for Python bytecode, 用于Python字节码的反汇编。可以用于查看函数的具体执行步骤,例如下面的局部变量的例子

1
2
3
def f1(a):
print(a)
print(b)

得到

1
2
3
4
5
6
7
8
9
10
11
3           0 LOAD_GLOBAL              0 (print)
3 LOAD_FAST 0 (a)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP

4 10 LOAD_GLOBAL 0 (print)
13 LOAD_GLOBAL 1 (b)
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 POP_TOP
20 LOAD_CONST 0 (None)
23 RETURN_VALUE

执行

1
2
3
a = 3
b = 4
f1(a)

不会报错,这里第13条是LOAD_GLOBAL,此时有全局变量b.

再看这个例子

1
2
3
4
def f2(a):
print(a)
print(b)
b = 9

得到的是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
4           0 LOAD_GLOBAL              0 (print)
3 LOAD_FAST 0 (a)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP

5 10 LOAD_GLOBAL 0 (print)
13 LOAD_FAST 1 (b)
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 POP_TOP

6 20 LOAD_CONST 1 (9)
23 STORE_FAST 1 (b)
26 LOAD_CONST 0 (None)
29 RETURN_VALUE

执行

1
2
3
a = 3
b = 4
f2(a)

会报找不到局部变量b, 这是因为第13条LOAD_FAST是加载局部变量,此时还没有局部变量b.

《Fluent Python》中写到,在使用Python字典时,有两种方式,d[key]与d.get(key),它们有一些细微的区别

  • d[key]底层实现是调用dict.__getitem__, 而d.get(key)就是一个函数调用。

  • dict.__getitem__ 没有找到key时,会调用dict.__missing__

执行如下代码

1
2
3
4
from dis import dis
d = dict()
print(dis('d["a"]'))
print(dis('d.get("a")'))

输出如下结果

1
2
3
4
5
6
7
8
9
10
 1           0 LOAD_NAME                0 (d)
3 LOAD_CONST 0 ('a')
6 BINARY_SUBSCR
7 RETURN_VALUE
None
1 0 LOAD_NAME 0 (d)
3 LOAD_ATTR 1 (get)
6 LOAD_CONST 0 ('a')
9 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
12 RETURN_VALUE

可以看到get是函数调用,而d[]不是。

编写测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Dict(dict):
def __getitem__(self, key):
return 2

class TestMiss(dict):
def __missing__(self, key):
return 3

if __name__ == "__main__":
d = Dict()
d["key"] = "test"
print(d.get("key"), d["key"], d.get("k"), d["k"])

d = TestMiss()
d["key"] = "test"
print(d.get("key"), d["key"], d.get("k"), d["k"])

输出结果如下

1
2
test 2 None 2
test test None 3

ici,基于python的终端查词小工具看到终端查词小工具,遗憾的是不能查中文,而这是我经常用的。尝试在ici上添加中文,没有搞定,于是决定自己写一个。因为经常使用有道词典,于是有了ydd.

真正写起来还是比较简单,就是一个HTTP请求,之后读取JSON里的数据,显示出来。使用了clickrequests两个库后,很快就写好了。click提供命令行参数解析以及终端颜色显示,requests用来发起请求非常方便。考虑到是小工具,所以异常情况都没有处理,代码总共才70行。后来发现不能兼容2.7,于是加上six来判断Python版本。

之后是使用setuptools发布到pypi上,很开心。目前使用上来说,查英文单词还是ici好,因为有例句,不过查中文当然是使用ydd好了。

好好的Chrome,在上传文件时,换回结果是text/plain类型时,突然报这个错,VM7369:1 Uncaught SyntaxError: Unexpected token ( in JSON at position 0,而其它浏览器就没有这个问题。

刚开始以为是版本太新的原因,于是改用低版本,发现问题还是存在,看同事的Mac,不存在这个问题。

最后在前端同事的帮助下,知道Chrome的隐身模式,在Chrome隐身模式下没有这个问题,于是猜测是插件问题。最终锁定是
LastPass: Free Password Manager这个插件的原因。禁用后就没有这个问题。

coverage用来统计代码测试覆盖率,非常方便。

安装

执行pip install coverage即可

指定代码路径

希望coverage只去统计我们关心的代码,此时–source选项派上用场。例如coverage --source .只统计当前目录下的所有代码。

coveragerc配置

通过使用coverage配置文件,可以很方便的控制coverage。coverage默认使用.coveragerc里的配置,也可以通过–rcfile来配置。

统计数据输出

执行完coverage测试后,可以执行coverage report和coverage html输出统计信息。

完整的Django测试执行命令可以这样coverage run --rcfile=.coveragerc --source . ./manage.py test

为了保证团队成员提交的代码是符合规范的,可以使用pre-commit来做代码检查。

安装

pre-commit安装很方便,执行`pip install pre-commit’即可。

添加到git hooks

执行pre-commit install, 将pre-commit添加到git hooks中

配置

在项目根目录下,添加.pre-commit-config.yaml文件即可进行配置,如下就是一个配置。

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
-   repo: https://github.com/pre-commit/pre-commit-hooks
sha: v0.7.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: autopep8-wrapper
- id: check-docstring-first
- id: check-json
- id: check-added-large-files
- id: check-yaml
- id: debug-statements

- id: requirements-txt-fixer
- repo: https://github.com/pre-commit/pre-commit
sha: v0.11.0
hooks:
- id: validate_config
- id: validate_manifest
- repo: local
hooks:
- id: pylint
name: pylint
entry: pylint
language: system
files: \.py$
exclude: test_gevent.py
args: [--rcfile=.pylintrc, --load-plugins=pylint_django]

此后,每次提交代码时,都会进行代码规范检查。

pylint用于Python代码规范检查。默认代码风格遵循PEP08

使用配置文件

配置文件可以通过如下命令生成

1
pylint --generate-rcfile > .pylintrc

执行pylint时,可以通过指定–rcfile参数来加载配置文件。而默认配置文件加载顺序可以参考命令行参数这节。

Django代码检查

对于Django, 有pylint-django这个pylint插件用来代码检查。pip install pylint-djangop安装后,添加–load-plugins参数即可启用,如pylint --load-plugins pylint_django

警告忽略

有时pylint的检查不满足需求,太繁琐,此时可以忽略它。如在for d in data:里,会报Invalid variable错误,即C0103, 此时加上# pylint: disable=C0103可以忽略这个警告。

在编写测试时遇到表单上传文件的问题,问了同事后,给了stackoverflow上how to unit test file upload in django链接, 在django.test.Client.post里看到如下例子

1
2
3
>>> c = Client()
>>> with open('wishlist.doc') as fp:
... c.post('/customers/wishes/', {'name': 'fred', 'attachment': fp})

对于图片,需要加上rb模式,例子如下

1
2
3
>>> c = Client()
>>> with open('wishlist.png', 'rb') as fp:
... c.post('/customers/wishes/', {'name': 'fred', 'attachment': fp})

解决问题。