IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Python知识库 -> Python 函数装饰器和闭包 -> 正文阅读

[Python知识库]Python 函数装饰器和闭包

函数装饰器和闭包

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为,用闭包实现。

nolocal是在Python3.0新增的保留关键字。

闭包除了在装饰器中有用之外,还回调式一步编程和函数式编程风格的基础。

装饰器基础,何时执行装饰器

装饰器是可调用对象,参数是另一个函数(被装饰的函数)。

函数装饰器再导入模块是立即执行,而被装饰的函数只有在调用时运行。

装饰器通常在一个模块中定义,然后应用到其他模块中的函数上。

registry = []


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


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


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


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


def main():
    print(registry)
    f1()
    f2()
    f3()


if __name__ == '__main__':
    main()
打印
running :<function f1 at 0x034F9618>
running :<function f2 at 0x034F96A8>
[<function f1 at 0x034F9618>, <function f2 at 0x034F96A8>]
f1 running.
f2 running.
f3 running.

以上可以看到,在加载模块时,函数装饰器立即执行。被装饰的函数在调用时执行。

示例中的装饰器原封不动的返回被装饰的函数,这种并不是没有用途,比如在Python web框架中,这样的装饰器把函数添加到某种中央注册处,实现路由功能。

使用装饰器实现“策略”模式

使用装饰器,这种方案有几个优点:

  • 促销策略函数无需特殊的名称(不需要以Promo结尾),或不需要放到指定模块中
  • @promotion 装饰器,还便于临时禁用某个促销策略,只需要把装饰器去掉。
  • 促销策略装饰器可以在其他模块中定义,在系统的任何地方都行,只需要导入后装饰。

示例,使用装饰器完成策略模式(部分代码)

promos = []


def promotion(func):
    promos.append(func)
    return func


@promotion
def FidelityPromo(order):
    """具体策略:有1000积分以上的顾客,整个订单可以享受5%的折扣"""
    return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0


@promotion
def BulkItemPromo(order):
    """具体策略:同一个订单中,单个商品的数量达到20个以上,单品享受10%折扣"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount


@promotion
def LargeOrderPromo(order):
    """具体策略:订单中不同商品的数量达到10个以上,整个订单享受7%折扣"""
    return order.total() * 0.07 if len({item.product for item in order.cart}) >= 10 else 0


def best_promo(oder):
    """选择最佳的折扣"""
    return max(promo(oder) for promo in promos)

变量作用域

这个是一个让人吃惊的示例

b = 2


def f(a):
    print(a)
    print(b)
    b = 2


f(1)
打印
1
Traceback (most recent call last):
  File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 53, in <module>
    f(1)
  File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 49, in f
    print(b)
UnboundLocalError: local variable 'b' referenced before assignment

如果在函数内不加b = 2 程序可以正常执行,因为b会找到全局变量中的b=2

但是在函数最后加上了b=2,在Python编译函数的定义体时,判断出b是局部变量,因为在函数内给它赋值了,就不会获取全局变量。

比较字节码来论证:

dis模块为反汇编Python函数字节码提供了简单的方式。

def f2(a):
    print(a)
    print(b)


from dis import dis

print(dis(f2))

结果可以看到a为LOAD_FAST 本地名称,b为LOAD_GLOBAL全局名称。

b = 2


def f2(a):
    print(a)
    print(b)
    b =2


from dis import dis

print(dis(f2))

可以看到这次b是LOAD_FAST 本地名称

另外补充:

LOAD_FAST(var_num)

将对本地co_varnames [var_num]的引用压入堆栈。

闭包

假如有个avg函数,他的作用是计算不断增加系列值的均值。

示例,使用类实现

class Avg:
    def __init__(self):
        self.elements = []

    def __call__(self, *args, **kwargs):
        self.elements.append(args[0])
        return sum(self.elements) / len(self.elements)


avg = Avg()
print(avg(10))
print(avg(11))
print(avg(12))
打印
10.0
10.5
11.0

示例,使用闭包实现

def make_avg():
    elements = []

    def inner(element):
        elements.append(element)
        return sum(elements) / len(elements)

    return inner


avg = make_avg()
print(avg(10))
print(avg(11))
print(avg(12))
打印
10.0
10.5
11.0

Avg类的实例avg存储历史值是在self.elements实例属性中。

在inner函数中,elements是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量

我们可以使用函数的__code__属性中查询局部变量和自由变量的名称

__code__.co_varnames 局部变量

__code__.co_freevars 自由变量

avg.__closure__[0].cell_contents 自由变量绑定的真实值

avg = make_avg()
avg(10)
print(avg.__code__.co_varnames)  # 局部变量
print(avg.__code__.co_freevars)  # 自由变量
print(avg.__closure__)  # 自由变量绑定的值
print(avg.__closure__[0].cell_contents)  # 自由变量绑定的真正的值
打印
('element',)
('elements',)
(<cell at 0x02CEEF50: list object at 0x02CA4A80>,)
[10]

从上所述,闭包是一种函数,它会保留定义函数时,存在自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。

nonlocal声明

前面的make_avg效率不高,应该只存储目前的总值和元素个数。

def make_avg():
    total = 0  # 总值
    count = 0  # 元素个数

    def inner(element):
        nonlocal total, count
        total += element
        count += 1
        return total / count

    return inner

首先说明区别,total和count是数字,对于数字、字符串、元祖等不可变类型,都是只读,可能更新。

而之前的例子中的elements是列表,是可变的对象。

这里就需要用到了nonlocal这个关键字,因为count+=1这样就在inner定义体中未count赋值了,这样会把count变成局部变量。

如果尝试count+=1,其实会隐式的创建局部变量count,这样count就不是自由变量了,不会保存到闭包中。

在Python3中,引入了nonlocal声明。它的作用是标记变量为自由变量,即使在函数中为变量赋予了新值,也是自由变量,如果赋予了新值,闭包中保存的绑定也会更新。

在Python2中,需要变通的办法,可以把内部函数修改的变量存储为可变对象(比如字典或者简单的实例),并且把那个对象绑定为一个自由对象。

简单的装饰器-记录函数运行时间

函数计时,记录经过的时间、传入的参数、调用的结果打印出来

import time
import functools


def clock(func):
    """函数执行时间,参数,结果"""

    @functools.wraps(func)
    def inner(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        time_use = time.time() - t0
        func_name = func.__name__
        parameter = []
        if args:
            parameter.append(','.join(repr(arg) for arg in args))
        if kwargs:
            kwargs_str = ",".join('%s=%r' % (k, v) for k, v in kwargs.items())
            parameter.append(kwargs_str)
        parameter = ','.join(parameter)
        print('[%0.8fs] %s(%s) -> %r' % (time_use, func_name, parameter, result))
        return result

    return inner


@clock
def use(a, b, c=1):
    time.sleep(1)
    print(a, b, c)


@clock
def fab(n):
    return 1 if n < 2 else fab(n - 1) * n


use(4, '5', c=6)

fab(6)
打印
4 5 6
[1.00087142s] use(4,'5',c=6) -> None
[0.00000000s] fab(1) -> 1
[0.00000000s] fab(2) -> 2
[0.00000000s] fab(3) -> 6
[0.00000000s] fab(4) -> 24
[0.00000000s] fab(5) -> 120
[0.00000000s] fab(6) -> 720

示例中要注意的几点:

@functools.wraps(func) 还原被装饰函数的 __name__和__doc__属性。

%0.8f 表示小数点后保留8位小数

%r 表示适合机器阅读的格式,相对于%s好处是可以接受数字类型。而%s接受到数字会报错。%r接受到字符串时会添加单引号''

标准库中的装饰器

functools.lru_cache实现缓存功能。

这是一项优化技术,把耗时的函数的结果保存起来,避免了传入相同的参数时重复计算。

lru是Least Recently Used的缩写,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉。

生成第n个斐波那契数列,这种慢速递归函数非常适合使用lru_cache。

?斐波那契数列指的是这样一个数列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

示例,生成第n个斐波那契数列

import time
import functools


def clock(func):
    """函数执行时间,参数,结果"""

    @functools.wraps(func)
    def inner(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        time_use = time.time() - t0
        func_name = func.__name__
        parameter = []
        if args:
            parameter.append(','.join(repr(arg) for arg in args))
        if kwargs:
            kwargs_str = ",".join('%s=%r' % (k, v) for k, v in kwargs.items())
            parameter.append(kwargs_str)
        parameter = ','.join(parameter)
        print('[%0.8fs] %s(%s) -> %r' % (time_use, func_name, parameter, result))
        return result

    return inner


@clock
def fibonacci(n):
    """计算斐波那契数列的第n个项(从0项开始)"""
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


print(fibonacci(4))
打印
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00100422s] fibonacci(3) -> 2
[0.00100422s] fibonacci(4) -> 3
3

可以看到,浪费时间的地方很明显,fibonacci(1)的运算调用了三次,如果使用lru_cache装饰后,性能会显著改善。

@functools.lru_cache()
@clock
def fibonacci(n):
    """计算斐波那契数列的第n个项(从0项开始)"""
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


print(fibonacci(4))
打印
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(3) -> 2
[0.00000000s] fibonacci(4) -> 3
3

每个n值值调用了一次函数,因为重复的调用会在缓存中取,不再真实的调用。

lru_cache可以使用两个可选的参数。lru_cache(maxsize=128, typed=False)

maxsize 表示存储多少个调用的结果。

typed 如果设置为True,会把不同的类型分开存储,比如说通常认为1.0和1是一样的结果,但是他们类型不同,一个是浮点数,一个数整数。

ps:因为lru_cache使用字典存储结果,键值是根据调用时传入的参数来创建的,所有被装饰的函数的参数,必须是可散列的。

单分派泛函数 singledispatch

Python3.4中新增的functools.singledispathch装饰器,可以把整体方案拆分成多个模块,还可以为无法修改的类提供专门的函数。

Python2.6可以使用第三方模块singledispatch实现

使用@singledispathch装饰的普通函数会变成泛函数: 根据第一个参数的类型,以不同的方式执行相同的操作的一种函数。

如果是根据多个参数选择专门的函数,就是多分派了。

示例,一个简单的生产HTML的工具

import html


def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>%s</pre>' % content


print(htmlize({1, 2, 3}))
print(htmlize(abs))
打印
<pre>{1, 2, 3}</pre>
<pre>&lt;built-in function abs&gt;</pre>

扩展:

html.escape 函数是把字符串 转换成 HTML转义符

html.unescape 函数是把HTML转义符再换成字符串 (这俩函数常用于爬虫)

比如上面的<转成了&lt; >转成了&gt;

现在想对这个htmlize函数做一个扩展,根据参数类型的不同,进行不同的方式处理。通常是htmlize函数变成一个分派函数,里面使用不同的if elif来判断不同类型,调用其他函数。这样并不优雅,使用functools.singledispatch能完成这个方案。

当使用@singledispatch装饰器,装饰了htmlize函数,就可以创建出一个htmlize.register()装饰器,使用这个装饰器就可以把多个函数绑定一起组成一个泛函数。

  • @singledispatch装饰器来标记处理object类型的基函数。
  • 各个类型的专门函数使用 @<base_function>.register()装饰。
  • 专门函数的名称无关紧要,使用_是个不错的选择,简洁明了。
  • 为每个需要特殊处理的类型注册一个函数
  • 可以叠加多个register装饰器,让一个函数处理不同的类型。

from functools import singledispatch
from collections import abc

import html
import numbers


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


@htmlize.register(str)
def _(text):
    """如果是str类型:把内部换行符\n改为<br>\n 不使用<pre> 而是使用<p>"""
    content = html.escape(text).replace(r'\n', r'<br>\n')
    return '<p>%s</p>' % content


@htmlize.register(numbers.Integral)  # numbers.Integral 是int的虚拟超类
def _(number):
    """如果是int类型:以十进制和十六进制显示数字"""
    return '<pre>%d (0x%x)</pre>' % (number, number)  # %x以十六进制显示,不带0x


@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)  # 可变序列MutableSequence ,继承自序列Sequence
def _(seq):
    """如果是tuple或者可变序列,输出一个HTML列表,根据各个元素的类型格式化"""
    res = []
    for element in seq:
        res.append('<li>{}</li>'.format(htmlize(element)))
    return '<ul>\n{}\n</ul>'.format('\n'.join(res))


print(htmlize(r'wo \n ai'))
print(htmlize(42))
print(htmlize(['wo', 66, {1, 2, 3}]))
打印
<p>wo <br>\n ai</p>
<pre>42 (0x2a)</pre>
<ul>
<li><p>wo</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>

扩展:

abc.MutableSequence 代表任何可变序列,集成自序列Sequence

numbers.Integral 是int的虚拟超类

%x 可以让数字以十六进制显示,但是不带0x

注册的专门函数的参数类型,应该尽量使用像abc.MutableSequence和numbers.Integral这种抽象基类,这样代码支持的兼容类型就更广泛。

补充,在Python3.8版本中带来了:

singledispatchmethod
singledispatch主要针对的是函数,但对于方法不友好,举个例子:

In : class Dispatch:
...:     @singledispatch
...:     def foo(self, a):
...:         return a
...:
...:     @foo.register(int)
...:     def _(self, a):
...:         return 'int'
...:
...:     @foo.register(str)
...:     def _(self, a):
...:         return 'str'
...:

In : cls = Dispatch()

In : cls.foo(1)
Out: 1  # 没有返回 'int'

In : cls.foo('s')
Out: 's'  # 没有返回 'str'


也就是 singledispatch 在方法上失效了。现在可以用singledispatchmethod来做了:

>>> from functools import singledispatchmethod
>>> class Dispatch:
...     @singledispatchmethod
...     def foo(self, a):
...         return a
...
...     @foo.register(int)
...     def _(self, a):
...         return 'int'
...
...     @foo.register(str)
...     def _(self, a):
...         return 'str'
...
>>> cls = Dispatch()
>>> cls.foo(1)
'int'
>>> cls.foo('s')
'str'

参数化装饰器

想要实现装饰器接收参数,需要创建一个装饰器工厂函数,把参数传给他,返回一个装饰器,然后再把它用在要装饰去函数上。

一个参数化的装饰器,让register装饰器具备可选注册和注销功能。通过设置一个参数,设为FALSE时,不注册被装饰去函数。

注意此时register函数不是装饰器,而是装饰器工厂函数。调用它时才会返回真正的装饰器。

示例,接收参数的注册功能装饰器

registry = set()  # 使用集合,添加和删除速度更快


def register(active=True):
    def decorate(func):  # decorate才是装饰器
        if active:  # 通过闭包获取active的值,是True时才注册
            registry.add(func)
        else:
            registry.discard(func)
        return func

    return decorate


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


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


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


print('main running')
print('registry ->', registry)
打印
main running
registry -> {<function f2 at 0x037C9738>}

扩展:

使用set集合比list添加和删除的速度更快

set.discard(x) 删除指定元素,set.?remove()?方法在移除一个不存在的元素时会发生错误,而?discard()?方法不会。

以上可以看到只有f2函数被注册了,f1虽然被装饰了,但是active=False,所以f1不会被注册。

如果不使用@语法,像常规函数那样使用register,装饰f2函数就是register()(f2),装饰f1函数就是register(active=False)(f1)

一个完整的三层函数的装饰器

最外层的函数是装饰器的工厂函数。第二层才是真正的装饰器,第三层是用来包装被装饰的函数用的。

示例,一个clock装饰器,参数是格式字符串。

"""clockdeco_param.py"""
import time
import functools
 
FMT = '[{time_use:0.8f}s] {func_name}({parameter}) -> {result}'  # 格式化的模板 (作为装饰器参数)
 
 
def clock(fmt=FMT):
    """函数执行时间,参数,结果"""
 
    def decorate(func):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            t0 = time.time()
            result = func(*args, **kwargs)
            time_use = time.time() - t0
            func_name = func.__name__
            parameter = []
            if args:
                parameter.append(','.join(repr(arg) for arg in args))
            if kwargs:
                kwargs_str = ",".join('%s=%r' % (k, v) for k, v in kwargs.items())
                parameter.append(kwargs_str)
            parameter = ','.join(parameter)
            # print('[%0.8fs] %s(%s) -> %r' % (time_use, func_name, parameter, result))
            print(fmt.format(**locals()))  # 这里拆包局部变量,省去了参数字典
            return result
 
        return inner
 
    return decorate
 
 
@clock()
def test(s, a=2):
    time.sleep(1)
    print('func test running')
    return 'over'
 
 
test('wo', a=3)
打印
func test running
[1.01234961s] test('wo',a=3) -> over

clock是参数化装饰器的工厂函数,decorate才是真正的装饰器。

扩展:

代码中使用了locals()然后拆包,locals()里面保存了inner函数的局部变量信息,是个字典,如果打印出来就是:

{'args': ('wo',), 'kwargs': {'a': 3}, 't0': 1618408087.1784744, 'result': 'over', 'time_use': 1.0123496055603027, 'func_name': 'test', 'parameter': "'wo',a=3", 'kwargs_str': 'a=3', 'fmt': '[{time_use:0.8f}s] {func_name}({parameter}) -> {result}', 'func': <function test at 0x020F4150>}

如果在其他文件中使用clockdeco_param.py模块中的clock装饰器,可以:

from?clockdeco_param import clock

@clock('{func_name}: {time_use}s')

def func():pass

这样就可以随意改变输出格式:

func: 0s

能接受可选参数的装饰器

那之前的注册装饰器来说,要实现可选参数,也就是可以使用@register、@register()、@register(active=True)这三种形式

这三种形式如果不用@表示,使用函数表示,分别是register(f)、 register()(f)、 register(active=True)(f)

这样就比较好理解以下的代码了

registry = set()  # 使用集合,添加和删除速度更快


def register(f=None, active=True):
    if f is not None:
        registry.add(f)

    def decorate(func):  # decorate才是装饰器
        if active:  # 通过闭包获取active的值,是True时才注册
            registry.add(func)
        else:
            registry.discard(func)
        return func

    return decorate


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


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


@register
def f3():
    print('f3 running.')


print('main running')
print('registry ->', registry)
打印结果
main running
registry -> {<function f3 at 0x02C49618>, <function f2 at 0x02C49738>}

可以看到register装饰器加不加括号()都成功把函数注册成功了。

ps:思路来自《Python Cookbook》

  Python知识库 最新文章
Python中String模块
【Python】 14-CVS文件操作
python的panda库读写文件
使用Nordic的nrf52840实现蓝牙DFU过程
【Python学习记录】numpy数组用法整理
Python学习笔记
python字符串和列表
python如何从txt文件中解析出有效的数据
Python编程从入门到实践自学/3.1-3.2
python变量
上一篇文章      下一篇文章      查看所有文章
加:2022-04-18 17:36:48  更:2022-04-18 17:39:01 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 17:34:18-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码