《Fluent Python》读书笔记-Function Decorators and Closures

概览

    本章主要介绍装饰器和闭包(closure)。函数装饰器允许我们在源码上对函数进行标记,以增强函数的功能。要想能充分掌握装饰器,必须先理解闭包。

Decorators 101

    装饰器是一种把另外一个函数作为参数的可调用对象。装饰器可能在被装饰的函数上做一些处理,然后返回这个函数或者其他的函数或可调用对象。
    装饰器的效果可以用下面这个例子来展示:

@decorate
def target():
    print('running target()')

    等价于:

def target():
    print('running target()')
target = decorate(target)

    严格来讲装饰器只是一种语法糖,你可以像常规的调用一个可调用对象一样去使用装饰器,把另外一个函数作为参数。
    总结一下装饰器有两个很重要的特性:

  • 能够把被装饰的函数替换为另一个
  • 装饰器是在模块加载的时候就立即执行的

When Python Executes Decorators

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()

main()
running register(<function f1 at 0x101722510>)
running register(<function f2 at 0x101722598>)
running main()
registry -> [<function f1 at 0x101722510>, <function f2 at 0x101722598>]
running f1()
running f2()
running f3()

    从上面的例子可以看到装饰器是在模块加载的时候就运行。

Variable Scope Rules

>>> def f1(a):
...     print(a)
...     print(b)
... 
>>> f1(3)
3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f1
NameError: name 'b' is not defined
>>> b = 6
>>> f1(3)
3
6
>>> b=6
>>> def f2(a):
...     print(a)
...     print(b)
...     b = 9
... 
>>> f2(3)
3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment
>>> def f3(a):
...     global b
...     print(a)
...     print(b)
...     b = 9
... 
>>> f3(3)
3
6
>>> b
9

    这是python的一个设计抉择:python不需要声明变量,但是python认为在一个函数里赋值的变量是一个局部变量。

Closures

    闭包是一个包含扩展了可见范围的非全局变量的函数,这些变量在函数中引用,但是不在函数中定义。闭包的关键在于是否能访问定义在函数体外的非全局变量。

>>> def make_averager():
...     series = []
... 
...     def averager(new_value):
...         series.append(new_value)
...         total = sum(series)
...         return total/len(series)
... 
...     return averager
... 
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

    可以看到seriesmake_averager中的局部变量。当调用avg(10)的时候,make_averager已经返回,并且他的局部可见范围已经消失。对于averagerseries是一个自由变量(free variable,没有绑定到局部可见范围的变量)。

>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x1016d7c18: list object at 0x1016fd208>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

    前面的series是一个可变对象,如果是一个不可变对象,进行赋值操作会使得变量变成一个局部变量,就会出错。python3提供了一个nonlocal关键字,可以把一个变量变为一个自由变量。

>>> def make_averager():
...     count = 0
...     total = 0
... 
...     def averager(new_value):
...         count += 1
...         total += new_value
...         return total / count
... 
...     return averager
... 
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in averager
UnboundLocalError: local variable 'count' referenced before assignment
>>> def make_averager():
...     count = 0
...     total = 0
... 
...     def averager(new_value):
...         nonlocal count, total
...         count += 1
...         total += new_value
...         return total / count
... 
...     return averager
... 
>>> avg = make_averager()
>>> avg(10)
10.0

简单装饰器

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


@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))
    print(factorial.__name__)

**************************************** Calling snooze(.123)
[0.12481301s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000064s] factorial(1) -> 1
[0.00001838s] factorial(2) -> 2
[0.00003116s] factorial(3) -> 6
[0.00004362s] factorial(4) -> 24
[0.00005653s] factorial(5) -> 120
[0.00007111s] factorial(6) -> 720
6! = 720
clocked

    这个是装饰器的一个典型的模式,用一个新的函数替换被装饰的函数,这个函数和被装饰的函数的参数相同,并且返回的内容也相同。从最后的打印可以看到factorial实际上持有的是到clocked函数的引用。

标准库里的装饰器

  • functools.wraps:把被装饰函数的一些属性拷贝到装饰器函数,如name
  • functools.lru_cache:可以对函数调用的结果进行缓存
  • functools.singledispatch:将一个函数变为泛型函数,可以根据第一个参数的不同类型去做不同的操作

带参数的装饰器

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


if __name__ == '__main__':
    @clock()
    def snooze(seconds):
        time.sleep(seconds)

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

    print(clock)
    print(clock())
    print(clock()(snooze))

[0.12632895s] snooze(0.123) -> None
[0.12463617s] snooze(0.123) -> None
[0.12320995s] snooze(0.123) -> None
<function clock at 0x107442268>
<function clock.<locals>.decorate at 0x1075b6c80>
<function clock.<locals>.decorate.<locals>.clocked at 0x1075f5268>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容

  • 函数和模块使得 重用代码 成为可能重用代码 使代码能够简短且易读 函数 给一段实现了某个功能的代码块起一个名字,在...
    全新的饭阅读 282评论 0 0
  • 今日体验:今天一个宝马5系需要查下支臂多少钱,因为用的多就没有先查询价格,直接给报的价格,结果这个宝马比较少见是四...
    谢波1阅读 95评论 0 0
  • Linux命令大全(手册)_Linux常用命令行实例详解_Linux命令学习手册 http://man.linux...
    pcliuyang阅读 267评论 0 3