Maison > Article > développement back-end > Compréhension approfondie de l'utilisation des décorateurs en Python
Parce que les fonctions ou les classes sont des objets, elles peuvent également être transmises. Ce sont des objets mutables et peuvent être modifiés. Le comportement consistant à modifier une fonction ou un objet de classe après sa création mais avant qu'il ne soit lié à un nom est un décorateur.
Il y a deux significations cachées derrière "décorateur" - l'une est que la fonction joue un rôle décoratif, par exemple, effectue un travail réel, et l'autre est une expression attachée à la syntaxe du décorateur, par exemple, le at symbole et Le nom de la fonction décorée.
Les fonctions peuvent être décorées via la syntaxe du décorateur de fonctions :
@decorator # ② def function(): # ① pass
Les fonctions sont définies de manière standard. ①
Utilisez @ comme expression définie comme préfixe de la fonction décorateur②. La partie après @ doit être une expression simple, généralement simplement le nom d'une fonction ou d'une classe. Cette partie est évaluée en premier, et une fois la fonction définie ci-dessous prête, le décorateur est appelé avec l'objet fonction nouvellement défini comme paramètre unique. La valeur renvoyée par le décorateur est attachée au nom de la fonction décorée.
Les décorateurs peuvent être appliqués aux fonctions et aux classes. La sémantique des classes est claire : la définition de la classe est utilisée comme paramètre pour appeler le décorateur, et tout ce qui est renvoyé est affecté au nom décoré.
Avant l'implémentation de la syntaxe du décorateur (PEP 318), la même chose pouvait être accomplie en attribuant des objets de fonction et de classe à des variables temporaires, puis en appelant explicitement le décorateur, puis en attribuant la valeur de retour au nom de la fonction. Cela semble prendre plus de saisie, et il est vrai que le nom de la fonction du décorateur est utilisé deux fois et la variable temporaire est utilisée au moins trois fois, ce qui est facile à commettre des erreurs. L'exemple ci-dessus est équivalent à :
def function(): # ① pass function = decorator(function) # ②
Les décorateurs peuvent être empilés - l'ordre d'application est de bas en haut ou de l'intérieur vers l'extérieur. C'est-à-dire que la fonction d'origine est utilisée comme paramètre du premier paramètre, et tout ce qui est renvoyé est utilisé comme paramètre du deuxième décorateur... Ce que renvoie le dernier décorateur est attaché au nom de la fonction d'origine.
La syntaxe Decorator a été choisie pour sa lisibilité. Étant donné que le décorateur est spécifié avant l’en-tête de la fonction et ne fait évidemment pas partie du corps de la fonction, il ne peut affecter que la fonction entière. Préfixer l'expression avec @ la rend trop évidente pour être ignorée (elle vous crie au visage selon PEP... :)). Lorsque plusieurs décorateurs sont appliqués, placer chacun d’eux sur une ligne différente facilite la lecture.
Remplacement et adaptation de l'objet d'origine
Un décorateur peut soit renvoyer la même fonction ou le même objet de classe, soit un objet complètement différent. Dans le premier cas, le décorateur profite du fait qu'une fonction ou un objet de classe est modifiable pour ajouter des propriétés, comme l'ajout d'une docstring à la classe. Le décorateur peut même faire des choses utiles sans modifier l'objet, comme enregistrer le global. classe décorée dans le registre. Dans le second cas, rien n’est impossible : quand quelque chose de différent remplace la classe ou la fonction décorée, le nouvel objet peut être complètement différent. Cependant, ce n’est pas le but des décorateurs : ils sont censés changer l’objet décoré plutôt que de faire des choses inattendues. Ainsi, lorsqu'une fonction est complètement remplacée par une fonction différente lors de la décoration, la nouvelle fonction appelle généralement la fonction d'origine après une certaine préparation. De même, lorsqu'une classe est décorée dans une nouvelle classe, la nouvelle classe provient généralement de la classe décorée. Lorsque le but d'un décorateur est de faire quelque chose "à chaque fois", comme enregistrer chaque appel à la fonction décorée, seul le deuxième type de décorateur est disponible. En revanche, si la première catégorie est suffisante, il vaut mieux l’utiliser car elle est plus simple.
Implémenter les décorateurs de classes et de fonctions
La seule exigence pour le décorateur est qu'il puisse être appelé avec un seul paramètre. Cela signifie que le décorateur peut être implémenté comme une fonction régulière ou une classe avec une méthode __call__ et, en théorie, même une fonction lambda.
Comparons les fonctions et les méthodes de classe. L'expression du décorateur (la partie après @) peut être simplement le nom. L'approche par nom uniquement est agréable (moins de saisie, aspect soigné, etc.), mais n'est possible que s'il n'est pas nécessaire de personnaliser le décorateur avec des paramètres. Le décorateur de la fonction en cours d'écriture peut être utilisé des deux manières suivantes :
>>> def simple_decorator(function): ... print "doing decoration" ... return function >>> @simple_decorator ... def function(): ... print "inside function" doing decoration >>> function() inside function >>> def decorator_with_arguments(arg): ... print "defining the decorator" ... def _decorator(function): ... # in this inner function, arg is available too ... print "doing decoration,", arg ... return function ... return _decorator >>> @decorator_with_arguments("abc") ... def function(): ... print "inside function" defining the decorator doing decoration, abc >>> function() inside function
Ces deux décorateurs appartiennent à la catégorie du renvoi de la fonction décorée. S'ils souhaitent restituer de nouvelles fonctions, une imbrication supplémentaire est nécessaire, dans le pire des cas, trois niveaux d'imbrication. La fonction
>>> def replacing_decorator_with_args(arg): ... print "defining the decorator" ... def _decorator(function): ... # in this inner function, arg is available too ... print "doing decoration,", arg ... def _wrapper(*args, **kwargs): ... print "inside wrapper,", args, kwargs ... return function(*args, **kwargs) ... return _wrapper ... return _decorator >>> @replacing_decorator_with_args("abc") ... def function(*args, **kwargs): ... print "inside function,", args, kwargs ... return 14 defining the decorator doing decoration, abc >>> function(11, 12) inside wrapper, (11, 12) {} inside function, (11, 12) {} 14
_wrapper est définie pour accepter tous les arguments de position et de mot-clé. Habituellement, nous ne savons pas quels paramètres la fonction décorée acceptera, donc le wrapper crée tout pour la fonction décorée. Une conséquence malheureuse est que les paramètres explicites prêtent à confusion.
Les décorateurs complexes définis comme classes sont plus simples que les décorateurs définis comme fonctions. Lorsqu'un objet est créé, la méthode __init__ est uniquement autorisée à renvoyer None et le type de l'objet créé ne peut pas être modifié. Cela signifie que lorsque le décorateur est défini comme une classe, cela n'a aucun sens d'utiliser la forme sans paramètre : l'objet décoré final n'est qu'une instance de la classe décorée, qui est renvoyée par l'appel du constructeur (constructeur), ce qui n'est pas très utile. En ce qui concerne les décorateurs basés sur des classes où les arguments sont donnés dans les expressions du décorateur, la méthode __init__ est utilisée pour construire le décorateur.
>>> class decorator_class(object): ... def __init__(self, arg): ... # this method is called in the decorator expression ... print "in decorator init,", arg ... self.arg = arg ... def __call__(self, function): ... # this method is called to do the job ... print "in decorator call,", self.arg ... return function >>> deco_instance = decorator_class('foo') in decorator init, foo >>> @deco_instance ... def function(*args, **kwargs): ... print "in function,", args, kwargs in decorator call, foo >>> function() in function, () {}
Les décorateurs écrits en classes se comportent davantage comme des fonctions relatives aux règles normales (PEP 8), donc leurs noms commencent par une lettre minuscule.
En fait, cela ne sert à rien de créer une nouvelle classe qui ne renvoie que la fonction décorée. Les objets doivent avoir un état et ce type de décorateur est plus utile lorsque le décorateur renvoie un nouvel objet.
>>> class replacing_decorator_class(object): ... def __init__(self, arg): ... # this method is called in the decorator expression ... print "in decorator init,", arg ... self.arg = arg ... def __call__(self, function): ... # this method is called to do the job ... print "in decorator call,", self.arg ... self.function = function ... return self._wrapper ... def _wrapper(self, *args, **kwargs): ... print "in the wrapper,", args, kwargs ... return self.function(*args, **kwargs) >>> deco_instance = replacing_decorator_class('foo') in decorator init, foo >>> @deco_instance ... def function(*args, **kwargs): ... print "in function,", args, kwargs in decorator call, foo >>> function(11, 12) in the wrapper, (11, 12) {} in function, (11, 12) {}
Un décorateur comme celui-ci peut tout faire car il peut modifier l'objet et les paramètres de la fonction décorée, appeler ou non la fonction décorée, et enfin modifier la valeur de retour.
复制原始函数的文档字符串和其它属性
当新函数被返回代替装饰前的函数时,不幸的是原函数的函数名,文档字符串和参数列表都丢失了。这些属性可以部分通过设置__doc__(文档字符串),__module__和__name__(函数的全称)、__annotations__(Python 3中关于参数和返回值的额外信息)移植到新函数上,这些工作可通过functools.update_wrapper自动完成。
>>> import functools >>> def better_replacing_decorator_with_args(arg): ... print "defining the decorator" ... def _decorator(function): ... print "doing decoration,", arg ... def _wrapper(*args, **kwargs): ... print "inside wrapper,", args, kwargs ... return function(*args, **kwargs) ... return functools.update_wrapper(_wrapper, function) ... return _decorator >>> @better_replacing_decorator_with_args("abc") ... def function(): ... "extensive documentation" ... print "inside function" ... return 14 defining the decorator doing decoration, abc >>> function <function function at 0x...> >>> print function.__doc__ extensive documentation
一件重要的东西是从可迁移属性列表中所缺少的:参数列表。参数的默认值可以通过__defaults__、__kwdefaults__属性更改,但是不幸的是参数列表本身不能被设置为属性。这意味着help(function)将显式无用的参数列表,使使用者迷惑不已。一个解决此问题有效但是丑陋的方式是使用eval动态创建wrapper。可以使用外部external模块自动实现。它提供了对decorator装饰器的支持,该装饰器接受wrapper并将之转换成保留函数签名的装饰器。
综上,装饰器应该总是使用functools.update_wrapper或者其它方式赋值函数属性。
标准库中的示例
首先要提及的是标准库中有一些实用的装饰器,有三种装饰器:
classmethod让一个方法变成“类方法”,即它能够无需创建实例调用。当一个常规方法被调用时,解释器插入实例对象作为第一个参数self。当类方法被调用时,类本身被给做第一个参数,一般叫cls。
类方法也能通过类命名空间读取,所以它们不必污染模块命名空间。类方法可用来提供替代的构建器(constructor):
class Array(object): def __init__(self, data): self.data = data @classmethod def fromfile(cls, file): data = numpy.load(file) return cls(data)
这比用一大堆标记的__init__简单多了。
staticmethod应用到方法上让它们“静态”,例如,本来一个常规函数,但通过类命名空间存取。这在函数仅在类中需要时有用(它的名字应该以_为前缀),或者当我们想要用户以为方法连接到类时也有用——虽然对实现本身不必要。
property是对getter和setter问题Python风格的答案。通过property装饰的方法变成在属性存取时自动调用的getter。
>>> class A(object): ... @property ... def a(self): ... "an important attribute" ... return "a value" >>> A.a <property object at 0x...> >>> A().a 'a value'
例如A.a是只读属性,它已经有文档了:help(A)包含从getter方法获取的属性a的文档字符串。将a定义为property使它能够直接被计算,并且产生只读的副作用,因为没有定义任何setter。
为了得到setter和getter,显然需要两个方法。从Python 2.6开始首选以下语法:
class Rectangle(object): def __init__(self, edge): self.edge = edge @property def area(self): """Computed area. Setting this updates the edge length to the proper value. """ return self.edge**2 @area.setter def area(self, area): self.edge = area ** 0.5
通过property装饰器取代带一个属性(property)对象的getter方法,以上代码起作用。这个对象反过来有三个可用于装饰器的方法getter、setter和deleter。它们的作用就是设定属性对象的getter、setter和deleter(被存储为fget、fset和fdel属性(attributes))。当创建对象时,getter可以像上例一样设定。当定义setter时,我们已经在area中有property对象,可以通过setter方法向它添加setter,一切都在创建类时完成。
之后,当类实例创建后,property对象和特殊。当解释器执行属性存取、赋值或删除时,其执行被下放给property对象的方法。
为了让一切一清二楚[^5],让我们定义一个“调试”例子:
>>> class D(object): ... @property ... def a(self): ... print "getting", 1 ... return 1 ... @a.setter ... def a(self, value): ... print "setting", value ... @a.deleter ... def a(self): ... print "deleting" >>> D.a <property object at 0x...> >>> D.a.fget <function a at 0x...> >>> D.a.fset <function a at 0x...> >>> D.a.fdel <function a at 0x...> >>> d = D() # ... varies, this is not the same `a` function >>> d.a getting 1 1 >>> d.a = 2 setting 2 >>> del d.a deleting >>> d.a getting 1 1
属性(property)是对装饰器语法的一点扩展。使用装饰器的一大前提——命名不重复——被违反了,但是目前没什么更好的发明。为getter,setter和deleter方法使用相同的名字还是个好的风格。
一些其它更新的例子包括:
functools.lru_cache记忆任意维持有限 参数:结果 对的缓存函数(Python
3.2)
functools.total_ordering是一个基于单个比较方法而填充丢失的比较(ordering)方法(__lt__,__gt__,__le__等等)的类装饰器。
函数的废弃
比如说我们想在第一次调用我们不希望被调用的函数时在标准错误打印一个废弃函数警告。如果我们不想更改函数,我们可用装饰器
class deprecated(object): """Print a deprecation warning once on first use of the function. >>> @deprecated() # doctest: +SKIP ... def f(): ... pass >>> f() # doctest: +SKIP f is deprecated """ def __call__(self, func): self.func = func self.count = 0 return self._wrapper def _wrapper(self, *args, **kwargs): self.count += 1 if self.count == 1: print self.func.__name__, 'is deprecated' return self.func(*args, **kwargs)
也可以实现成函数:
def deprecated(func): """Print a deprecation warning once on first use of the function. >>> @deprecated # doctest: +SKIP ... def f(): ... pass >>> f() # doctest: +SKIP f is deprecated """ count = [0] def wrapper(*args, **kwargs): count[0] += 1 if count[0] == 1: print func.__name__, 'is deprecated' return func(*args, **kwargs) return wrapper
while-loop移除装饰器
例如我们有个返回列表的函数,这个列表由循环创建。如果我们不知道需要多少对象,实现这个的标准方法如下:
def find_answers(): answers = [] while True: ans = look_for_next_answer() if ans is None: break answers.append(ans) return answers
只要循环体很紧凑,这很好。一旦事情变得更复杂,正如真实的代码中发生的那样,这就很难读懂了。我们可以通过yield语句简化它,但之后用户不得不显式调用嗯list(find_answers())。
我们可以创建一个为我们构建列表的装饰器:
def vectorized(generator_func): def wrapper(*args, **kwargs): return list(generator_func(*args, **kwargs)) return functools.update_wrapper(wrapper, generator_func)
然后函数变成这样:
@vectorized def find_answers(): while True: ans = look_for_next_answer() if ans is None: break yield ans
插件注册系统
这是一个仅仅把它放进全局注册表中而不更改类的类装饰器,它属于返回被装饰对象的装饰器。
class WordProcessor(object): PLUGINS = [] def process(self, text): for plugin in self.PLUGINS: text = plugin().cleanup(text) return text @classmethod def plugin(cls, plugin): cls.PLUGINS.append(plugin) @WordProcessor.plugin class CleanMdashesExtension(object): def cleanup(self, text): return text.replace('—', u'\N{em dash}')
这里我们使用装饰器完成插件注册。我们通过一个名词调用装饰器而不是一个动词,因为我们用它来声明我们的类是WordProcessor的一个插件。plugin方法仅仅将类添加进插件列表。
关于插件自身说下:它用真正的Unicode中的破折号符号替代HTML中的破折号。它利用unicode literal notation通过它在unicode数据库中的名称(“EM DASH”)插入一个符号。如果直接插入Unicode符号,将不可能区分所插入的和源程序中的破折号。
更多深入理解Python中装饰器的用法相关文章请关注PHP中文网!