Decorator修饰器
我在看mmcv源码的时候,发现库中的mm.Config.fromfile模块中(可能其他模块也有)出现了很多修饰器,因此现在对修饰器的定义以及作用,利用网上的资源进行一个总结。
闭包
先看一下闭包的解释:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
看下面这个例子:
# print_msg是外围函数
def print_msg():
msg = "I'm closure"
# printer是嵌套函数
def printer():
print(msg)
return printer
# 这里获得的就是一个闭包
closure = print_msg()
# 输出 I'm closure
closure()
msg
是一个局部变量,在print_msg
函数执行之后应该就不会存在了,但是inner函数printer
引用了这个变量,将这个局部变量封闭在了嵌套函数中,这样就形成了一个闭包。【inner函数和变量msg形成了一个闭包】
闭包其实就是引用了自由变量的函数,这个函数保存了执行的上下文,可以脱离原本的作用域独立存在。
用函数作Decorator
-
首先来一个demo,看一下decorator的简单用法:
# 参数fn为一个函数 def hello(fn): def wrapper(): print("hello, {}".format(fn.__name__)) # 函数的__name__属性为函数的名字 fn() print("goodbye, {}".format(fn.__name__)) return wrapper @hello def foo(): print("I am foo") foo()
当运行上述代码,会看到如下输出:
hello, foo i am foo goodby, foo
由上可以分析一下这个输出现象:
(1)函数foo前面有一个@hello的“注解”,hello就是我们在上面定义的函数hello;
(2)在hello函数中,其需要一个fn作为参数(这就是用来做回调的函数);
(3)hello函数中返回了一个inner函数(就是函数中的函数)wrapper,这个wrapper回调了传进来的fn,并在回调前后加了两条语句。
这里,“装饰”体现在,hello装饰了foo的每次调用foo().
-
Decorator的本质
装饰器的使用方法很固定:
(1)先定义一个装饰函数(帽子)(也可以用类、偏函数实现)。
(2)再定义业务函数,或者类(人)。
(3)最后把这顶帽子扣在这个人头上。
对于Python 的这个@注解语法糖(语法糖就是程序语言中提供[
奇技淫巧
]的一种手段和方式而已。 通过这类方式编写出来的代码,可读性和易用性都更高,好似糖一般的语法。)来说,当你在用某个@decorator来修饰某个函数func时,如下所示:@decorator def func(): pass
其解释器会解释成下面的语句:
func = dectoror(func)
这就很清楚了,就是把func函数当作参数传到decorator函数中,然后再回调func函数。但这里需要注意一点,这里还有一个赋值语句,把decorator这个函数的返回值赋值回了原来的func函数。因此上面的那个小demo也可以被翻译为:
foo = hello(foo)
这是一条语句,并且还被执行了。在这个demo中,hello(foo)返回了wrapper()函数,所以,其实foo变成了wrapper的一个变量,而后面我们执行foo(),其实变成了执行wrapper,这就是decorator的本质(可以看作foo是wrapper的一个“装饰”)。
-
带参数及多个Decorator
(1)多个decorator:
@decorator_one @decorator_two def func(): pass
相当于:
func = decorator_one(decorator_two(func))
(2)带参数的decorator
@decorator(arg1, arg2) def func(): pass
相当于:
func = decorator(arg1, arg2)(func)
即这里的
decorator(arg1, arg2)
会返回一个“真正的”decorator。例子1:多个decorator:
""" 补充知识:*args和**kwargs 打包(pack):*args是把传入的多个参数打包成元组,**kwargs是把传入的多个关键字参数打包成字典。 拆分(unpack):*args是把打包了的参数拆成单个的,依次赋值给函数的形参,**kwargs是把字典的键值拆成单个的,依次赋值给函数的形参。 """ def makeHtmlTag(tag, *args, **kwds): def real_decorator(fn): css_class = " class='{0}'".format(kwds["css_class"]) if "css_class" in kwds else "" def wrapped(*args, **kwds): return "<"+tag+css_class+">" + fn(*args, **kwds) + "</"+tag+">" return wrapped return real_decorator @makeHtmlTag(tag="b", css_class="bold_css") @makeHtmlTag(tag="i", css_class="italic_css") def hello(): return "hello world" print hello() # 输出: # <b class='bold_css'><i class='italic_css'>hello world</i></b>
在上面这个例子中,我们可以看到:makeHtmlTag有两个参数。所以,为了让 hello = makeHtmlTag(arg1, arg2)(hello) 成功,makeHtmlTag 必需返回一个decorator(这就是为什么我们在makeHtmlTag中加入了real_decorator()的原因),这样一来,我们就可以进入到 decorator 的逻辑中去了—— decorator得返回一个wrapper,wrapper里回调hello。看似那个makeHtmlTag() 写得层层叠叠,但是,已经了解了本质的我们觉得写得很自然。
例子2:带参数的装饰器
def Log(deco_name): def logger(func): def wrapper(*args,**kw): print('Info from {}'.format(deco_name)) print('{} is called...'.format(func.__name__)) func(*args,**kw) return wrapper return logger @Log('logger') def add(x,y): print(x+y) if __name__ == '__main__': add(3,5)
-
用decorator来装饰类:
Python中的单例模式,有一种写法就是用装饰器实现的
def singleton(cls): # cls是类本身,不是实例对象本身 def get_instance(*args, **kw): print('class name:',cls.__name__) #判断是不是已经有了这个类的实例,如果有了,就直接返回实例 if not cls.__name__ in instances: instance = cls(*args, **kw) # 实例化在这里被回调 instances[cls.__name__] = instance return instances[cls.__name__] return get_instance @singleton class User: _instance = None def __init__(self, name): self.name = name if __name__ == '__main__': usr1=User('wyuheng') print('user name:', usr1.name) usr2=User('yohuna') print('user name:',usr2.name)
输出:
class name: User user name: wyuheng class name: User user name: wyuheng
由此看出,User类通过singleton函数的装饰,只能被实例一次。并且注意,当User类被实例化这一行为,被singleton函数给装饰了,并由get_instance函数进行回调来实现。相当于singleton函数装饰了User的每次实例化。
用class作decorator
-
首先还是来看个例子:
class myDecorator(object): def __init__(self, fn): print "inside myDecorator.__init__()" self.fn = fn # __call__的用法 # 该方法的功能类似于在类中重载 () 运算符,使得类实例对象可以像调用普通函数那样,以“对象名()”的形式来调用__call__函数。 def __call__(self): self.fn() print "inside myDecorator.__call__()" @myDecorator def aFunction(): print "inside aFunction()" print "Finished decorating aFunction()" aFunction() # 输出: # inside myDecorator.__init__() # Finished decorating aFunction() # inside aFunction() # inside myDecorator.__call__()
上述这个例子,用类的方式声明了一个decorator。我们可以看到这个类中有两个成员【注意下列方法的调用时机】:
(1)__ init __ (),这个方法是在我们给某个函数进行装饰时被调用,所以需要一个fn的参数,也就是被装饰的函数。
(2)__ call __(),这个方法是在我们调用被装饰的那个函数时被调用的。
从输出来看,在aFunction函数被调用前,就已经被装饰好了,即已经调用了init()函数,然后一调用aFunction,就会回调myDecorator中的call函数。相当于myDecorator装饰了aFunction的每次调用。
Python自带装饰器
python有三种函数装饰器@staticmethod、@classmethod 和 @property,分别是修饰静态方法,类方法,属性方法(把方法当做属性)
-
@staticmethod:
用该装饰器装饰的类方法,可以不通过实例化对象来调用,而直接通过类本身来调用。【相当于C++中的static成员函数】,如下:
class C(object): @staticmethod def f(): print('runoob'); C.f(); # 静态方法无需实例化即可调用
静态方法不能访问类的成员方法和变量。
-
@classmethod
classmethod修饰符对应的函数不需要实例化,不需要 self 参数,但第一个参数需要是表示自身类的 cls 参数,可以来调用类的属性,类的方法,实例化对象等。例子如下
class A(object): bar = 1 def func1(self): print ('foo') @classmethod def func2(cls): # cls表示类本身,self表示实例对象本身 print ('func2') print (cls.bar) cls().func1() # 调用 foo 方法 A.func2() # 不需要实例化
输出:
func2 1 foo
此时,实例化后的对象,如
a=A()
不能访问声明为类方法【@classmethod装饰的】的方法 -
@property
我们可以使用@property装饰器来创建只读属性,@property装饰器会将方法转换为相同名称的只读属性,可以与所定义的属性配合使用,这样可以防止属性被修改。
使用场景:
(1)修饰方法,使方法可以像属性一样访问,函数调用时不用加():
class DataSet(object): @property def method_with_property(self): ##含有@property return 15 def method_without_property(self): ##不含@property return 15 l = DataSet() print(l.method_with_property) # 加了@property后,可以用调用属性的形式来调用方法,后面不需要加()。 print(l.method_without_property()) #没有加@property , 必须使用正常的调用方法的形式,即在后面加()
注意,被@property修饰之后,就不能在调用方法的时候,加()了。
(2)与所定义的属性配合使用,这样可以防止属性被修改。【这个重要】
由于python进行属性的定义时,没办法设置私有属性,因此要通过@property的方法来进行设置。这样可以隐藏属性名,让用户进行使用的时候无法随意修改。
class DataSet(object): def __init__(self): self._images = 1 self._labels = 2 #定义属性的名称 @property def images(self): #方法加入@property后,这个方法相当于一个属性,这个属性可以让用户进行使用,而且用户有没办法随意修改。 return self._images @property def labels(self): return self._labels l = DataSet() #用户进行属性调用的时候,直接调用images即可,而不用知道属性名_images,因此用户无法更改属性,从而保护了类的属性。 print(l.images) # 加了@property后,可以用调用属性的形式来调用方法,后面不需要加()。
这样之后,用户每次以为调用的是images等属性,但其实调用的是方法,这样就可以隐藏真实的属性,防止被外部改变。【真实属性的命名一般在变量名前加一个下划线_】
后言
Python中装饰器的作用无处不在,后续见到具体的用法再做具体的笔记。