Chapter9 “Decorators and Closures“

函数装饰器 顾名思义就是装饰函数,装饰器需要用另外一个函数作为参数并且返回一个可调用类型:

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

等同于

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

target = decorate(target)

这个例子里面装饰器把函数替换成了另一个函数:

>>> def deco(func):
...     def inner():
...         print('running inner()')
...     return inner  
...
>>> @deco
... def target():  
...     print('running target()')
...
>>> target()  
running inner()
>>> target  
<function deco.<locals>.inner at 0x10063b598>

装饰器就是语法糖,本身没有任何功能的提升,不用装饰器也能实现所有装饰器实现了的功能。

装饰器的三大特点:

  • 装饰器是一个函数或者可被调用对象
  • 装饰器可以用另一个函数替换装饰的函数
  • 装饰器在模块加载时执行

最后一点比较重要,我们来检验一下:

registry = []  

def register(func):  
    print(f'running register({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()  

结果十分合理:

$ python3 registration.py
running register(<function f1 at 0x100631bf8>)
running register(<function f2 at 0x100631c80>)
running main()
registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
running f1()
running f2()
running f3()

哪怕是从别的地方 import 这个模块:

>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)

实际写代码的时候,装饰器的应用跟上面那个例子有些出入:

  • 装饰器一般定义在一个模块,然后应用在另外一个模块
  • 装饰器大部分在内部会定义一个函数然后返回(闭包)

讨论闭包前必须先说一下变量的范围规则,函数有局部变量,如果在函数内部没有定义的变量,会把它当成全局变量看待:

>>> 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: global name 'b' is not defined

再看一个好玩的例子:

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

这里因为编译 f2 这个函数的时候,看到了 b 在函数内被赋值了,所以将b看待成一个局部变量,然后执行到print(b), 这一步,发现 局部变量 b 的范围不包括这里,所以报错。

修改成这样,b就是全局变量了

>>> b = 6
>>> def f3(a):
...     global b
...     print(a)
...     print(b)
...     b = 9
...
>>> f3(3)
3
6
>>> b
9

闭包

函数里面套着一个函数

现在这里有一个计算平均值的类:

class Averager():

    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total / len(self.series)
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

每次调用都会计算之前调用过的所有数的平均值,这里面因为 这个类 里 有保存一个列表,记录着所有数据

这里再仿照这个类的行为用函数来实现

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

这个函数保存数据的位置在这里:

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

再看一个说明如何使用 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):
  ...
UnboundLocalError: local variable 'count' referenced before assignment
>>>

这里因为 count 或者其他的不可变类型 再 averager 函数下面被赋值,所以编译的时候会将其认定成这个函数的局部变量,但是在外层也已经有了 count 变量,这时候需要 nonlocal 关键词 让其变成一个 free variable,之前的 list 的例子没有问题因为 list 的那些操作只是引用,没有对其赋值

改完后:

def make_averager():
    count = 0
    total = 0

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

    return averager

变量查找的逻辑:

  • 如果是 global x 这样声明的,那么 x 就是这个模块的全局变量
  • 如果是 nonlocal x 这样声明的,那么x属于这个函数外层第一次定义的方法
  • 如果x是函数的参数,然后被赋值了,那么x 是这个函数的局部变量
  • 如果 x 只是引用,没有赋值,也不是参数 ,那么 会根据 nonlocal 的规则查找 x,然后再根据 global 的规则去 找,最后会去 __buitins__.__dict__找

这里来实现一个简单的装饰器:

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(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
        return result
    return clocked  
import time
from clockdeco0 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.12363791s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000095s] factorial(1) -> 1
[0.00002408s] factorial(2) -> 2
[0.00003934s] factorial(3) -> 6
[0.00005221s] factorial(4) -> 24
[0.00006390s] factorial(5) -> 120
[0.00008297s] factorial(6) -> 720
6! = 720

这时候,factorial 的 __name__属性就会变成:

>>> import clockdeco_demo
>>> clockdeco_demo.factorial.__name__
'clocked'
>>>

然而,我们并不想这样,因为如果不同函数被同一个装饰器装饰,那么它们的 __name__, __doc__ 属性都会被 装饰器覆盖,这时候需要 用 functools.wraps 这个 装饰器去把原函数的属性复制过来。

import time
import functools


def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = [repr(arg) for arg in args]
        arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
        arg_str = ', '.join(arg_lst)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
        return result
    return clocked

还有一些比较典型的装饰器:

第一个是 functools.cache,这个是对内存优化处理的,在计算被装饰函数用同样的参数时会保留结果:

from clockdeco import clock


@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == '__main__':
    print(fibonacci(6))
$ python3 fibo_demo.py
[0.00000042s] fibonacci(0) -> 0
[0.00000049s] fibonacci(1) -> 1
[0.00006115s] fibonacci(2) -> 1
[0.00000031s] fibonacci(1) -> 1
[0.00000035s] fibonacci(0) -> 0
[0.00000030s] fibonacci(1) -> 1
[0.00001084s] fibonacci(2) -> 1
[0.00002074s] fibonacci(3) -> 2
[0.00009189s] fibonacci(4) -> 3
[0.00000029s] fibonacci(1) -> 1
[0.00000027s] fibonacci(0) -> 0
[0.00000029s] fibonacci(1) -> 1
[0.00000959s] fibonacci(2) -> 1
[0.00001905s] fibonacci(3) -> 2
[0.00000026s] fibonacci(0) -> 0
[0.00000029s] fibonacci(1) -> 1
[0.00000997s] fibonacci(2) -> 1
[0.00000028s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00000031s] fibonacci(1) -> 1
[0.00001019s] fibonacci(2) -> 1
[0.00001967s] fibonacci(3) -> 2
[0.00003876s] fibonacci(4) -> 3
[0.00006670s] fibonacci(5) -> 5
[0.00016852s] fibonacci(6) -> 8
8

可以看到 fibonacci(0) 啥的被调用了很多次,如果加了这个装饰器,看效果:

import functools

from clockdeco import clock


@functools.cache  
@clock  
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == '__main__':
    print(fibonacci(6))
$ python3 fibo_demo_lru.py
[0.00000043s] fibonacci(0) -> 0
[0.00000054s] fibonacci(1) -> 1
[0.00006179s] fibonacci(2) -> 1
[0.00000070s] fibonacci(3) -> 2
[0.00007366s] fibonacci(4) -> 3
[0.00000057s] fibonacci(5) -> 5
[0.00008479s] fibonacci(6) -> 8
8

嵌套装饰器是从上到下对应从左到右的顺序:

@alpha
@beta
def my_fn():
    ...

等于

my_fn = alpha(beta(my_fn))

functools.lru_cache 说明了装饰器也可以添加参数:

@lru_cache(maxsize=2**20, typed=True)
def costly_function(a, b):
    ...

functools.singledispatch 这个装饰器可以处理同一个函数的不同类型参数:

from functools import singledispatch
from collections import abc
import fractions
import decimal
import html
import numbers

@singledispatch  
def htmlize(obj: object) -> str:
    content = html.escape(repr(obj))
    return f'<pre>{content}</pre>'

@htmlize.register  
def _(text: str) -> str:  
    content = html.escape(text).replace('\n', '<br/>\n')
    return f'<p>{content}</p>'

@htmlize.register  
def _(seq: abc.Sequence) -> str:
    inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n</ul>'

@htmlize.register  
def _(n: numbers.Integral) -> str:
    return f'<pre>{n} (0x{n:x})</pre>'

@htmlize.register  
def _(n: bool) -> str:
    return f'<pre>{n}</pre>'

@htmlize.register(fractions.Fraction)  
def _(x) -> str:
    frac = fractions.Fraction(x)
    return f'<pre>{frac.numerator}/{frac.denominator}</pre>'

@htmlize.register(decimal.Decimal)  
@htmlize.register(float)
def _(x) -> str:
    frac = fractions.Fraction(x).limit_denominator()
    return f'<pre>{x} ({frac.numerator}/{frac.denominator})</pre>'

带参数的装饰器怎么写:

先来个不带参数的:

registry = []

def register(func):
    print(f'running register({func})')
    registry.append(func)
    return func

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

print('running main()')
print('registry ->', registry)
f1()

这个是带参数的版本:

registry = set()  

def register(active=True):  
    def decorate(func):  
        print('running register'
              f'(active={active})->decorate({func})')
        if active:   
            registry.add(func)
        else:
            registry.discard(func)  

        return func  
    return decorate  

@register(active=False)  
def f1():
    print('running f1()')

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

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

其实就是在原来的装饰器函数外层再套一个函数,这个函数返回的是原装饰器函数,然后这个函数的参数就是装饰器的参数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Python decorators是一种用于修改或增强函数功能的语法结构。它们允许开发者在不修改原始函数代码的情况下,通过在函数定义之前使用特殊符号(@)和装饰器函数来对函数进行包装或修饰。装饰器函数接受被装饰函数作为参数,并可以在原始函数执行之前或之后添加额外的逻辑或功能。这种技术可以用来实现缓存、日志记录、身份验证等功能。 Python decorators的使用方法可以根据具体需求进行定义和实现。常见的方法包括使用装饰器函数、使用类作为装饰器、使用带参数的装饰器等。装饰器函数是最常见的一种方式,它接受一个函数作为参数并返回一个新的函数,新函数会替换原始函数。这样,在调用原始函数时,实际上是调用了被装饰的函数,从而在不修改原始函数的情况下添加了额外的功能。 除了使用Python内置的装饰器语法,还可以使用第三方库来简化装饰器的编写和使用。例如,可以使用decorator模块来定义和使用装饰器。这个模块提供了一种更简洁的语法,可以直接在函数定义之前使用@decorator语法来应用装饰器。该模块的使用方法如下所示: ```python from decorator import decorator @decorator def hint(func, *args, **kwargs): print('{} is running'.format(func.__name__)) return func(*args, **kwargs) ``` 上述代码定义了一个名为hint的装饰器函数,它接受一个函数作为参数,并在函数执行之前打印出函数名。然后,通过在函数定义之前使用@hint语法,将装饰器应用到目标函数上。这样,在调用目标函数时,实际上会先执行装饰器函数内部的逻辑,然后再执行目标函数本身的逻辑。 总结来说,Python decorators是一种用于修饰函数的语法结构,可以通过装饰器函数在不修改原始函数代码的情况下增强函数功能。它可以通过Python内置的装饰器语法或第三方库来实现。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Python Decorator](https://blog.csdn.net/weixin_30951231/article/details/96490117)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [Python系列之装饰器(decorator)](https://blog.csdn.net/ikxyang/article/details/121995824)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值