装饰器是Python
的语法糖,它可以将复杂的功能封装起来,在外部表现出特别简单的语法和语义。装饰器还可以实现AOP
面向切面的编程。
Python
装饰器的本质,就是闭包。一般谈Python
的闭包,都是指普通的入参,而谈装饰器的时候,入参一定有函数。闭包和装饰器,返回都是函数。函数是代码的最小封装单位,装饰器作用于函数,它不影响函数自身的执行,只是在函数的执行前后增加一些“装饰性”的动作。装饰器被称为Python
的语法糖(Syntax Sugar
),也被视为Python
支持AOP
编程(面向切面编程)的工具。
在函数执行前,增加一行print
打印,代码如下:
def calling_trace(func):
def wrapper():
print('calling', func.__name__)
func()
return wrapper
@calling_trace
def test1():
print('test1 is runing...')
test1()
test1
函数加上了calling_trace
装饰器,相同于test1 = calling_trace(test1)
。以上代码运行效果如下:
calling test1
test1 is runing...
这就是装饰器的概念:不修改已有函数的代码,也不修改已有函数的调用处代码,却达到了丰富函数功能的效果。
如果被装饰的函数有参数,需要在装饰器内部原样复制函数的参数定义。
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)
*args
表示一个tuple
,在函数定义处出现,就是packing
打包调用时的参数,在调用时出现,就是unpacking
展开tuple
。跟**kw
(对应dict
)用法一样。以上程序运行结果如下:
calling test4
test4 is runing...
reture value: 36
calling test4
test4 is runing...
reture value: 158
装饰器本身也可以带参数,通过在装饰器外再使用闭包,给装饰器封装其执行环境,可以使装饰器的功能更强大更灵活,也可以更好的控制函数的执行(比如在某些情况下不执行)。
而带参数的装饰器,在语法上,也就跟闭包区分开来。(应该就是这个原因,闭包和装饰器在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 test5
test5 is runing...
reture value: 6
calling test5
test5 is runing...
reture value: 11
calling test6
not allow to run
函数可以有多个装饰器!多个装饰器的效果,就相当于对函数进行了多层的封装包裹,而不同的装饰器对函数执行的功能影响,完全独立。比如有个装饰器用来控制函数是否能够被执行,另一个装饰器控制函数的所有raise
出来的异常。
@a
@b
@c
def tt(): pass
# tt = a(b(c(tt)))
Python
装饰器函数其实是这样一个接口约束,它必须接受一个callable
对象作为参数,然后返回一个callable
对象。在Python
中一般callable
对象都是函数,但也有例外。只要某个对象重载了__call__()
方法,那么这个对象就是callable
的。
class call_class():
def __call__(self):
print('I am in call_class')
tt = call_class()
tt()
执行结果如下:
I am in call_class
我们判断一个对象(变量和函数都是对象)是否可调用,其实就是判断这个对象是否含有__call__
函数。
像__call__
这样前后都带下划线的方法在Python
中被称为内置方法,有时候也被称为魔法方法。重载这些魔法方法一般会改变对象的内部行为。上面这个例子就让一个类对象拥有了被调用的行为。
回到装饰器上的概念上来,装饰器要求接受一个callable
对象,并返回一个callable
对象,那么用类来实现也是也可以的。我们可以让类的构造函数__init__()
接受一个函数,然后重载__call__()
并返回一个函数,也可以达到装饰器函数的效果。
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
#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)
class calling_trace
与注释掉的那个函数,效果是完全一样的。运行结果为:
calling test5
test5 is runing...
return value: 6
calling test5
test5 is runing...
return value: 11
calling test6
not allow to run
一般类中的函数,都是与对象绑定,其第1个参数习惯上都使用self这个名称(也可以使用别的名称),表示当此函数被调用的时候,对象自己会作为第1个参数(隐式地)传入。不过有趣的是,这些函数在被调用的时候,此对象并没有显式的出现在参数列表中。将对象自身作为第1个参数传入,是python解释器自动完成的动作。
>>> '-'.join('12345')
'1-2-3-4-5'
以上代码,调用了str对象的join函数,join函数的定义如下:
join(self, iterable, /)
Concatenate any number of strings.
The string whose method is called is inserted in between each given string.
The result is returned as a new string.
Example: '.'.join(['ab', 'pq', 'rs']) -> 'ab.pq.rs'
join函数的第1个参数是self,表示对象本身,按上文的示例代码,就是 ‘-’ 这个str对象。不过写出来的代码,参数只有’12345’,没有写出self参数。这里请仔细体会!
被@staticmethod
装饰过的函数,就不再是一般的成员函数了,而是“静态函数”。这类函数在调用的时候:
(1)Python
解释器不会将对象作为第1
个参数传入;
(2)不要通过对象来调用此类函数,要通过类来调用;
(3)因为没有对象传入,这类函数就无法获取与对象绑定的成员信息,也就是不能访问任何一个对象绑定的属性信息,只能通过类名来访问类变量(class variable
),这也是第2
点的原因,要用类来调用静态函数;
(4)此类函数,被封装在类中,只能在此类以及继承此类的范围内访问,外界不可访问,也是一种封装技术。
它是静止的,不会随不同的类和对象而发生变动,只会出现在类以及继承于此类的空间中。
class group():
num = 0
def __init__(self, sn):
self.sn = sn
group.num += 1
@staticmethod
def getTotal():
return group.num
g1 = group('123')
g2 = group('124')
g3 = group('125')
print(group.getTotal()) # >>> 3
@staticmethod
定义的函数,只存在此类或继承此类的空间(namespace
)中,它不接受对象参数,因此它的最佳使用场景,就是为此类以及继承此类的类提供某种计算服务(比如某些属性值的检查),这类计算不需要具体对象的参与,也最好不要去访问类变量(在继承的情况下,类的名称会变),它的计算参数很可能均来自外部输入,但是计算结果为类和对象服务。或者,@staticmethod
就是用一个类来封装一个功能块接口,如果没有继承,它们之间可以通过类名来相互调用。
@classmethod
一样,也是应用于类中的函数,将这些函数转换为类函数。装饰器也是一个函数!内置的装饰器,当然也是内置函数。我们定义的Python
类,内部可以有三种成员函数:
@staticmethod
装饰的静态函数,静态类的成员函数,只能通过明确的类名来访问类中的变量,使用方面受到限制。@classmethod
装饰的类函数,使用比静态函数要广一些,因为类函数的第1
个参数默认为当前类名。由于类函数可以访问类变量(class variable
),使得类函数常常用来提供创建对象不同的接口。class fruit():
num = 0
def __init__(self, price):
self.price = price
self.count()
@classmethod
def count(cls):
cls.num += 1
@classmethod
def float_price(cls, price):
if not isinstance(price, float):
try:
price = float(price)
except: raise
return cls(price)
f1 = fruit(1.234)
print(fruit.num)
f2 = fruit.float_price(1.2345)
print(fruit.num)
f3 = fruit.float_price('1.234')
print(fruit.num)
f4 = fruit.float_price('abcde')
一般情况下,我们实用f1 = fruit(1.234)
的方式创建对象。如果我们需要对入参进行一些限制和判断,甚至转换,就可以考虑实用float_price
这个类函数。请看f2和f3的创建,确定参数没有问题之后,再调用cls(price)创建对象。
cls
是classmethod
类函数的第1
个默认参数,cls
这个名字,与self
一样,只是一个约定俗称的写法。cls(price)
,就跟fruit(price)
一样,内部调用__init__
创建对象。
count
函数也是类函数,用于累加类变量num
,用来计数fruit
对象的个数。上面代码执行效果如下:
1
2
3
Traceback (most recent call last):
File "classmethod.py", line 30, in <module>
f4 = fruit.float_price('abcde')
File "classmethod.py", line 19, in float_price
price = float(price)
ValueError: could not convert string to float: 'abcde'
为什么这个简单的记录对象个数的函数,要用@classmethod
来修饰呢?因为,在有继承关系的时候,可以让你少些很多代码,整体结构也更优雅。
class fruit():
num = 0
def __init__(self, price):
self.price = price
self.count()
@classmethod
def count(cls):
cls.num += 1
@classmethod
def float_price(cls, price):
if not isinstance(price, float):
try:
price = float(price)
except: raise
return cls(price)
class apple(fruit):
@classmethod
def int_price(cls, price):
if not isinstance(price, int):
try:
price = int(price)
except: raise
return cls(price)
f1 = apple(12)
print(apple.num)
f2 = apple.float_price(1.2345)
print(apple.num)
f3 = apple.int_price('12')
print(apple.num)
f4 = apple.int_price('abcde')
现在我们来创建apple
对象,apple
类继承fruit
,只是增加了一个int_price
类函数。同样,我们尝试创建4
个apple
对象,预期输入'abcde'
的时候,创建会失败,以上代码执行结果如下:
1
2
3
Traceback (most recent call last):
File "classmethod.py", line 41, in <module>
f4 = apple.int_price('abcde')
File "classmethod.py", line 30, in int_price
price = int(price)
ValueError: invalid literal for int() with base 10: 'abcde'
结果符合我们的预期,apple
对象的计数也在正常进行,这就要归功于fruit
类中定义的那个count class method
。因为classmethod
函数可以使用第1
个参数,cls
,来访问类变量,因此继承之后,cls
自动就指向了继承后的类。注意,这是@classmethod与@staticmethod不一样的地方。
这类函数是服务于类的,它可以访问类变量,因此在继承之后,依然可以保持对继承后的类的支持。
@property
装饰器,它的作用,就是能够把一个类的成员函数,当成一个属性来访问,访问这个由函数装扮成的属性,表面上看是对属性的直接访问,实质上是在调用函数。
class fruit():
def __init__(self, num, price):
self.num = num
self.price = price
@property
def totalcost(self):
return round(self.num*self.price,2)
ft = fruit(100, 12.34)
print(ft.totalcost) #>>> 1234.0
fruit
类中有一个函数叫totalcost
,被@property
装饰了一下。与你你会看到,代码在访问(调用)totalcost
的时候,并没有带括号,就像是一个普通的属性一下直接访问,但是实际上是在做函数调用。
Python
解释器在访问对象属性的时候,要先判断属性本身属于什么类型,内部会做一些转换,因此加上@property
装饰器后,用访问普通属性的方式调用函数成为了可能。
Python
对象本质上是没有私有成员的,用双下划线的方式定义的成员,也可以通过附加类名的方式来访问。不过这种访问方式应该要被禁止。
Python
提供了一个@property
装饰器,可以更好的控制对对象成员的访问,也可以更好的控制对成员的修改。
class test():
def __init__(self):
self.__foo = 'foo'
self.__bar = 'bar'
@property
def foo(self):
# print('running in foo function')
return self.__foo
t = test()
print(t.foo)
用@property
装饰的foo
函数,就变成了foo
成员变量,访问这个成员变量的时候,触发内部的一个函数执行。此时,foo
这个成员变量只能读,不能写,如果尝试使用t.foo=123
来赋值,会出现错误:AttributeError: can't set attribute
。若想要实现修改内部__foo
成员,可采用以下代码:
class test():
def __init__(self):
self.__foo = 'foo'
self.__bar = 'bar'
@property
def foo(self):
# print('running in foo function')
return self.__foo
@foo.setter
def foo(self, foo_value):
if foo_value < 100:
pass
else:
self.__foo = foo_value
t = test()
print(t.foo)
t.foo = 123
print(t.foo)
注意@foo.setter
这个装饰器的由来。在设置的时候,外部代码还是简简单单地使用等号实现,而触发的是内部的一段代码,这段代码可以实现设置前的检查!
下面的代码,实现对某个成员的删除控制:
class test():
def __init__(self):
self.__foo = 'foo'
self.__bar = 'bar'
@property
def foo(self):
# print('running in foo function')
return self.__foo
@foo.setter
def foo(self, foo_value):
if foo_value < 100:
pass
else:
self.__foo = foo_value
@foo.deleter
def foo(self):
if self.__foo > 200:
self.__foo = 200
t = test()
print(t.foo)
t.foo = 123
print(t.foo)
t.foo = 234
print(t.foo)
del t.foo
print(t.foo)
注意@foo.deleter
这个装饰器。执行del t.foo
语句的时候,同样触发一段自定义代码,上面的代码示例,并没有真正删除foo
这个变量,只是限制它的值。
通过@property
装饰器,以及其衍生出来的一些其它装饰器,我们可以通过简单的语义实现复杂的功能,可以增加代码的可读性。