详解Python的装饰器和闭包

Last Updated: 2023-05-31 07:37:36 Wednesday

-- TOC --

Python编程语言的超级语法糖,装饰器,decorator,看着就很高大上。装饰器的基础,是闭包,closure!

装饰器基础

下面是一个简单的装饰器代码:

def calling_trace(func):
    def wrapper():
        print('calling', func.__name__)
        func()
    return wrapper


@calling_trace
def test1():
    print('test1 is runing...')


# test1 = calling_trace(test1)
test1()

Python解释器在最后调用test1之前,就已经运行了装饰器(wrapper记住了func),效果如注释掉的那一行。这段代码的运行效果:

$ python3 deco.py
calling test1
test1 is runing...

装饰器作用于函数,就是把这个函数包裹起来,在此函数执行前后,增加一些其它的代码!经过装饰器修饰的函数,虽然函数名称不变,但实际上调用的入口已经发生了变化。如上例,直接调用test1(),入口实际上是calling_trace中的wrapper!

在function前使用@装饰器,与test=calling_trace(test1)的效果一样!装饰器将function作为参数,同时返回另一个function!

或者可以这样理解,装饰器将函数作为参数,执行装饰器的过程,实际上是创建了另一个函数,在这个新创建的函数内,执行被装饰的函数,被装饰的函数的原有的执行方式已经不再存在。

装饰一个function的概念,就是在不影响此function的执行的前提下,在其前其后增加一些代码,即所谓装饰。前后加打印,是最简单的装饰:

def calling_trace(func):
    def wrapper():
        print('calling', func.__name__)
        a = func()
        print('this is the reture:', a)
    return wrapper


@calling_trace
def test2():
    print('test2 is runing...')
    return 'cs.pynote.net'

# test2 = calling_trace(test2)
test2()

执行结果:

$ python3 deco.py
calling test2
test2 is runing...
this is the reture: cs.pynote.net

装饰器就是一个返回函数的函数!

闭包(closure)

沿用上面的测试代码,稍作修改:

def calling_trace(func):
    def wrapper():
        print('calling', func.__name__)
        a = func()
        print('this is the reture:',a)
    return wrapper


def test2():
    print('test2 is runing...')
    return 'cs.pynote.net'


tt = calling_trace(test2)
tt()

运行效果如下,与之前直接装饰test2的效果完全一样:

$ python3 deco.py
calling test2
test2 is runing...
this is the reture: cs.pynote.net

此时的tt,就是装饰过的test2!原生的test2还可以被使用,没有被装饰过。(使用装饰器就没有这样的效果)

可以看出closure的过程,就是调用一个可以接收函数作为参数的接口,通过返回得到一个新的接口。此时的tt,已经包含了test2这个函数参数在内,调用tt,相当于调用已经确定了参数的wrapper。

这就是python中的闭包的概念:把一个函数包起来,通过不同的外部参数,生成多个此函数的不同版本!这些外部参数,也成为被抱起来的函数接口的执行环境。(closure就是包起来,除了可以把函数接口包起来,还可以把class包起来。包起来还带来了一个封装的效果,被包起来的function或class,外部不能直接访问,比如通过函数调用返回后才能访问。class的__iter__接口实现,就可以采用这个思路。)

>>> def addx(x):
...   def inner(y):
...     return y+x
...   return inner
... 
>>> add2 = addx(2)
>>> add2(1)
3
>>> add2(2)
4
>>> add2(3)
5
>>> add9 = addx(9)
>>> add9(1)
10
>>> add9(2)
11
>>> add9(3)
12

通过addx,生成了2个function,分别是add2和add9。注意这个case中的x入参,add2在执行的时候,x一直是2,而add9在执行的时候,x一直是9。我认为这是闭包的一个很重要的特性,它体现了add2和add9两个同源的接口的不同的执行环境。

功能上有点类似 from functools import partial

再看一个示例,模拟C语言的static variable:

def static_var():
    dd = {}
    def add_dd(a):
        dd.update(a)
        print(id(dd), dd)
    return add_dd


a = static_var()
a({'a':1})
a({'b':2})

b = static_var()
b({'c':3})

a({'a':5})
b({'b':9})

以上代码执行效果如下:

$ python3 test.py
140246105463168 {'a': 1}
140246105463168 {'a': 1, 'b': 2}
140246105463424 {'c': 3}
140246105463168 {'a': 5, 'b': 2}
140246105463424 {'c': 3, 'b': 9}

可以看出,dd这个dict对象,对于a和b而言,是不一样的。

其实,在函数接口直接使用一个默认为可变对象的参数(比如list或dict),但在调用的时候,绝不给这个参数提供值,这就是Python实现static变量的方式。这是不是bug,看情况了。以上闭包的测试代码,是另一种static变量的实现方案,更安全,a和b作为生成的接口,没有入参对应static变量。

装饰器与闭包是一个东西,根据不同的使用方式,名称有了区别!

装饰带参数的函数

case one:

def calling_trace(func):
    def wrapper(a,b,c=3):
        print('calling', func.__name__)
        a = func(a,b,c)
        print('reture value:',a)
    return wrapper


@calling_trace
def test3(a,b,c=3):
    print('test3 is runing...')
    return a+b+c


test3(1,2,5)
test3(1,2)

case two:

def calling_trace(func):
    def wrapper(*args):
        print('calling', func.__name__)
        a = func(*args)
        print('reture value:',a)
    return wrapper


@calling_trace
def test4(*args):
    print('test4 is runing...')
    return sum(args)

test4(1,2,3,4,5,6,7,8)
test4(23,34,45,56)

wrapper与要装饰的函数,参数列表必须相同!

给带参数的函数加装饰器,还有一种更通用的参数写法,这种写法在修改函数参数和调用处时,又可以反过来保持装饰器部分代码不变。示例如下:

def calling_trace(func):
    def wrapper(*args, **kw):
        print('calling', func.__name__)
        a = func(*args, **kw)
        print('reture value:',a)
    return wrapper


@calling_trace
def test5(a,b,c=3):
    print('test5 is runing...')
    return a+b+c


test5(1,2)
test5(1,2,c=8)

这种写法,要求key/value参数要放在后面,Python函数接口定义也有这个要求。

带参数的装饰器

本文以上示例,都是不带参数的装饰器,在使用@语法的时候,没有参数。而装饰器本身也可以带参数。示例如下:

def calling_trace(run):
    def deco(func):
        def wrapper(*args, **kw):
            print('calling', func.__name__)
            if run == 1:
                a = func(*args, **kw)
                print('reture value:',a)
            else:
                print('not allow to run')
        return wrapper
    return deco


# test5 = calling_trace(run=1)(test5)
@calling_trace(run=1)
def test5(a,b,c=3):
    print('test5 is runing...')
    return a+b+c


@calling_trace(run=0)
def test6(*args):
    print('test6 is runing...')
    return sum(args)


test5(1,2)
test5(1,2,c=8)
test6(23,34,45,56)

calling_trace(run=1)返回的deco成为了真正装饰test5和test6的装饰器!

多重装饰器

可以同时使用多个装饰器!多个装饰器的效果,就相当于对函数进行了多层的封装包裹,而不同的装饰器对函数执行的功能影响,完全独立。比如有个装饰器用来控制函数是否能够被执行,另一个装饰器控制函数的所有raise出来的异常。

@a
@b
@c
def tt(): pass
# tt = a(b(c(tt)))

面向切面编程(AOP)

从装饰器引出了一个很厉害的概念,叫做面向切面的编程!

面向切面编程(AOP是Aspect Oriented Program的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。

但是人们也发现,在分散代码的同时,也增加了代码的重复性。

什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。

也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程(AOP)。

一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。

这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。

AOP思想:

闭包骚操作

在网上看到如下一个经典的case,略作修改:

flist = []

for i in range(3):
    def func(x):
        return x*i
    flist.append(func)

for f in flist:
    print(id(f), f(2))

这段代码的运行结果:

140623728098704 4
140623727591088 4
140623727591232 4

func函数在运行时,始终只能访问def定义之外的i。因此当执行f(2)的时候,得到的结果都是4,因为loop之后,i=2。另一个值得注意的细节是,每一次flist.append的func的地址都不同!func被重复定了3次。

如何修正上面的代码,使其提供闭包特性,下面提供多种不同的修正方式供参考:

# avoid closures and use default args which copy on function definition
for i in range(3):
    def func(x, i=i):
        return x*i
    flist.append(func)

# or introduce an extra scope to close the value you want to keep around:
for i in range(3):
    def makefunc(i):
        def func(x):
            return x*i
        return func
    flist.append(makefunc(i))

# the second can be simplified to use a single makefunc():
def makefunc(i):
    def func(x):
        return x*i
    return func

for i in range(3):
    flist.append(makefunc(i))

# if your inner function is simple enough, lambda works as well for either option:
for i in range(3):
    flist.append(lambda x, i=i: x*i)

def makefunc(i):
    return lambda x: x*i
for i in range(3):
    flist.append(makefunc(i))

基于类的装饰器

直接上代码:

class calling_trace():
    def __init__(self, run):
        self.run = run

    def __call__(self, func):
        def wrapper(*args, **kw):
            print('calling', func.__name__)
            if self.run == 1:
                a = func(*args, **kw)
                print('return value:', a)
            else:
                print('not allow to run')
        return wrapper


@calling_trace(run=1)
def test5(a,b,c=3):
    print('test5 is runing...')
    return a+b+c


@calling_trace(run=0)
def test6(*args):
    print('test6 is runing...')
    return sum(args)

test5(1,2)
test5(1,2,c=8)
test6(23,34,45,56)

运行情况:

$ python3 deco.py
calling test5
test5 is runing...
return value: 6
calling test5
test5 is runing...
return value: 11
calling test6
not allow to run

在Python中,像__call__这样前后都带下划线(dunden)的方法在Python中被称为内置方法,有时候也被称为魔法方法(magic method)。重载这些魔法方法一般会改变对象的内部行为。这个例子就让一个类对象拥有了被直接像函数调用的的能力。

update_wrapper接口

from functools import update_wrapper

这个接口用来更新wrapper,让它看起来更像wrapped function。它主要是改变wrapper的__name____doc__,修改之后,返回wrapper。

from functools import update_wrapper


def before(func):
    def wrapper():
        print('before...')
        func()
    return wrapper


def after(func):
    def wrapper():
        print('after...')
        func()
    return update_wrapper(wrapper, func)


@before
def test1():
    """fucntion test1"""
    print('hello test1')


@after
def test2():
    """function test2"""
    print('hello test2')


test1()
print('__name__', test1.__name__)
print('__doc__', test1.__doc__)
print('------------')
test2()
print('__name__', test2.__name__)
print('__doc__', test2.__doc__)

运行结果:

$ python3 test_uw.py
before...
hello test1
__name__ wrapper
__doc__ None
------------
after...
hello test2
__name__ test2
__doc__ function test2

这样看来,自定义的装饰器,几乎必须要使用update_wrapper接口了。

Python自带的装饰器

@classmethod和@staticmethod

@singledispatch

@singledispatchmethod

@property

@lru_cache

@abstractmethod

本文链接:https://cs.pynote.net/sf/python/202203091/

-- EOF --

-- MORE --