闭包与装饰器#

1. 闭包#

闭包(closure)指内部函数对外部函数变量的引用。其局部优化了变量,原来需要类对象完成的工作,闭包也可完成。但此处也有一些陷阱:

  • 闭包引用了外部函数的局部变量,外部函数的局部变量没有及时释放,消耗内存

  • 闭包利用了解释器会跳过函数定义的部分,是一种直接调用内部函数的方法

def line_constants(a, b):

    # 在函数内部再定义一个函数,调用外部函数的参数
    def line(x):
        # 此处返回含有外部参数的表达式
        return a * x + b

    # 此处返回内部函数的值
    return line


# 直接调用 line_constants(1, 1),不会执行内部函数
# 先给外部函数传递参数,此时变量即为内部函数
line1 = line_constants(1, 1)
print(line1(5))

1.1. nonlocal#

默认情况下,闭包不能通过赋值变量来影响包围的作用域。此时可使用 nonlocal 来指示闭包何时修改其包围作用域中的变量。

numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}


def sort_priority(numbers, group):

    found = False

    def helper(x):

        nonlocal found
        if x in group:
            found = True
            return (0, x)
        return (1, x)

    numbers.sort(key=helper)
    return found


found = sort_priority(numbers, group)
print(f'Found: {found}')
print(numbers)

2. 常规装饰器#

装饰器(decorator):一种在代码运行期间动态增加功能的方式,本质上是一个返回函数的高阶函数。

代码要遵循开放封闭原则,已经实现的功能代码不允许被修改,但可被扩展。

2.1. 调用顺序#

# 定义功能函数,此处往往需要闭包
def Bold(fn):

    def wrapped():
        print('Bold')
        if True:
            return "<b>" + fn() + "</b>"
        else:
            print('Already')

    return wrapped


def Italic(fn):

    def wrapped():
        print('Italic')
        return "<i>" + fn() + "</i>"

    return wrapped


@Bold
@Italic
def test():
  return "test"

# 实现顺序为:调用装饰 1 -> 调用装饰 2 -> 调用目标函数
print(test())
# Bold
# Italic
# <b><i>test</i></b>

2.2. 返回值#

当装饰器内部函数没有 return 时,被装饰函数无法得到返回值;当装饰器内部函数有 return ,被装饰函数无 return 时,装饰器默认返回 None

from time import ctime, sleep


def timefun(func):

    def wrapper():
        print(f"{func.__name__} called at {ctime()}")
        func()

    return wrapper


@timefun
def getInfo():
    return '--hahah-'


print(getInfo())
# getInfo called at Sun May  1 00:03:17 2022
# None

2.3. 参数#

装饰器的内部函数,需要接收与被装饰函数等量的参数,为灵活起见,内部函数定义时常使用可变参数和关键字参数。

from time import perf_counter


def clock(func):

    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        arg_str = ', '.join(repr(arg) for arg in args)
        print(f'[{elapsed:0.8f}s] {func.__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(5)')
    print(f'6! = {factorial(5)}')


# **************************************** Calling snooze(.123)
# [0.12801371s] snooze(0.123) -> None
# **************************************** Calling factorial(5)
# [0.00000021s] factorial(1) -> 1
# [0.00000787s] factorial(2) -> 2
# [0.00001029s] factorial(3) -> 6
# [0.00002133s] factorial(4) -> 24
# [0.00003317s] factorial(5) -> 120
# 6! = 120

3. 改进#

3.1. 副作用#

使用装饰器时,被装饰后的函数其实已经是另外一个函数了,不支持关键字参数,而且遮盖了被装饰函数 的 __name____doc__ 属性。functools.wraps 可消除这样的副作用,不影响装饰器功能。

import time
import functools


def clock(func):

    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - t0

        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = [f'{k}={w}' for k, w in sorted(kwargs.items())]
            arg_lst.append(', '.join(pairs))

        arg_str = ', '.join(arg_lst)
        print(f'[{elapsed:0.8f}s] {func.__name__}({arg_str}) -> {result}')
        return result

    return clocked

3.3. 参数化#

import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {func.__name__}({arg_str}) -> {result}'


def clock(fmt=DEFAULT_FMT):

    def decorate(func):

        def clocked(*args):
            t0 = time.time()
            result = func(*args)
            elapsed = time.time() - t0

            arg_str = ', '.join(repr(arg) for arg in args)
            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)

    @clock('{func.__name__}: {elapsed}s')
    def snooze(seconds):
        time.sleep(seconds)

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

# [0.12667298s] snooze(0.123) -> None
# [0.12758803s] snooze(0.123) -> None
# [0.12805295s] snooze(0.123) -> None
# snooze: 0.12808489799499512s
# snooze: 0.1241450309753418s
# snooze: 0.12802600860595703s

3.4. 局部重载#

import html


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


print(f'{1, 2, 3}: {htmlize({1, 2, 3})}')
# (1, 2, 3): <pre>{1, 2, 3}</pre>
print(f'abs: {htmlize(abs)}')
# abs: <pre>&lt;built-in function abs&gt;</pre>
print(f"'Heimlich & Co. - a game': {htmlize('Heimlich & Co. - a game')}")
# 'Heimlich & Co. - a game': <pre>&#x27;Heimlich &amp; Co. - a game&#x27;</pre>
print(f"['alpha', 66, {3, 2, 1}]: {htmlize(['alpha', 66, {3, 2, 1}])}")
# ['alpha', 66, (3, 2, 1)]: <pre>[&#x27;alpha&#x27;, 66, {1, 2, 3}]</pre>

因为 Python 不支持重载方法或函数,所以我们不能使用不同的签名定义 htmlize 的变体, 也无法使用不同的方式处理不同的数据类型。使用 @singledispatch 装饰的普通函数会变成 泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函 数。

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


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


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


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


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