《流畅的Python》4-函数装饰器和闭包详解


我另一篇博客里稍微有讲到一点,说的不是很好。

首先学习前要了解 Python 里函数是一等对象。
函数装饰器是用来增强函数的行为,而想实现函数装饰器就要了解闭包,这两者不相等。
nonlocal 是 Python3 的保留关键字,用于实现闭包。

要思考以下内容:

  • Python 装饰器基本语法
  • Python 作为动态语言,怎么判断变量作用域?
  • 为什么要用到闭包?闭包的工作原理?
  • nonlocal 怎么去解决闭包的?

然后再进一步探讨装饰器:

  • 实现一个良好的装饰器
  • 使用标准库中的装饰器
  • 实现参数化装饰器

基础知识

装饰器是可调用的对象,其参数是另一个函数,返回值也是一个函数,作用是去“修饰”一个函数,从而实现更多的功能,(比如C++里的重载)。被装饰的函数会被替换。

一个简单的装饰器:

def decorate(func):
    print('use decorate')
  return func

使用装饰器

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

[output]:use decorate

上述写法和下面的一样

def target():
  print('use target')

target=decorate(target)

这里装饰器的作用仅仅是改为输出 use decorate ,然后返回原来的func,所以后面再执行target()不变,实际上装饰器可能会修改函数。

Python 何时执行装饰器

装饰器的关键特性是,被装饰的函数定义后立即运行,因此常发生在导入时,(装饰器定义和使用往往不在同一个文件)。

用装饰器改进“策略”模式

“策略”模式在设计模式中提到,这里省略了。
感兴趣可以给出一个实例,可以跳到下一节 [变量作用域规则](# 变量作用域规则)。

计算 order 的最优计算方案,三个被装饰的函数是不同的计算方案,关注一下函数作为参数和列表元素。

promos = []


def promotion(promo_func):
    promos.append(promo_func)
    return promo_func


@promotion
def fidelity(order):
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0


@promotion
def bulk_item(order):
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount


@promotion
def large_order(order):
    distinct_item = {item.product for item in order.cart}
    if len(distinct_item) >= 10:
        return order.total() * .07
    return 0


def best_promo(order):
    return max(promo(order) for promo in promos)

变量作用域规则

a=3
def f1():
    print(a)
    print(b)

f1()

3
Traceback (most recent call last):
  File "/home/jo/桌面/1.py", line 58, in <module>
    f1()
  File "/home/jo/桌面/1.py", line 56, in f1
    print(b)
NameError: name 'b' is not defined
a=3
def f1():
    print(a)
    print(b)

f1()

3
Traceback (most recent call last):
  File "/home/jo/桌面/1.py", line 60, in <module>
    f1()
  File "/home/jo/桌面/1.py", line 57, in f1
    print(b)
UnboundLocalError: local variable 'b' referenced before assignment

首先 Python 在编译定义体时已经知道b是局部变量,因此执行到print(b)时首先发现局部变量不在这条语句前。

在 f1()内 print(b) 前添加 global 即可定义为全局变量

闭包

了解变量作用域后再讨论闭包。
要区分闭包和匿名函数。设计嵌套函数才会涉及闭包问题。而函数内声明函数在匿名函数中比较常见,因此容易混淆两者概念。

看一个计算平均值的高阶函数:

def make_average():
    series=[]

    def average(new_value):
        series.append(new_value)
        total=sum(series)
        return total/len(series)

    return average

调用函数:

>>>avg=make_average()
>>>avg(10)
10.0
>>avg(11)
10.5

和类的使用类似,因为函数也是一等对象。
seriesmake_average()的局部变量,但该函数返回值是average(),调用avg时已经被释放了,不存在本地作用域。

average()中,series又是自由变量(free variable),指未在本地作用域中绑定的变量
serices绑定在avg的closure属性中,该属性对应到 avg.__code__.co_freevar中的一个名称。

>>> avg.__closure__
(<cell at 0x7f85f718c738: list object at 0x7f85f71737c8>,)
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__[0].cell_contents
[10, 11]

因此,可以认定,闭包是一种函数,保留了定义函数时存在的自由变量的绑定,从而让定义作用域虽然不存在了,但是存在绑定可以继续使用自由变量的情况。

nonlocal 声明

闭包中常见的错误:

def make_average():
    count = 0
    total = 0

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

count += 1 等价于 count=count+1
这里会产生一个新的局部变量,产生错误。
解决方法是count += 1前添加 nonlocal count,total

实现一个简单的装饰器

装饰器clock 计算函数运行的时间:

import time
def clock(func):
        def clocked(*args): #接收任意定位参数
                t0=time.perf_count
                result=func(*args)
                elapsed=time.perf_count-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 装饰器

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

输出结果:

$ python3 clockdeco_demo.py
**************************************** 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

传递参数的工作原理:

@decorate
def target():
        print('running target()')
等同于

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

因此,factorial(6) 函数是一级级传下去的。

标准库中的装饰器

functools.lru_cache 做备忘

是一项优化技术,把耗时的函数结果保存起来,实现动态优化。 使用时在定义体前@functools.lru_cache()即可。
括号内为配置选项,@functools.lru_cache(maxsize=128,typed=False)

单分派泛函数

这个名词看上去很高大上,分为分派函数和泛函数理解就好了。分派函数就是一个函数内用一串if/elif/else调用专门的函数来处理问题。单分派就是只根据一个参数来选择,多分派是根据多个参数选择。

@functools.singledispatch 将整体方案划分为多个模块,被修饰的函数将变成泛函数(generic function):根据第一个参数的类型,以不同的方式执行相同操作的一组函数。

其实也就是重载啦....
网上扒了一个样例下来

from functools import singledispatch

@singledispatch
def show(obj):
    print (obj, type(obj), "obj")

@show.register(str)
def _(text):
    print (text, type(text), "str")

@show.register(int)
def _(n):
    print (n, type(n), "int")
show(1)
show("xx")
show([1])

输出:

1 <class 'int'> int
xx <class 'str'> str
[1] <class 'list'> obj

register的用法来自官方文档:

To add overloaded implementations to the function, use the register() attributeof the generic function.
It is a decorator, taking a type parameter and decorating a function implementing the operation for that type:

@functools.singledispatch 可以讲的东西蛮多的,这里可以看到

  • 每个专门函数名字无关紧要,_即可。
  • 每个专门函数用 @<<base_function>>.register(<<type>>)装饰。

这个机制明显支持模块化拓展,这是一个优点,任意一个模块下都可以注册专门函数。

叠放装饰器

字面意思,可以叠加装饰器

@a
@b
def f()
等价于
def f()
        pass
f=a(b(f))

参数化装饰器

解析源码中的装饰器时,Python把装饰的函数作为第一个参数传给装饰器函数。
为了接收其他参数,需要创建一个装饰器工厂函数,把参数传给他,返回一个装饰器,然后再应用到要装饰的函数上。

简单地说,就是在真正的装饰器外面再嵌套函数,这个函数能根据参数来选择不同的装饰器,并返回。

def deco(arg):
    def _deco(func):
        def __deco():
            print("before %s called [%s]." % (func.__name__, arg))
            func()
            print("  after %s called [%s]." % (func.__name__, arg))
        return __deco
    return _deco

@deco("mymodule")
def myfunc():
    print(" myfunc() called.")

@deco("module2")
def myfunc2():
    print(" myfunc2() called.")

注意:
这里处理的是参数化装饰器,而不是用装饰器装饰带参数的函数,带参数的函数考虑参数传递,参数化装饰器是在装饰器外多一次嵌套。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值