Python装饰器

Python @函数装饰器及用法

假设用 funA() 函数装饰器去装饰 funB() 函数,如下所示:

1
2
3
4
5
6
7
8
9
#funA 作为装饰器函数
def funA(fn):
    #...
    fn() # 执行传入的fn参数
    #...
    return '...'
@funA
def funB():
    #...

实际上,上面程序完全等价于下面的程序:

1
2
3
4
5
6
7
8
def funA(fn):
    #...
    fn() # 执行传入的fn参数
    #...
    return '...'
def funB():
    #...
funB = funA(funB)

通过比对以上 2 段程序不难发现,使用函数装饰器 A() 去装饰另一个函数 B(),其底层执行了如下 2 步操作:

  • 将 B 作为参数传给 A() 函数;
  • 将 A() 函数执行完成的返回值反馈回 B。

举个实例:

1
2
3
4
5
6
7
8
9
#funA 作为装饰器函数
def funA(fn):
    print("C语言中文网")
    fn() # 执行传入的fn参数
    print("http://c.biancheng.net")
    return "装饰器函数的返回值"
@funA
def funB():
    print("学习 Python")

程序执行流程为:

C语言中文网
学习 Python
http://c.biancheng.net

在此基础上,如果在程序末尾添加如下语句: print(funB) 其输出结果为: 装饰器函数的返回值

显然,被“@函数”修饰的函数不再是原来的函数,而是被替换成一个新的东西(取决于装饰器的返回值),即如果装饰器函数的返回值为普通变量,那么被修饰的函数名就变成了变量名;同样,如果装饰器返回的是一个函数的名称,怎么被修饰的函数名依然表示一个函数。

实际上,所谓函数装饰器,就是通过装饰器函数,在不修改原函数的前提下,来对函数的功能进行合理的扩充。

带参数的函数装饰器

在分析 funA() 函数装饰器和 funB() 函数的关系时,细心的读者可能会发现一个问题,即当 funB() 函数无参数时,可以直接将funB作为funA()的参数传入。但是,如果被修饰的函数本身带有参数,那应该如何传值呢?

比较简单的解决方法就是在函数装饰器中嵌套一个函数,该函数带有的参数个数和被装饰器修饰的函数相同。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def funA(fn):
    # 定义一个嵌套函数
    def say(arc):
        print("Python教程:",arc)
    return say
@funA
def funB(arc):
    print("funB():", a)
funB("http://c.biancheng.net/python")

程序执行结果为: Python教程: http://c.biancheng.net/python

它和如下程序是等价的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def funA(fn):
    # 定义一个嵌套函数
    def say(arc):
        print("Python教程:",arc)
    return say
def funB(arc):
    print("funB():", a)
   
funB = funA(funB)
funB("http://c.biancheng.net/python")

如果运行此程序会发现,它的输出结果和上面程序相同。

显然,通过 funB() 函数被装饰器 funA() 修饰,funB 就被赋值为 say。这意味着,虽然我们在程序显式调用的是 funB() 函数,但其实执行的是装饰器嵌套的 say() 函数。

但还有一个问题需要解决,即如果当前程序中,有多个(≥ 2)函数被同一个装饰器函数修饰,这些函数带有的参数个数并不相等,怎么办呢?

最简单的解决方式是用 *args 和 **kwargs 作为装饰器内部嵌套函数的参数,*args 和 **kwargs 表示接受任意数量和类型的参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def funA(fn):
    # 定义一个嵌套函数
    def say(*args,**kwargs):
        fn(*args,**kwargs)
    return say
@funA
def funB(arc):
    print("C语言中文网:",arc)
@funA
def other_funB(name,arc):
    print(name,arc)
funB("http://c.biancheng.net")
other_funB("Python教程:","http://c.biancheng.net/python")

运行结果

C语言中文网: http://c.biancheng.net
Python教程: http://c.biancheng.net/python

函数休市前可以嵌套

Python 也支持多个装饰器,比如:

1
2
3
4
5
@funA
@funB
@funC
def fun():
    #...

上面程序的执行顺序是里到外,所以它等效于下面这行代码: fun = funA( funB ( funC (fun) ) )


@staticmethod和@classmethod的作用与区别

一般来说,要使用某个类的方法,需要先实例化一个对象再调用方法。

而使用@staticmethod或@classmethod,就可以不需要实例化,直接类名.方法名()来调用。

这有利于组织代码,把某些应该属于某个类的函数给放到那个类里去,同时有利于命名空间的整洁。

既然@staticmethod和@classmethod都可以直接类名.方法名()来调用,那他们的区别是:

  • @staticmethod不需要表示自身对象的self和自身类的cls参数,就跟使用函数一样。
  • @classmethod也不需要self参数,但第一个参数需要是表示自身类的cls参数。

如果在@staticmethod中要调用到这个类的一些属性方法,只能直接类名.属性名或类名.方法名。

而@classmethod因为持有cls参数,可以来调用类的属性,类的方法,实例化对象等,避免硬编码。

代码例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class A(object):
    bar = 1
    def foo(self):
        print("foo")
    @staticmethod
    def static_foo():
        print("static_foo")
        print(A.bar)

    @classmethod
    def class_foo(cls):
        print("ckass_foo")
        print(cls.bar)
        cls().foo()

A.static_foo()
A.class_foo()

输出

1
2
3
4
5
statci_foo
1
class_fo
1
foo
updatedupdated2020-04-182020-04-18