抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

这里讲一讲前面提到的python装饰器,@classmethod和@staticmethod是python内置装饰器,在了解什么是装饰器之前首先要了解函数的几个特征。

1. 有关函数的几个概念

1.1 函数可以接收另一个函数作为参数传入

高阶函数可以接收另一个函数作为传入的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
def func1(a, b):
return a + b

# 高阶函数,函数func2接收函数作为参数传入
def func2(func, m, n):
return func(m, n)

func2(func1, 1, 2)

'''
运行结果:
3
'''

从上面例子可以看到,在执行 func2函数的时候,函数对象func1作为参数被传入func2,返回func1(1, 2)的执行结果也就是3.

1.2 函数可以把另一个函数作为结果返回

高阶函数也可以将函数作为结果返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 高阶函数,把函数作为结果返回
def func1():
pass

def func2(): # 内层函数(嵌套函数)
print('执行func2函数')

return func2 # 返回内层函数的引用

a = func1() # 返回的函数对象func2的引用赋值给a
print(a) # 打印函数对象,获得存储地址
a() # 执行内层函数func2()的功能

'''
运行结果:
<function func1.<locals>.func2 at 0x00000288D3701750>
执行func2函数
'''

上面的例子可以看到,外围函数 func1将内层函数 func2的引用赋值给a,此时a就有了内层函数func2 的方法,此时打印的a是函数的存储地址,执行a() 就可以执行func2 函数的功能。

1.3 嵌套函数可以引用外层函数的变量

稍稍修改1.2的例子,在外层函数添加局部变量msg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def func1():    # 外围函数
msg = 'I am Phantom'

def func2(): # 内层函数(嵌套函数)
print(msg)

return func2

a = func1() # 实际上这里获得的就是一个闭包
a() # 引用外层函数的变量,执行内层函数func2()的功能

'''
运行结果:
I am Phantom
'''

这里先引用闭包的概念:

闭包:指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。概念比较晦涩,简单来说就是嵌套函数引用了外层函数的变量

这个例子和上个例子唯一的区别是,msg是一个在外围函数中的局部变量,在print_msg()函数执行之后应该就不会存在了。但是嵌套函数引用了这个变量,将这个局部变量封闭在了嵌套函数中,这样就形成了一个闭包

有了以上关于高阶函数和闭包的概念后,就可以开始理解什么是装饰器以及装饰器的作用了。

2. 装饰器decorator

装饰器的本质就是一个闭包,把一个函数当做参数然后返回一个替代版函数(函数的引用)。

2.1 @标识符将装饰器应用到函数

下面将用代码方式简单演示装饰器是怎么应用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def func1(func3):

def func2():
print(f'被装饰的函数{func3.__name__}即将执行')
func3() # 被装饰的函数
print(f'被装饰的函数{func3.__name__}执行结束')
return func2

def funcx():
print('函数正在运行')

a = func1(funcx) # 1
a() # 2

'''
运行结果:
被装饰的函数funcx即将执行
函数正在运行
被装饰的函数funcx执行结束
'''

上面这个例子就是不用@标识符的装饰器,首先我定义了一个函数func1,它只有一个func3参数,这个函数里面定义了一个嵌套函数func2。func2的作用是调用func3前打印一串字符,然后执行被装饰的函数func3,结束之后再打印一串字符。

我们再定义一个测试函数funcx,功能是打印一段“函数正在运行”的字符串。

在1处,函数func1中传入函数funcx,返回函数func2的引用赋值给变量a,此时并没有执行函数,也不会有打印结果。在2处执行了func2函数,前面传入的函数funcx作为参数在原先的func3处执行,这个时候就会依次输出三行字符串。

将@标识符应用到函数上,只需要在函数定义前加上@和装饰器的名称即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def func1(func3):

def func2():
print(f'被装饰的函数{func3.__name__}即将执行')
func3() # 被装饰函数
print(f'被装饰的函数{func3.__name__}执行结束')
return func2

@func1
def funcx():
print('函数正在运行')

funcx()

'''
运行结果:
被装饰的函数funcx即将执行
函数正在运行
被装饰的函数funcx执行结束
'''

这里@func1就是装饰器,它接受被装饰的函数作为参数传入,返回内部嵌套函数的引用(注意这个时候并没有执行函数),内部嵌套函数func2持有被装饰函数func3的引用。

可以看到@语法只是将函数传入装饰器函数,并不是什么特别难理解的概念,主要作用就是节省代码量(避免了再一次的赋值操作)。

2.2 带参数的装饰器

前面示范的是不带参数的装饰器,带参数的装饰器也是类似的,我们只要知道装饰器最终返回的一定是嵌套函数的引用。在前面的参数传递博文中,我们说过*args和**kargs可以以包裹传递的方式传递不定长参数,这里也是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def func1(func3):
def func2(*args, **kargs):
print(f'被装饰的函数{func3.__name__}即将执行')
func3(*args, **kargs)
print(f'被装饰的函数{func3.__name__}执行结束')
return func2

@func1
def funcx(a, b):
print(a + b)

@func1
def funcy(a, b, c):
print(str(a) + str(b) + str(c))

funcx(1, 2)
print('*************************')
funcy('Phan', 't', 'om')

'''
被装饰的函数funcx即将执行
3
被装饰的函数funcx执行结束
*************************
被装饰的函数funcy即将执行
Phantom
被装饰的函数funcy执行结束
'''

上面的装饰器带的参数都是我们后面自定义函数里的参数,装饰器的语法允许我们在调用时提供其他参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#import functools
def func1(text):
def decorator(func):
# @functools.wraps(func)
def func2(*args, **kwargs):
if text == 'Phantom':
print('%s 正在运行' % func.__name__)
print(*args)
print(text)
return func(*args, **kwargs)
return func2
return decorator

@func1(text = "Phantom")
def funcx(a):
print(funcx.__name__)

funcx('test')

'''
注释的运行结果:
funcx 正在运行
test
Phantom
func2
*************************
去掉注释的运行结果:
funcx 正在运行
test
Phantom
funcx
'''

先不看导入的模块,后面再解释。

上面的例子看上去很复杂,可以一层一层剥开理解。func1是允许带参数的装饰器,实际上是原有装饰器decorator的再一次封装,并且返回了这个装饰器,可以理解为含有一个形参text的闭包。当我们使用@func1(text = “Phantom”)时,python解释器将我们的实参“Phantom”传入到装饰器的环境。

而嵌套函数func2在检查到传入的text参数与字符串“Phantom”相同时,就会执行后面的打印函数名、funcx传入的实参和func1传入到decorate的实参。

通过特殊属性__name__可以看到,funcx函数指向了装饰器内部定义的func2函数,也就是经过装饰器装饰后丢失了原函数的元信息,我们真正调用的是装饰后生成的新函数。那么是不是每次都要使用func2.__name__ = func.__name__这样的代码来保留原函数信息呢?并不是,我们可以使用functools库中的@functools.wraps()来保留原函数的属性,其实这种保留只是将原始被装饰的函数的属性拷贝给了装饰函数,如果不干这件事,有些依赖函数签名的代码执行就会出错,感兴趣的小伙伴可以继续探究~

2.3 内置装饰器

上面说的@functools.wraps()其实也是内置装饰器,下面介绍其他几个常用的内置装饰器。

2.3.1 @property

这个内置装饰器用来装饰类函数,被装饰的类函数不可以在类被实例化后调用,只能通过访问与函数同名的属性进行调用(也就是把类的方法伪装成属性)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A():

def func1(self):
print('Phantom')

@property
def func2(self):
print('Aria')

a = A() # 实例化一个对象

a.func1() # 通过实例化对象访问类方法
a.func2 # 通过实例化对象将类方法伪装成属性调用

'''
运行结果:
Phantom
Aria
'''

我们知道属性是可以被赋值的,但是经过property装饰的方法不可以像普通属性那样被赋值

这个特性很有意思,我们可以实现对python类私有属性的安全访问(再次强调不存在严格意义的私有属性)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class A:
__number = 'Phantom' # 类内的私有属性
@property
def number(self):
return self.__number

a = A()

try:
print(a.__number) # 尝试直接访问类内的私有属性失败
except:
print("访问私有属性失败")
try:
print(a.number) # 通过类方法伪装的属性访问私有属性成功
print("访问私有属性成功")
except:
pass
try:
a.number = 1 # 类方法伪装的属性无法被赋值
except:
print("修改私有属性失败")

'''
运行结果:
访问私有属性失败
Phantom
访问私有属性成功
修改私有属性失败
'''

2.3.2 @classmethod

直接翻译,这个装饰器就是用来定义类方法的,被装饰的函数必须有一个cls参数用来绑定类本身,隐式地将类作为对象,传递给方法,调用地时候不需要进行实例化。

如果不加这个装饰器,必须要使用self参数,隐式地将类实例传递给方法,也就是说必须要实例化。

强调一点,这里地cls和self只是为了方便编程的时候一眼看出来绑定的是类还是对象,都可以用别的xxx名字代替(但是不建议)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A():
def func1(self,x,y): # 实例方法
return x * y

@classmethod
def func2(cls,x,y): # 类方法
return x * y

print(A().func1(5,5)) # 必须实例化A()之后通过实例化对象才可以调用方法
print(A.func2(5,5)) # 不需要实例化,直接通过类对象调用

'''
运行结果:
25
25
'''

由于被classmethod装饰的函数强制暴露了类自身,所以我们可以通过被classmethod装饰的函数对类的静态变量进行一定操作,在实例化之前和类进行交互。还有类方法可以通过实例对象或者类对象去访问,所以有一个用途就是通过实例调用类方法实现对类属性的修改(点击见第三篇博客例子)

2.3.3 @staticmethod

前面博客介绍过,这个装饰器是声明静态方法的,静态方法和上面的类方法一样,不需要实例化就可以直接调用,但是这个方法不强制要求传递参数,无法直接使用任何类变量、类方法或者实例方法、实例变量(这里要注意,只有主动传参才可以调用,因为主动传参是可以按照逻辑去找需要的参数的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class People():
name = 'Phantom'
def __init__(self, name = 'Aria'):
self.name = name

@staticmethod
def getName():
print('静态方法调用类属性', People.name)
#print(self.name) #不能调用实例的属性,会报错,名义上是类方法,实际已经和类无关

p = People()
p.getName() # 可以通过 类.方法名 或者 实例.方法名 进行调用

'''
运行结果:
静态方法调用类属性 Phantom
'''

staticmethod更像是与实例无关但与类封装功能有关的函数,如果有一个功能实现的方法比较独立,可以考虑用静态方法来实现。

在继承类中,staticmethod和classmethod有以下区别

子类的实例继承了父类的@staticmethod静态方法,调用该方法,还是调用的父类的方法和类属性。

子类的实例继承了父类的@classmethod类方法,调用该方法,调用的是子类的方法和子类的类属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class A():
name = 'Phantom'
@staticmethod
def func1():
return print(A.name)

@classmethod
def func2(cls):
return print(cls.name)

class B(A):
name = 'Aria'

a = A()
a.func1()
a.func2()
print('***********************')
b = B()
b.func1()
b.func2()

'''
运行结果:
Phantom
Phantom
***********************
Phantom
Aria
'''

上面这个例子可以看出来,@classmethod装饰后的func1函数实际上已经和父类没什么关系了,尽管在父类方法里但也可以当作是个独立的函数,不管子类的实例化还是父类的实例化都是调用同一个函数,输出结果一致。而@classmethod装饰后的func2函数,cls参数绑定了类本身,子类在实例化后继承了父类@classmethod类方法,但是调用的是子类的方法和类属性。

所有装饰器存在的意义都是为函数扩展功能,总结以下几点:

装饰器通过高级函数、嵌套函数和闭包实现

装饰器返回闭包函数的引用,这个闭包函数引用中有被装饰函数的引用

装饰器通过语法糖 @ 修饰

装饰器不修改原函数和调用方式(调用的是装饰后的新函数)

欢迎小伙伴们留言评论~