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

文章目录

  • 基础知识
    • 定义
    • 执行时机
  • 使用
    • 返回被装饰函数
        • 注册器
        • 促销
    • 返回新定义函数
      • 闭包
      • nonlocal声明
      • 示例
        • 函数计时器
  • 参数化装饰器
      • 示例
        • 授权
        • 日志
        • 改进的函数计时器
      • Python标准库的装饰器
        • functools.lru_cache
        • single dispatch generic function
  • References

基础知识

定义

装饰器(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,因此在引入nonlocal的PEP3104中的第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 '
{}
'
.format(content) @htmlize.register(str) # <2> def _(text): # <3> content = html.escape(text).replace('\n', '
\n'
) return '

{0}

'
.format(content) @htmlize.register(numbers.Integral) # <4> def _(n): return '
{0} (0x{0:x})
'
.format(n) @htmlize.register(tuple) # <5> @htmlize.register(abc.MutableSequence) def _(seq): inner = '\n
  • '.join(htmlize(item) for item in seq) return '
      \n
    • ' + inner + '
    • \n
    '
    # END HTMLIZE r""" htmlize(): generic function example # BEGIN HTMLIZE_DEMO >>> htmlize({1, 2, 3}) # <1> '
    {1, 2, 3}
    ' >>> htmlize(abs) '
    <built-in function abs>
    ' >>> htmlize('Heimlich & Co.\n- a game') # <2> '

    Heimlich & Co.
    \n- a game

    ' >>> htmlize(42) # <3> '
    42 (0x2a)
    ' >>> print(htmlize(['alpha', 66, {3, 2, 1}])) # <4>
    • alpha

    • 66 (0x42)
    • {1, 2, 3}
    # 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 函数装饰器 | 菜鸟教程

    你可能感兴趣的:(python,学习,笔记)