Python 3.X | decorator/装饰器,不是一道难迈的坎。

win10+Python 3.6.3

Python三大神器(装饰器、迭代器、生成器)之一:decorator是用于扩展原来函数功能的一种函数,它的特殊之处在于:其返回值也是一个函数。

一、在深刻理解、熟练应用decorator之前,先理解几个概念

1、function(函数)

是组织好的、可重复使用的,用于实现单一、或相关联功能的代码段。语法如下:

def 函数标识符名称([,参数]):#参数是可选的
	函数体(若有return,则将返回一个值给调用者;若没有,将返回None)

示例:

>>> def func():
...     return "Life is short,I use Python."
...
>>> func()
'Life is short,I use Python.'

2、scope(作用域)

在Python中,创建一个函数,它将拥有自己的作用域(命名空间)。即 在函数内部碰到一个variable时,函数会优先在自己的命名空间中查找。
>>> a_string = "This is a global variable."
>>> def func():#该函数的功能是:打印【局部作用域】(即函数拥有自己独立的命名空间)。虽然目前是空的。
...     print(locals())
...
>>> print(globals())#打印【全局作用域】。包括变量a_string。
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'a_string': 'This is a global variable.', 'func': <function func at 0x000002430BC02E18>}
>>> 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__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'a_string': 'This is a global variable.', 'func': <function func at 0x0000025AFFEE2E18>}
>>> func()
{'b_string': 'This a local variable.'}

3、variable resolution rules(变量解析规则)

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变量(全局变量)的值并未改变。

4、variable lifetime(变量生存周期)

值得注意的是,变量不仅生存在一个个命名空间内,它们还有自己的 生存周期
>>> def func():
...     x = 1
...
>>> func()
>>> print(x)#1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

#1处引发的Error 不仅因为作用域规则导致的(尽管这 是抛出了NameError的原因),还因为与Python以及其他编程语言中函数调用实现机制有关。在上述这个位置、这个执行时间点 并没有有效的语法能够获取到变量x的值,因为它压根不存在。即 函数func()的命名空间随着函数调用开始而开始、结束而销毁。

5、Function parameters(函数参数)

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 "<stdin>", line 1, in <module>
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,是通过位置方式传递值的。尽管它是一个命名参数。

函数的参数 可以有名称位置

6、Nested funcitons(嵌套函数)

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查找匹配的变量。如下可说明 innerouter()作用域中的一个变量:

>>> def outer():
...     x = 1
...     def inner():
...             print(x)
...     inner()#2
...     print(locals())
...
>>> outer()
1
{'inner': <function outer.<locals>.inner at 0x000001EC707B67B8>, 'x': 1}

7、Functions are first class objects in Python(Python世界中 函数是第一类对象)

在Python中,函数 与其他东西一样 都是对象、一切皆对象。解说:
>>> issubclass(int, object)
True
>>> def func():
...     pass
...
>>> func.__class__
<class 'function'>
>>> issubclass(func.__class__, object)
True

函数只是一些普通的值而已,跟其他值一样。即可以将函数参数一样传递给其他函数 或从函数里返回函数

>>> 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

上述代码中,addsub是两个普通的Python函数,用于实现简单的加减法功能,接收两个值,返回一个计算后的结果值。
#1接收的是一个函数的变量 只是一个普通的变量而已,和其他变量一样;
#2调用传递进来的函数func(),并调用变量包含的值;
#3可发现传递函数并没有特殊语法。函数名称只是像其他变量一样的标识符而已。

>>> def outer():
...     def inner():
...             print("Inside inner.")
...     return inner#1
...
>>> func = outer()#2
>>> func
<function outer.<locals>.inner at 0x0000024F27976950>
>>> func()
Inside inner.

在上述代码中:
#1正是将函数标识符的变量(名)inner作为返回值返回;如若不将函数inner return,它将根本不被调用到。每次outer()函数被调用时,innner()函数都将被重新定义。如果它不被当作变量返回,在每次执行之后,它将不复存在(生命周期)。
#2捕获返回值(即 函数inner),将它存放在一个新的变量func里。当对变量func进行求值时,它确实包含inner。还可对其进行调用。

8、Closures(闭包)

>>> def outer(x):
...     def inner():
...             print(x)#1
...     return inner
...
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
1
>>> print2()
2

如果在一个内部函数(如 inner())里,对外部作用域但不是在全局作用域的变量(x)进行引用,那么内部函数(inner())就被认为是闭包(closure)

二、装饰器

真正的主角来了!前面的讲述都是为了更好地深刻理解装饰器。

1、装饰器,其实就是一个闭包,它将一个函数当作参数,然后返回一个替代版函数

>>> 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
<function outer.<locals>.inner at 0x000002BF19946730>

现在,任何调用都不会牵扯到原先的函数func,都会得到新的装饰版本func

综上所述,小结一下:装饰器的作用是为已存在的函数或对象添加额外的功能。
①、装饰器本质上是一个Python函数,它可让其他函数在不需要做任何代码变动的前提下增加额外的功能,它的返回值也是一个函数对象(注意用词:函数对象。不带圆括号)。
②、装饰器适用于有切面需求的场景,比如:插入日志、性能测试、事物处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计。有了它,我们可抽离出大量与函数功能本身无关的雷同代码、并继续重用。


下方编写一个有具体功能的装饰器。 假设有个类,提供类似坐标的对象,仅是一些x、y的坐标对。可惜的是( 假设)这些坐标对象不支持数学运算符,并且我们不能对源代码进行修改,即也不能直接加入运算符的支持。
>>> 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}

在不更改坐标对象onetwothree的前提下: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}

上述代码的装饰器 像之前的装饰器例子一样进行工作,返回一个经过修改的函数。但在此例中,它能够对函数输入的参数、返回值做一些有用的检查、格式化工作,将负值的xy替换成0

很明显,通过这样的方式,代码变得更加简洁:将边界检查的逻辑隔离到单独的方法中;然后通过装饰器包装的方式应用到我们需要进行检查的地方。另外一种方式是通过在计算方法的开始处、返回值之前调用边界检查的方法也能够达到同样的目的。但不可置否的是,使用装饰器能够让我们以最小的代码量达到坐标边界检查的目的。

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

Python 2.4版本以上,支持使用标识符 @将装饰器应用在函数上,只需要在函数的定义前加上 @装饰器名称。上方例子中是将原本的方法用装饰后的方法代替:
>>> 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的字典中。argskwargs并不是Python语法的一部分,但在定义函数时,使用这样的变量名是一个不成文的约定。和*一样,同样可在定义或者调用函数时候使用**

>>> dct = {"x":1, "y":2}
>>> def bar(x, y):
...     return x + y
...
>>> bar(**dct)
3

2、更通用的装饰器

现在可以很轻松地编写一个能 记录下传递给函数的参数的装饰器了,并将其输出到界面。
>>> 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

3、重点:更高级的装饰器

带参数的装饰器、类装饰器,属于进阶内容。

3.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....

3.2、基于类实现的装饰器

装饰器函数 其实是这样一个接口约束:它必须接受一个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!

3.3、Python 内置装饰器

内置装饰器 和我们自定义的装饰器(普通装饰器)实现原理是一样的,不过,前者返回类对象,不是函数。

Python 内置装饰器有3个:@property@staticmethod@classmethod

3.3.1、@property

不使用装饰器如何编写一个属性:
def getx(self):
	return self._x

def setx(self, value):
	self._x = value

def delx(self):
	del self._x

#create a property
x = property(getx, setx, delx, "I am doc for x property.")

有了 @ 语法糖,超级简单了:

@property
def x(self): ...

# 等同于

def x(self): ...
x = property(x)

属性有3个装饰器:settergetterdeleter,都是基于property()进行的封装,因为setterdeleterproperty()的第二、三个参数,不能直接套 用@ 语法。getettr装饰器 与不带getter的属性装饰器效果是一样的(为了凑数?本身没有任何存在的意义)。经过@property装饰过的函数返回的不再是一个函数,而是一个property对象。

>>> property()
<property object at 0x0000027674C77778>

3.3.2、@staticmethod、@classmethod

@staticmethod@classmethod两个装饰器的原理差不多:前者返回一个staticmethod类对象;后者返回一个classmethod类对象。两者调用的是各自的__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 Func:
	@staticmethod
	def bar():
		pass
	#等价于 bar = staticmethod(bar)

上述装饰器接口定义可更加明确一些,装饰器必须接受一个callable对象,其实它并不关心你返回什么,可以是另外一个callable对象(大部分情况下),也可以是其他类对象(如 property)。

3.4、装饰器也有坑

至此,看到的都是讲述装饰器的优点:让代码更加优雅;减少重复。 但天下没有完美之物,使用不当,也会带来一些问题。
位置错误的代码
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.
<b>Hello David</b>
<b>Hello David</b>

错误的函数签名和文档
装饰器装饰过的函数 看上去 表面名字没变,其实已经变了。

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,如docsource

解决方案:使用标准库中的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

3.5、优化装饰器

嵌套的装饰器函数不太直观,可使用第三方包改进,让装饰器函数可读性更好。
  1. decorator.py模块
  2. wrapt包

装饰器的理念是对原函数、对象的加强,相当于重新封装,所以一般装饰器函数都命名为wrapper(),译作 包装。函数只有在被调用时才会发挥其作用。如 @logging装饰器可在函数执行时额外输出日志;@cache装饰过的函数可计算缓存结果。

猜你喜欢

转载自blog.csdn.net/weixin_38256474/article/details/83349809
今日推荐