闭包与装饰器#
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><built-in function abs></pre>
print(f"'Heimlich & Co. - a game': {htmlize('Heimlich & Co. - a game')}")
# 'Heimlich & Co. - a game': <pre>'Heimlich & Co. - a game'</pre>
print(f"['alpha', 66, {3, 2, 1}]: {htmlize(['alpha', 66, {3, 2, 1}])}")
# ['alpha', 66, (3, 2, 1)]: <pre>['alpha', 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>'