win10+Python 3.6.3
Python 装饰器,不再是一道坎,但你得有耐心。
Python三大神器(装饰器、迭代器、生成器)之一:decorator,是用于扩展(增加)原来函数功能的一种函数,它的特殊之处在于:其返回值也是一个函数。就是一个返回函数的高阶函数。
如果没有装饰器,若想给一个函数增加新功能,最直接的办法是 修改(篡改)原函数代码。很多情况下,不允许或不便修改原函数代码,装饰器 应运而生。
是组织好的、可重复使用的,用于实现单一、或相关联功能的代码段。语法如下:
def 函数标识符名称([,参数]):#参数是可选的
函数体(若有return,则将返回一个值给调用者;若没有,将返回None)
示例:
>>> def func():
... return "Life is short,I use Python."
...
>>> func()
'Life is short,I use Python.'
>>> func # 函数也是一种特殊的变量
>>> a_string = "This is a global variable."
>>> def func():#该函数的功能是:打印【局部作用域】(即函数拥有自己独立的命名空间)。虽然目前是空的。
... print(locals())
...
>>> print(globals())#打印【全局作用域】。包括变量a_string。
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': , '__spec__': None, '__annotations__': {}, '__builtins__': , 'a_string': 'This is a global variable.', 'func': }
>>> func()
{}
修改一下:比较一下异同。对于func()
函数而言,在函数体之内是局部作用域。之外是全局作用域。
>>> a_string = "This is a global variable."
>>> def func():
... b_string ="This a local variable."
... print(locals())
...
>>> print(globals())
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': , '__spec__': None, '__annotations__': {}, '__builtins__': , 'a_string': 'This is a global variable.', 'func': }
>>> func()
{'b_string': 'This a local variable.'}
Python作用域规则,创建变量一定会在当前作用域中创建一个变量,但是访问、或修改变量时会先在当前作用域查找变量,若未找到匹配变量,则将依次向上在闭合的作用域里进行查找。所以,若修改func()
的实现,让其打印全局作用域中的变量是可以的:
>>> a_string = "This is a global variable."
>>> def func():
... print(a_string)#1
...
>>> func()
This is a global variable.
在#1
处,Python解释器会优先在函数的局部作用域中查找a_string
变量;当查找不到时,将在它的上层作用域查找。
但是,可在函数内部给全局变量
赋值(本质是 新建一个跟全局变量同名的局部变量
):
>>> a_string = "This is a global variable."
>>> def func():
... a_string = "test"#1
... print(locals())
...
>>> func()
{'a_string': 'test'}
>>> a_string#2
'This is a global variable.'
上述代码中,全局变量
能被访问到(若是可变数据类型(如list、dict),甚至可被更改),但不能赋值。在函数内部#1
处,实则是新建了一个局部变量。可见在#2
处打印出的a_string
变量(全局变量)的值并未改变。
>>> def func():
... x = 1
...
>>> func()
>>> print(x)#1
Traceback (most recent call last):
File "", line 1, in
NameError: name 'x' is not defined
#1
处引发的Error
不仅因为作用域规则导致的(尽管这 是抛出了NameError
的原因),还因为与Python以及其他编程语言中函数调用实现机制有关。在上述这个位置、这个执行时间点 并没有有效的语法
能够获取到变量x
的值,因为它压根不存在。即 函数func()
的命名空间随着函数调用开始而开始、结束而销毁。
Python允许向函数传递parameter
,parameter
会变成局部变量
存在于函数内部。
>>> def func(x):
... print(locals())
...
>>> func(1)
{'x': 1}
在Python中有很多方式来定义、传递参数。在此简要地说明:函数的参数可以是必须的位置参数
或 可选的命名,默认参数
。
>>> def func(x, y=0):#1
... return x - y
...
>>> func(3, 1)#2
2
>>> func(3)#3
3
>>> func()#4
Traceback (most recent call last):
File "", line 1, in
TypeError: func() missing 1 required positional argument: 'x'
>>> func(y=1, x=3)#5
2
上述代码中,#1
定义了函数func()
,有一个位置参数x
、一个命名参数y
。
#2
通过常规方式调用函数,尽管已有一个命名参数,但参数仍可通过位置传递给函数。
在调用函数时,对于命名参数y
,可完全不管它,例如#3
。若命名参数未接收到任何值,Python会自动使用声明的默认值(即 0
)。
不过,不能忽略位置参数x
,否则将引发像#4
所示Error
。
Python支持函数调用时的命名参数(命名实参)。#5
函数调用,传递的是两个命名实参,此时因为有名称标识,参数传递顺序可不必在意。
当然,#2
函数第二个形参y
,是通过位置方式传递值的。尽管它是一个命名参数。
函数的参数 可以有名称
、位置
。
Python允许创建嵌套函数。即可在函数中定义函数、且现有的作用域和变量生存周期依旧适用。
>>> def outer():
... x = 1
... def inner():
... print(x)#1
... inner()#2
...
>>> outer()
1
#1
Python解释器需找一个名为x
的局部变量,若查找失败则会继续在上层作用域查找,在此的上层作用域是定义在外头一个函数outer()
里。
对outer()
函数来说,变量x
是一个局部变量,inner()
函数可访问封闭的作用域(读、修改)。
#2
调用inner()
函数,inner
也是一个遵循Python变量解析规则的变量名
,Python解析器会优先在outer
作用域中对变量名inner
查找匹配的变量。如下可说明 inner
是outer()
作用域中的一个变量:
>>> def outer():
... x = 1
... def inner():
... print(x)
... inner()#2
... print(locals())
...
>>> outer()
1
{'inner': .inner at 0x000001EC707B67B8>, 'x': 1}
>>> issubclass(int, object)
True
>>> def func():
... pass
...
>>> func.__class__
>>> issubclass(func.__class__, object)
True
函数只是一些普通的值而已,跟其他值一样。即可以将函数像参数一样传递给其他函数 或从函数里返回函数。换句话说:在Python中,函数可看作是一个特殊变量。
>>> def add(x, y):
... return x + y
...
>>> def sub(x, y):
... return x - y
...
>>> def apply(func, x, y):#1
... return func(x, y)#2
...
>>> apply(add, 2, 1)#3
3
>>> apply(sub, 2, 1)
1
上述代码中,add
、sub
是两个普通的Python函数,用于实现简单的加减法功能,接收两个值,返回一个计算后的结果值。
#1
接收的是一个函数的变量 只是一个普通的变量而已,和其他变量一样;
#2
调用传递进来的函数func()
,并调用变量包含的值;
#3
可发现传递函数并没有特殊语法。函数名称
只是像其他变量一样的标识符而已。
>>> def outer():
... def inner():
... print("Inside inner.")
... return inner#1
...
>>> func = outer()#2
>>> func
.inner at 0x0000024F27976950>
>>> func()
Inside inner.
在上述代码中:
#1
正是将函数标识符
的变量(名)inner
作为返回值返回;如若不将函数inner
return
,它将根本不被调用到。每次outer()
函数被调用时,innner()
函数都将被重新定义。如果它(inner
)不被当作变量
返回,在每次执行之后,它将不复存在(生命周期)。
#2
捕获返回值(即 函数inner
),将它存放在一个新的变量func
里。当对变量func
进行求值时,它确实包含inner
。还可对其进行调用。
>>> def outer(x):
... def inner():
... print(x)#1
... return inner
...
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
1
>>> print2()
2
如果在一个内部函数(如 inner()
)里,对外部作用域(但不是在全局作用域)的变量(x
)进行引用,那么内部函数(inner()
)就被认为是闭包(closure)。
加糖后的代码功能 与加糖前保持一致,且糖在不改变其所在位置的语法结构前提下,实现了运行时等价,即加糖后的代码编译后 跟加糖前一样。
语法糖,是为了避免编程者出现错误并提供效率的语法层面的一种优雅的解决方案。
实例是:Python中的 @
装饰器符号、*args
和**kwargs
、super()
等。
PS:有个名词叫“语法盐”,与语法糖相反,是让编程者忧伤的语法!!
它可让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。
>>> def outer(some_func):
... def inner():
... print("before some_func")
... ret = some_func()#1
... return ret +1
... return inner
...
>>> def func():
... return 1
...
>>> decorated = outer(func)#2
>>> decorated()
before some_func
2
上述代码中,定义函数outer()
,它只有一个参数some_func
;接着定义了一个内嵌函数inner
,它会打印一行字符串;然后调用some_func
,在#1
得到它(some_func()
)的返回值。在outer()
每次调用时,some_func
的值可能会不一样,但不管some_func
的值如何,都会调用它。最后,inner
返回some_func()+1
的值。
调用在#2
存储在变量decorated
里的函数将打印那行字符串、返回值2
,而不是期望中调用函数func
得到的返回值1
。
在此,变量decorated
是函数func
的一个装饰版本、加强版本。如果打算编写一个有用的装饰器,可用装饰版本完全替代原先的函数func
,如此将得到加强版func
。想达到这个效果,完全不需要新的语法,简单赋值给变量func
就行了:
>>> func = outer(func)
>>> func
.inner at 0x000002BF19946730>
现在,任何调用都不会牵扯到原先的函数func
,都会得到新的装饰版本func
。
综上所述,小结一下:装饰器的作用是为已存在的函数或对象添加额外的功能。
①、装饰器本质上是一个Python函数,它可让其他函数在不需要做任何代码变动的前提下增加额外的功能,它的返回值也是一个函数对象(注意用词:函数对象。不带圆括号)。
②、装饰器适用于有切面需求的场景,比如:插入日志、性能测试、事物处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计。有了它,我们可抽离出大量与函数功能本身无关的雷同代码、并继续重用。
>>> class Coordinate:
... def __init__(self, x, y):
... self.x = x
... self.y = y
... def __repr__(self):
... return "Coord:"+str(self.__dict__)
...
接下来将做一系列数学运算,对两个坐标对象进行加减运算,这个方法很容易写出:
>>> def add(a, b):#a,b分别表示两个坐标对象,分别有x、y值。
... return Coordinate(a.x +b.x, a.y +b.y)
...
>>> def sub(a,b):
... return Coordinate(a.x -b.x, a.y -b.y)
...
>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> add(one, two)
Coord:{'x': 400, 'y': 400}
假设还需要增加边界检查的行为,怎么办??
>>> three = Coordinate(-100, -100)
>>> sub(one, two)
Coord:{'x': -200, 'y': 0}
>>> add(one, three)
Coord:{'x': 0, 'y': 100}
在不更改坐标对象one
、two
、three
的前提下:one
减去two
的值是{'x':0, 'y':0}
;one
加上three
的值是{'x':100, 'y':200}
。与其给每个方法都加上参数和返回值边界检查的逻辑,下方将编写一个边界检查的装饰器:
>>> def wrapper(func):
... def checker(a, b):#1
... if a.x <0 or a.y <0:
... a = Coordinate(a.x if a.x>0 else 0, a.y if a.y >0 else 0)
... if b.x <0 or b.y <0:
... b = Coordinate(b.x if b.x>0 else 0, b.y if b.y >0 else 0)
... ret = func(a, b)
... if ret.x <0 or ret.y <0:
... ret = Coordinate(ret.x if ret.x >0 else 0, ret.y if ret.y >0 else 0)
... return ret
... return checker
...
>>> add = wrapper(add)
>>> sub = wrapper(sub)
>>> sub(one, two)
Coord:{'x': 0, 'y': 0}
>>> add(one, three)
Coord:{'x': 100, 'y': 200}
上述代码的装饰器 像之前的装饰器例子一样进行工作,返回一个经过修改的函数。但在此例中,它能够对函数输入的参数、返回值做一些有用的检查、格式化工作,将负值的x
、y
替换成0
。
很明显,通过这样的方式,代码变得更加简洁:将边界检查的逻辑隔离到单独的方法中;然后通过装饰器包装的方式应用到我们需要进行检查的地方。另外一种方式是通过在计算方法的开始处、返回值之前调用边界检查的方法也能够达到同样的目的。但不可置否的是,使用装饰器能够让我们以最小的代码量达到坐标边界检查的目的。
@
标识符将装饰器应用到函数@
将装饰器应用在函数上,只需要在函数的定义前加上
@
和
装饰器名称
。上方例子中是将原本的方法用装饰后的方法代替:
>>> add = wrapper(add)
上述方法能在任何时候对任意方法进行包装。但是如果我们自定义一个方法,可以使用@
进行装饰:
>>> @wrapper
... def add(a, b):
... return Coordinate(a.x + b.x, a.y + b.y)
上述两种方法效果是一样的。Python只是加了一些语法糖(即 @
) 将装饰的行为更加直接明确、优雅。
*args
和**kwargs
可变参数实例:实现一个能应用在任何方法上的类似于计数器的装饰器,不需要更变原有方法的任何逻辑。即意味着装饰器能够接受拥有任何签名的函数作为自己的被装饰方法,同时能够用传递给它的参数对被装饰的方法进行调用。
正巧,Python有支持这个特性的语法(官方指导)。在定义函数时,若使用了*
,那么对于通过位置传递的参数将会被放在带有*
前缀的变量中。
>>> def one(*args):
... print(args)#1
...
>>> one()
()
>>> one(1, 2, 3)
(1, 2, 3)
>>> def two(x, y, *args):#2
... print(x, y, args)
...
>>> two("a", "b", "c")
a b ('c',)
函数one()
只是简单地将任何传递进的位置参数全部打印出来。#1
只是引用了函数内的变量args
,*args
仅是用在函数定义时用于表示位置参数应该存储在变量args
中。Python允许编程者制定一些参数并通过args
捕获其他所有剩余的未被捕获的位置参数。如#2
所示。
*
操作符 在函数被调用时也能使用。意义是一样的。当调用一个函数时,一个用*
标志的变量 意味着变量里内容需要被提取出来,然后当作位置参数被使用。举例:
>>> def add(x, y):
... return x + y
...
>>> lst = [1, 2]
>>> add(lst[0], lst[1])#1
3
>>> add(*lst)#2
3
#1
、#2
所做的事情其实是一样的。*args
要么是表示调用方法时额外的参数可从一个可迭代列表中取得;要么是定义方法时标志该方法可接受任意的位置参数。
**
代表键值对的参数字典,和*
所代表的意义相差无几。
>>> def func(**kwargs):
... print(kwargs)
...
>>> func()
{}
>>> func(x=1, y=2)
{'x': 1, 'y': 2}
当我们定义一个函数时,可用**kwargs
来表明,所有未被捕获的关键字参数都应该存储在kwargs
的字典中。args
和 kwargs
并不是Python语法的一部分,但在定义函数时,使用这样的变量名是一个不成文的约定。和*
一样,同样可在定义或者调用函数时候使用**
。
>>> dct = {"x":1, "y":2}
>>> def bar(x, y):
... return x + y
...
>>> bar(**dct)
3
>>> def logger(func):
... def inner(*args, **kwargs):#1
... print("Arguments were:%s, %s" % (args, kwargs))
... return func(*args, **kwargs)#2
... return inner
...
函数inner()
可接受任意数量、类型的参数,并将它们传递给被包装的方法,这使得我们能用这个装饰器来装饰任何方法。
>>> @logger
... def func1(x, y=1):
... return x * y
...
>>> @logger
... def func2():
... return 2
...
>>> func1(5, 4)
Arguments were:(5, 4), {}
20
>>> func1(1)
Arguments were:(1,), {}
1
>>> func2()
Arguments were:(), {}
2
装饰器本身也支持参数。达到这个目的就需要多一层嵌套函数 即可。装饰器传参就相当于三层函数嵌套,在闭包的外面包裹一层函数用于处理传入的参数。
实例1:进入某函数后打印出log信息、且要指定log的级别。
def logging(level):
def wrapper(func):
def inner_wrapper(*args, **kwargs):
print("[%s]:enter function %s" % (level, func.__name__))
return func(*args, **kwargs)
return inner_wrapper
return wrapper
@logging(level="INFO")
def say(something):
print("Say %s" % something)
@logging(level="DEBUG")
def do(something):
print("Do %s..." % something)
if __name__ == "__main__":
say("hello!")
do("my work.")
运行结果:
[INFO]:enter function say
Say hello!
[DEBUG]:enter function do
Do my work....
实例2:通过装饰器的参数实现是否使用计时功能。
import time
def timer(bool=True):
if bool:
def _timer(func):
def wrapper(*args, **kwargs):
startTime = time.time()
func(*args, **kwargs)
endTime = time.time()
msecs = (endTime - startTime) *1000
print('->elapsed time:%f ms.' % msecs)
return wrapper
else:
def _timer(func):
return func
return _timer
@timer(False)
def myFunc():
print('start myFunc.')
time.sleep(1)
print('end myFunc.')
@timer(True)
def add(a, b):
print('start add.')
time.sleep(1)
print('result is %d.' % (a+b))
print('end add.')
print('myFunc is %s.' % myFunc.__name__)
myFunc()
print('add is %s.' % add.__name__)
add(1, 2)
运行结果:
myFunc is myFunc.
start myFunc.
end myFunc.
add is wrapper.
start add.
result is 3.
end add.
->elapsed time:1000.015736 ms.
等价于add(1, 2) = timer(True)(add(1, 2))
、myFunc() = timer(False(myFunc())
。
其中,作为参数的func
函数 是在返回函数wrapper()
内执行的;然后在add()
前面加上@timer(True)
,add()
函数就相当于被注入了计时功能,今后只要调用add()
,它就变身为“新的、功能更多的”函数了。
装饰器函数 其实是这样一个接口约束:它必须接受一个callable
对象作为参数,然后返回一个callable
对象。Python中一般callable对象都是函数,但也有例外。只要某个对象重载了__call__()
方法,那么这个对象就是callable
。
class Test:
def __call__(self):
print("call me!")
t = Test()
t()
运行结果:
call me!
如同上述代码中__call__
前后带2个下划线的方法在Python中称为内置方法(魔法方法)。重载这些魔法方法一般会改变对象的内部行为,上述示例作用是:让一个对象拥有被调用的行为。
装饰器要求接受一个callable对象,并返回一个callable
对象,也就说:用类来实现装饰器也是OK的。即让类的构造函数__init__()
接受一个函数(需要装饰的函数),然后重载__call__()
并返回一个函数,以达到装饰器函数的效果。
class Logging:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("[DEBUG]:enter function %s()." % self.func.__name__)
return self.func(*args, **kwargs)
@Logging
def say(something):
print("Say %s!" % something)
if __name__ == '__main__':
say('hello')
运行结果:
[DEBUG]:enter function say().
Say hello!
带参数的类装饰器
通过类形式实现带参数的装饰器:在构造函数中接受传入的参数,通过类把参数保存起来;然后在重载__call__()
方法,接受一个函数并返回一个函数。
class Logging:
def __init__(self, level="INFO"):
self.level = level
def __call__(self, func):#接受函数
def wrapper(*args, **kwargs):
print("[%s]:enter function %s()." % (self.level, func.__name__))
func(*args, **kwargs)
return wrapper#返回函数
@Logging(level="INFO")
def say(something):
print("Say %s!" % something)
if __name__ == '__main__':
say('hello')
运行结果:
[INFO]:enter function say().
Say hello!
装饰器是可以叠加使用的,这就涉及到装饰器调用顺序问题。对于Python @
语法糖,装饰器的调用顺序 与使用 @
语法糖声明的顺序相反。
即下方使用装饰器的地方等价于:add(1, 2) = deco_1(deco2(add(1, 2)))
@deco_1
时,它的下方并非一个函数,无法进行装饰;@deco_2
会对add()
进行装饰,返回的是 add = deco_2(add) = wrapper_2
;@deco_1
下方是一个函数了,即已是wrapper_2
的引用,同理add = deco_1(wrapper_2) = wrapper_1
;add()
进行调用时的顺序是 调用@deco_1
中的wrapper_1
,运行其中的func()
,在func()
中调用wrapper_2
,运行其中的func()
指向原先的add()
。多个装饰器执行顺序是:从最后一个装饰器开始,执行到第一个装饰器,再执行函数本身。
def deco_1(func):
print('enter into deco_1.')
def wrapper_1(a, b):
print('enter into deco_1_wrapper_1.')
func(a, b)
return wrapper_1
def deco_2(func):
print('enter into deco_2.')
def wrapper_2(a, b):
print('enter into deco_2_wrapper_2.')
func(a, b)
return wrapper_2
@deco_1
@deco_2
def add(a, b):
print('result is %d.' % (a+b))
add(1, 2)
运行结果:
enter into deco_2.
enter into deco_1.
enter into deco_1_wrapper_1.
enter into deco_2_wrapper_2.
result is 3.
@a
@b
@c
def func():
pass
等价于 a(b(c(func)))
。
Python 内置装饰器有3个:@property
、@staticmethod
、@classmethod
@property
property,译作 属性。在Python中,表示可以通过类实例直接访问的信息。使调用类中的方法像引用类中的字段属性一样。被修饰的特性方法,内部可以实现处理逻辑,但对外提供统一的调用方式。遵循了统一访问的原则。
属性有3个装饰器:getter
,setter
,deleter
,都是基于property()
进行的封装,因为setter
、deleter
是property()
的第二、三个参数,不能直接套 用@
语法。getettr
装饰器 与不带getter
的属性装饰器效果是一样的(为了凑数?本身没有任何存在的意义)。经过@property
装饰过的函数返回的不再是一个函数,而是一个property
对象。
参考官方文档:@property
场景1:定义一个屏幕对象(宽、高、分辨率)。在未接触到@property
前,是这么编写的:
class Screen:
def __init__(self):
self.width = 1024
self.height = 768
s = Screen()
print(s.width, s.height)
运行结果:
1024 768
但在实际编码中可能会产生一个严重的问题,__init__()
中定义的属性是可变的,In other words,其他开发人员在知道属性名的情况下,可进行随意更改(如 无意识情况下),这可能造成严重后果。
class Screen:
def __init__(self):
self.width = 1024
self.height = 768
s = Screen()
s.height = 7.68#无意中点了小数点
print(s.width, s.height)
上述代码打印的 s.height
结果将是7.68。可能难以排查到此错误。
解决方案:将width
、height
属性都设为私有,其他人不可随意更改。@property
就起到此作用。
class Screen:
@property
def width(self):
#变量名 不跟方法名相同,改为_width
return self._width
@property
def height(self):
return self._height
s = Screen()
s.width = 1024#与方法名一致
s.height = 768
print(s.width, s.height)
@property
使方法 像属性一样调用,如同一种特殊的属性。而此时,若要在外部给width
或height
重新直接赋值将报错 AtributeError
,这样保证了属性的安全性。即 read only(只读属性)。
若真想对属性进行操作,则提供了封装方法的方式进行属性的修改:
class Screen:
@property
def width(self):
#变量名 不跟方法名相同,改为_width。下同
return self._width
@width.setter
def width(self, value):
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
self._height = value
@height.deleter
def height(self):
del self._height
s = Screen()
s.width = 1024#与方法名一致
s.height = 768
print(s.width, s.height)
上述代码中,就可以对属性进行赋值操作、删除属性操作。
场景2:在定义数据库字段类时,可能需要对其中的类属性做一些限制,一般用get
、set
方法来写,那么在Python中该如何以尽量少的代码优雅地实现想要的限制、减少错误的发发生。
举例:定义一个学生成绩表
class Student:
def get_score(self):
return self._score
def set_score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0~100!')
self._score = value
一般情况下,是这么调用的:
>>> s = Student()
>>> s.set_score(60)
>>> s.get_score()
60
>>> s.set_score(200)
Traceback (most recent call last):
File "", line 1, in
s.set_score(200)
File "D:\ATP\common\test.py", line 8, in set_score
raise ValueError('score must between 0~100!')
ValueError: score must between 0~100!
懒人思维(哈哈),为了方便、节省时间,不想写s.set_score(200)
,直接写s.score = 200
更快:@property
出场!修改代码如下:
class Student:
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0~100!')
self._score = value
根据上述代码,将get
方法变为属性只需加上@property
装饰器即可,此时,@property
本身又会创建另外一个装饰器(@score.setter
),它负责将set
方法变成给属性赋值,这个操作后,调用将变得可控、方便:
>>> s = Student()
>>> s.score = 60#实际转化为s.set_score(60)
>>> s.score#实际转化为s.get_score()
60
>>> s.score = 200
Traceback (most recent call last):
File "", line 1, in
s.score = 200
File "D:\ATP\common\test.py", line 11, in score
raise ValueError('score must between 0~100!')
ValueError: score must between 0~100!
综上,@property
提供了可读、可写、可删除操作;若要实现只读属性,只需定义@property
即可,不定义代表禁止其他操作。@property
就是负责将一个方法变成属性调用的。
@classmethod、@staticmethod (类方法、静态方法)
@classmethod
、@staticmethod
两个装饰器的原理差不多:但具体使用上有细微差别。
self
没关系,self
也只是一个普通的参数而已)都是对象的绑定方法,对象在调用绑定办法时会自动将自己作为参数传递给方法的第一个参数。除此之外有两种常见的方法:静态方法、类方法,二者皆为类量身定制,但实例非要使用(不建议),也不会报错。是一种普通函数,位于类定义的命名空间中,不会对任何实例类型进行操作。@classmethod
返回一个classmethod
类对象,与成员方法的区别在于其必须使用类对象作为第一个参数(即 所接收的第一个参数一定是一个类),是将类本身作为操作的方法。类方法被哪个类调用,就传入哪个类作为第一个参数进行操作。用于指定一个类的方法为类方法,而没有它时指定的类为实例方法。@staticmethod
返回一个staticmethod
类对象,类静态方法,其跟成员方法的区别是没有self
参数(可以不传递任何参数),并且可以在类不进行实例化的情况下调用,可以通过类名直接引用到达将函数功能与实例解绑的效果。两者调用的是各自的__init__()
构造函数。
class classmethod:
'''
classmethod(function) ->method
'''
def __init__(self, function):#for @classmethod decorator
pass
class staticmethod:
'''
staticmethod(function) ->method
'''
def __init__(self, function):#for @staticmethod decorator
pass
装饰器的 @
语法糖 等同于调用了这两个类的构造函数:
class Coordinate:
@staticmethod#装饰器
def coo(x, y, z):
print(x, y, z)
#等价于 coo = staticmethod(coo)
print(type(Coordinate))#类型本质就是函数
Coordinate.coo(1, 2, 3)#调用时 函数应该有几个参数就传递几个参数
运行结果:
1 2 3
上述装饰器接口定义可更加明确一些,装饰器必须接受一个callable
对象,其实它并不关心你返回什么,可以是另外一个callable
对象(大部分情况下),也可以是其他类对象(如 property
)。
场景:一个处理日期信息的类。用于存储日期信息(不考虑时区,用UTC表示)
class Date:
day, month, year = 0, 0, 0
def __init__(self, day=0, month=0, year=0):
self.day = day
self.month = month
self.year = year
上述代码使用了典型的类实例初始化方法__init__
,它作为典型的instancemethod
接受参数,其中第一个参数传递的必要参数是新建的实例本身。
场景递进:假如有很多('dd-mm-yyyy')
格式字符串的日期信息 需要被创建成Date
类实例。常规方法是不得不在项目的不同地方完成此事:
Date
。得到:
day, month, year = map(int, string_date.split('-'))
date = Date(day, month, year)
优雅的Python使用@classmethod
,一种另类的构造函数:
class Date:
day, month, year = 0, 0, 0
def __init__(self, day=0, month=0, year=0):
self.day = day
self.month = month
self.year = year
@classmethod
def from_string(cls, date_as_string):#第一个参数cls,表示调用当前类名
day, month, year = map(int, date_as_string.split('-'))
date = cls(day, month, year)
return date#返回一个初始化后的类
def print_date(self):
print('Year:%s, Month:%s, Day:%s.' % (self.year, self.month, self.day))
date = Date.from_string('29-10-2018')#使用@classmethod修饰的方法是类专属的,可直接通过类名调用。【不需要实例化类!!!用起来方便。】
date.print_date()
运行结果:
Year:2018, Month:10, Day:29.
解析上述代码、以及其优势:
date = Date.from_string('29-10-2018')
相当于先调用from_string()
对字符串解析出来,然后再使用Date
的构造函数初始化。@classmethod
。@classmethod
方法。这样直接调用这个方法,可以完成构造函数的工作。cls
表示类对象,而不是类实例。This is very cool,因为如果继承Date类,那么所有的子类也将拥有from_string()
方法。@staticmethod
与@classmethod
非常相似,但不强制要求传递参数,不过做的事与类方法或实例方法一样。
场景再递进:以某种方式验证日期字符串。与之前一样要求定义在Date
类内部,但不要求实例化它。
class Date:
day, month, year = 0, 0, 0
def __init__(self, day=0, month=0, year=0):
self.day = day
self.month = month
self.year = year
@classmethod
def from_string(cls, date_as_string):#第一个参数cls,表示调用当前类名
day, month, year = map(int, date_as_string.split('-'))
date = cls(day, month, year)
return date#返回一个初始化后的类
def print_date(self):
print('Year:%s, Month:%s, Day:%s.' % (self.year, self.month, self.day))
@staticmethod
def is_date_valid(date_as_string):#用Date.is_date_valid()的形式产生实例
day, month, year = map(int, date_as_string.split('-'))
return day <= 31 and month <=12 and year <=3999
date = Date.from_string('29-10-2018')
date.print_date()
is_date = Date.is_date_valid('29-10-2018')
print(is_date)
运行结果:
Year:2018, Month:10, Day:29.
True
对于@staticmethod
的使用,不需要访问它所属的类,它本质上就是一个函数,调用方式跟调用函数一样,不同之处在于 它不关注对象和对象内部属性。
小结,使用场景:
@classmethod
主要用途是作为构造函数,在工厂模式下使用较多,即OOP继承时使用。Python只有一个构造函数__new__
,若想要多种构造函数时就不方便了,只能在new里写一堆if isinstance
。使用@classmethod
就可以方便地编写不同的构造函数。@staticmethod
主要用于限定namespace,有利于组织代码和命名空间整洁。一般情况下可替换为外部的函数,后者继承时不可更改。虽然这个静态方法是个普通的function,但它只有这个class会用到,不适合作为module level function。def html_tags(tag_name):
print('begin outer function.')
def wrapper_(func):
print('begin of inner wrapper function.')
def wrapper(*args, **kwargs):
content = func(*args, **kwargs)
print('<%s>%s%s>' % (tag_name, content, tag_name))
print('end of inner wrapper function.')
return wrapper
print('end of outer function.')
return wrapper_
@html_tags('b')
def hello(name='David'):
return 'Hello %s' % name
hello()
hello()
在装饰器中各个可能的位置上加上了print()
语句,用于记录被调用的情况。能预计出打印顺序吗?如果不能,那么最好不要在装饰器函数之外添加逻辑功能,否则这个装饰器就不受我们控制了。输出结果:
begin outer function.
end of outer function.
begin of inner wrapper function.
end of inner wrapper function.
Hello David
Hello David
错误的函数签名和文档
PS:函数签名,表示调用函数的方式,即定义了函数的输入和输出(函数的声明信息,包括参数、返回值、调用约定之类。)。在Python中,可以使用标准库inspect的一些方法或类,来操作或创建函数签名。
装饰器装饰过的函数 看上去 表面名字没变,其实已经变了。
def logging(func):
def wrapper(*args, **kwargs):
"""print log before a function."""
print('[DEBUG] %s:enter %s().' % (datetime.now(), func.__name__))
return func(*args, **kwargs)
return wrapper
@logging
def say(something):
"""say something"""
print('say %s!' % something)
print(say.__name__)#wrapper
输出结果:
wrapper
为何上述打印的是 wrapper
??
其实回顾装饰器的 @
语法糖 就明白了。@
等价于 say = logging(say)
。logging
返回的函数名字就是wrapper
,上述语句正是把这个结果赋值say
,因而say
的__name__
自然是wrapper
了。不仅是name
,其他属性都来自wrapper
,如doc
、source
。
解决方案:使用标准库中的functools.wraps
:
from functools import wraps
def logging(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""print log before a function."""
print('[DEBUG] %s:enter %s().' % (datetime.now(), func.__name__))
return func(*args, **kwargs)
return wrapper
@logging
def say(something):
"""say something"""
print('say %s!' % something)
print(say.__name__)#wrapper
print(say.__doc__)
运行结果:
say
say something
顺利解决。
import inspect
print inspect.getargspec(say) # failed
print inspect.getsource(say) # failed
但是函数签名、源码是拿不到的,除非借助第三方包 wrapt
。
不能装饰 @staticmethod
或@classmethod
如若将装饰器用在一个静态方法 或 类方法中,则将报错:
class Car:
def __init__(self, model):
self.model = model
@logging #装饰实例方法,OK
def run(self):
print("%s is running!" % self.model)
@logging #装饰静态方法,Failed
@staticmethod
def check_model_for(obj):
if isinstance(obj, Car):
print("The model of your car is %s" % obj.model)
else:
print("%s is not a car!" % obj)
运行将报:AttributeError: 'staticmethod' object has no attribute '__module__'
回顾一下,@staticmethod
装饰器返回的是一个staticmethod
对象,而不是callable
对象。这是不符合装饰器要求的(如 传入一个callable
对象),自然而然不可在其上方添加其他装饰器。
解决方案:将@staticmethod置前,因为装饰返回一个正常的函数,然后再加上@staticmethod就没问题了。
class Car(object):
def __init__(self, model):
self.model = model
@staticmethod
@logging # 在@staticmethod之前装饰,OK
def check_model_for(obj):
pass
装饰器的理念是对原函数、对象的加强,相当于重新封装,所以一般装饰器函数都命名为wrapper()
,译作 包装。函数只有在被调用时才会发挥其作用。如 @logging
装饰器可在函数执行时额外输出日志;@cache
装饰过的函数可计算缓存结果。