【Python学习笔记】装饰器与闭包

本文详细介绍了Python装饰器的基础知识,包括定义、执行时机。装饰器在被装饰的函数定义后立即运行,常用于注册、授权、日志等功能。文章通过函数计时器、授权和日志记录的示例解释装饰器的使用,还探讨了闭包的概念和nonlocal声明。同时,介绍了Python标准库中的`functools.lru_cache`和`single dispatch generic function`装饰器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基础知识

定义

装饰器(Decorator)是参数为被装饰函数(function as parameter)的可调用对象(callable object)。

class C:
    def meth (cls):
        ...
    meth = classmethod(meth)   # Rebind name to wrapped-up class method

通过引入装饰器将上述代码包装为以下形式的等效代码:

class C:
    @classmethod
    def meth (cls):
        ...

严格来说,装饰器只是简化编程的语法糖。元编程(运行时改变程序的行为)中常用。

执行时机

装饰器的关键特性之一是其在被装饰的函数定义之后立即运行,即导入时(即Python加载模块时)执行。

# BEGIN REGISTRATION

registry = []  # <1>

def register(func):  # <2>
    print('running register(%s)' % func)  # <3>
    registry.append(func)  # <4>
    return func  # <5>

@register  # <6>
def f1():
    print('running f1()')

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

def f3():  # <7>
    print('running f3()')

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

if __name__=='__main__':
    main()  # <9>

# END REGISTRATION

借助装饰器实现注册功能:

$ python registration.py
running register(<function f1 at 0x000001F67FF974C0>)
running register(<function f2 at 0x000001F67FF97C40>)
running main()
registry -> [<function f1 at 0x000001F67FF974C0>, <function f2 at 0x000001F67FF97C40>]
running f1()
running f2()
running f3()

导入时执行:

>>> import registration
running register(<function f1 at 0x000002E8F38CA160>)
running register(<function f2 at 0x000002E8F38CA0C0>)

使用

装饰器函数与被装饰函数往往不在同一模块中定义。装饰器定义后应用到其他模块中的函数上。
从装饰器的行为上看,装饰器可能会处理被装饰的函数然后将其返回,或者将其替换为另一个函数或可调用对象。

  • 原封不动地返回被装饰函数
  • 在装饰器内部定义函数,并返回新定义的函数

返回被装饰函数

注册器

这类装饰器很重要的应用就是将函数注册到某一中心化数据结构中,如前文的REGISTRATION模块。

促销

利用装饰器将所有促销策略轻松地加入策略列表中,避免难以发现的代码遗忘引起的报错。

promos = []  # <1>

def promotion(promo_func):  # <2>
    promos.append(promo_func)
    return promo_func

@promotion  # <3>
def fidelity(order):
    """5% discount for customers with 1000 or more fidelity points"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0

@promotion
def bulk_item(order):
    """10% discount for each LineItem with 20 or more units"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount

@promotion
def large_order(order):
    """7% discount for orders with 10 or more distinct items"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0

def best_promo(order):  # <4>
    """Select best discount available
    """
    return max(promo(order) for promo in promos)

返回新定义函数

当装饰器返回新的内部定义的函数时,往往会形成高阶的嵌套函数,涉及闭包(closure)问题。

闭包

闭包指延伸了作用域的函数,其中包含在函数体外定义而在函数体内引用的非全局变量。只有在涉及嵌套函数时才会产生闭包问题。通过以下计算平均值的示例理解:

# class implementation
class Averager():

    def __init__(self):
        self.series = [] # save in class

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

等效于以闭包方式(嵌套函数)实现的高阶函数:

# high-level function implementation
def make_averager():
    series = [] # local variable in make_avg(), also free variable for average()

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

高阶函数实现的最关键问题在于:average()函数从哪里寻找存储历史值的series数组呢?
审查返回的对象一窥玄机:

>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__  # doctest: +ELLIPSIS
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

Python在__code__属性中保存局部变量和自由变量的名称,该属性表示编译后的函数定义体。
series绑定于avg的__closure__属性。avg.__closure__各元素对应于avg.__code__.co_freevars的一个名称。这些绑定于闭包属性的元素均为cell对象,有cell_contents属性,保存历史值。
综上,闭包是一种保存自由变量的绑定的函数,从而在调用时使用定义作用域之外的绑定自由变量。

nonlocal声明

出于空间复杂性的考虑,计算均值的函数不必存储所有历史值,只需保存元素个数和总和两个变量即可。如果我们以这种思路轻易地编写代码便会出现缺陷:

def make_average():
    count = 0
    total = 0
    
    def average(new_val):
        count += 1
        total += new_val
        return total / count
      
    return average
    
"""
>>> avg = make_average()
>>> avg(10)
Traceback (most recent call last):
    ...
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
"""

问题在于:counttotal变量是不可变类型(与字典、列表等可变类型不同),在average()内部赋值时会隐式创建局部变量副本,如此变量就不是自由变量,因而不能保存在闭包中。
为了解决这一问题,Python 3引入了nonlocal声明。其作用是将变量标记为自由变量,无论是否赋予变量新值,均不会改变自由变量的事实。如果为声明后的变量赋予新值,那么闭包中保存的绑定也会更新。

def make_average():
    count = 0
    total = 0
    
    def average(new_val):
        nonlocal count, total
        count += 1
        total += new_val
        return total / count
      
    return average
    
"""

>>> avg = make_average()
>>> avg(10)
10.0
"""

Python 2没有nonlocal,因此在引入nonlocalPEP3104中的第3个代码片段给出了一种方法。这种处理方式是将内部函数需要修改的变量存储为可变对象(如字典或简单的实例)的元素或属性,并且把该对象绑定给一自由变量。

示例

函数计时器

新定义的函数接受和被装饰函数相同的参数,在函数运行之外执行额外的动作,最后返回被装饰函数的执行结果。这是常用的装饰器编写策略。函数运行计时器就是这种编写方法的示例之一。

# clockdeco.py

import time


def clock(func):
    def clocked(*args):
        t0 = time.time()
        result = func(*args)
        elapsed = time.time() - 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

if __name__ == '__main__':

    @clock()
    def snooze(seconds):
        time.sleep(seconds)

    for i in range(3):
        snooze(.123)
        
"""
$ python clockdeco.py
[0.12865448s] snooze(0.123) -> None
[0.12400031s] snooze(0.123) -> None
[0.12472081s] snooze(0.123) -> None
"""

参数化装饰器

示例

授权

装饰器能有助于检查某个人是否被授权去使用一个web应用的端点(endpoint)。它们被大量使用于Flask和Django web框架中。这里是一个例子来使用基于装饰器的授权:

from functools import wraps
 
def requires_auth(f):
    @wraps(f)
    def decorated(*args, kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            authenticate()
        return f(*args, kwargs)
    return decorated
日志

日志是装饰器运用的另一个亮点。这是个例子:

from functools import wraps
 
def logit(func):
    @wraps(func)
    def with_logging(*args, kwargs):
        print(func.__name__ + " was called")
        return func(*args, kwargs)
    return with_logging
 
@logit
def addition_func(x):
   """Do some math."""
   return x + x
 
result = addition_func(4)
# Output: addition_func was called
改进的函数计时器
# clockdeco_param.py
# BEGIN CLOCKDECO_PARAM
import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

def clock(fmt=DEFAULT_FMT):  # <1>
    def decorate(func):      # <2>
        def clocked(*_args): # <3>
            t0 = time.time()
            _result = func(*_args)  # <4>
            elapsed = time.time() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)  # <5>
            result = repr(_result)  # <6>
            print(fmt.format(**locals()))  # <7>
            return _result  # <8>
        return clocked  # <9>
    return decorate  # <10>

if __name__ == '__main__':

    @clock()  # <11>
    def snooze(seconds):
        time.sleep(seconds)

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

# END CLOCKDECO_PARAM
"""
>>> snooze(.1)  # doctest: +ELLIPSIS
[0.101...s] snooze(0.1) -> None
>>> clock('{name}: {elapsed}')(time.sleep)(.2)  # doctest: +ELLIPSIS
sleep: 0.20...
>>> clock('{name}({args}) dt={elapsed:0.3f}s')(time.sleep)(.2)
sleep(0.2) dt=0.201s
"""

Python标准库的装饰器

functools.lru_cache
import functools

from clockdeco import clock

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

if __name__=='__main__':
    print(fibonacci(6))
single dispatch generic function
# BEGIN HTMLIZE

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

@singledispatch  # <1>
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

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

@htmlize.register(numbers.Integral)  # <4>
def _(n):
    return '<pre>{0} (0x{0:x})</pre>'.format(n)

@htmlize.register(tuple)  # <5>
@htmlize.register(abc.MutableSequence)
def _(seq):
    inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n</ul>'

# END HTMLIZE
r"""
htmlize(): generic function example

# BEGIN HTMLIZE_DEMO

>>> htmlize({1, 2, 3})  # <1>
'<pre>{1, 2, 3}</pre>'
>>> htmlize(abs)
'<pre>&lt;built-in function abs&gt;</pre>'
>>> htmlize('Heimlich & Co.\n- a game')  # <2>
'<p>Heimlich &amp; Co.<br>\n- a game</p>'
>>> htmlize(42)  # <3>
'<pre>42 (0x2a)</pre>'
>>> print(htmlize(['alpha', 66, {3, 2, 1}]))  # <4>
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>

# END HTMLIZE_DEMO
"""

References

Fluent Python Chapter 7: Function Decorators and Enclosures(src: github/fluentpython/example-code/07-closure-deco)
Python Documentation: PEP318: Decorators for Functions and Methods & PEP3129: Class Decorators
Python Enhancement Proposals: PEP3104, PEP318 & PEP329
Python 函数装饰器 | 菜鸟教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值