http://blog.youkuaiyun.com/beckel/article/details/4072886
我预计它会成为Python最重要的几个特性之一。而问题是我见过的所有介绍decorators的文章都很容易让人感到困惑。所以我打算在这里给以纠正以正视听。
首先,你得明白使用 “decorator”一词是十分谨慎的决定,因为它可能会让人联想到
Design Patterns(设计模式)一书中的Decorator模式。从某种角度看可能别的词也适用于这个特性,但“decorator”仍是不二之选。
实际上你可以使用Python decorators来实现Decorator模式,但它的威力远不止于此。我想,最和Python decorators接近的就是宏(macros)了。
宏的历史并不短,不过多数人大概都是使用过C的预处理宏。C中宏的问题是:(1) 它们属于另外一个语言(不是C);(2) 其行为有时候很古怪。并且经常和其余C的行为有不一致的情况。
除此之外,很多C++程序员(也包括我)也领教过C++模版的生成能力,并多少使用过一些这个和宏相似的特性。
很多其他语言都有过宏。虽然对其知之不多,但我敢肯定,Python decorators与Lisp宏在能力上不分伯仲。
一个函数decorators用于函数定义,它位于在函数定义之前的一行。例如:
当编译器经过这段代码时,aFunction()被编译然后将结果函数对象传递给myDecorator代码,后者创建一个类函数对象并取代原来的aFunction()。
myDecorator代码长什么样呢?大多数介绍性的例子都将其作为函数给出,但我发现对于decoration机制使用类而非函数来理解decorators会更容易些,而且也更强大。
Decorator所返回的对象唯一的约束是它可以作为函数使用--也就意味着它必须是可调用的。因此,作为decorators使用的任何一个类必须实现__call__。
decorator该做什么呢?它什么都能做,只是你常常期望在某些地方能够使用原来的函数代码。其实没有这个必要:
print "inside myDecorator.__init__()"
f() # Prove that function definition has completed
print "inside myDecorator.__call__()"
print "Finished decorating aFunction()"
Finished decorating aFunction()
有了‘@’这个decoration操作符的加入,你可以这样做:
现在,让我们回过头来实现第一个例子。这里我们实现一些更常规的东西,并在decorated函数中实际使用这些代码:
print "Entering", self.f.__name__
print "Exited", self.f.__name__
现在可以看见,在整个调用期间decorated函数拥有了“Entering”和“Exited”跟踪语句。
构造器保存着自变量,即函数对象。在调用中,我们使用函数的__name__属性来显示函数名称,然后调用函数本身。
new_f()在entryExit()里定义,所以在调用entryExit()时它就创建并返回了。注意,new_f()是一个闭包(closure),因为它获得的是f的实际值。
只要定义过new_f(),它就从entryExit()返回,这样decorator机制就可以将结果作为decorated函数进行赋值了。
‘print func1.__name__’一行的输出是new_f,因为new_f函数在decoration期间已取代原函数。如果还有疑问,可以在函数返回前改变decorator函数的名字:
动态获得有关函数的信息、对函数的修改能力,都是Python的强大之处。
既然你有一定基础了,在这里可以看到更多一些decorators的例子。注意使用类作为decorators的例子数量远多于使用函数的例子。
在本文中我有意避开含有自变量的decorated函数,在下一篇文章中我将重点讨论它。
回顾:不含参数的decorators
在前文中,我介绍了如何使用不含参数的decorators,并使用类来实现。因为我发现这样做更容易接受。
如果创建了一个无参decorator,被decorated的函数被传至构造器,每次调用decorated函数时就会调用__call__()方法:
class decoratorWithoutArguments(object):
def __init__(self, f):
"""
If there are no decorator arguments, the function
to be decorated is passed to the constructor.
"""
print "Inside __init__()"
self.f = f
def __call__(self, *args):
"""
The __call__ method is not called until the
decorated function is called.
"""
print "Inside __call__()"
self.f(*args)
print "After self.f(*args)"
@decoratorWithoutArguments
def sayHello(a1, a2, a3, a4):
print 'sayHello arguments:', a1, a2, a3, a4
print "After decoration"
print "Preparing to call sayHello()"
sayHello("say", "hello", "argument", "list")
print "After first sayHello() call"
sayHello("a", "different", "set of", "arguments")
print "After second sayHello() call"
decorated函数的所有参数会被传至__call__()。输出结果是:
Inside __init__()
After decoration
Preparing to call sayHello()
Inside __call__()
sayHello arguments: say hello argument list
After self.f(*args)
After first sayHello() call
Inside __call__()
sayHello arguments: a different set of arguments
After self.f(*args)
After second sayHello() call
注意,__init__()是唯一一个被调用执行decoration的方法,每次调用decorated的sayHello()时就会调用__call__()。
含有参数的decorators
现在让我们来修改上面的代码,看看向decorator加入参数后结果是什么。
class decoratorWithArguments(object):
def __init__(self, arg1, arg2, arg3):
"""
If there are decorator arguments, the function
to be decorated is not passed to the constructor!
"""
print "Inside __init__()"
self.arg1 = arg1
self.arg2 = arg2
self.arg3 = arg3
def __call__(self, f):
"""
If there are decorator arguments, __call__() is only called
once, as part of the decoration process! You can only give
it a single argument, which is the function object.
"""
print "Inside __call__()"
def wrapped_f(*args):
print "Inside wrapped_f()"
print "Decorator arguments:", self.arg1, self.arg2, self.arg3
f(*args)
print "After f(*args)"
return wrapped_f
@decoratorWithArguments("hello", "world", 42)
def sayHello(a1, a2, a3, a4):
print 'sayHello arguments:', a1, a2, a3, a4
print "After decoration"
print "Preparing to call sayHello()"
sayHello("say", "hello", "argument", "list")
print "after first sayHello() call"
sayHello("a", "different", "set of", "arguments")
print "after second sayHello() call"
从输出结果可以看到,加入参数使程序执行发生了很大变化。
Inside __init__()
Inside __call__()
After decoration
Preparing to call sayHello()
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: say hello argument list
After f(*args)
after first sayHello() call
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: a different set of arguments
After f(*args)
after second sayHello() call
现在decoration方法调用构造器,然后就马上调用__call__(),后者只能包含一个参数(函数对象)且返回替代原有函数的decorated函数对象。注意当前decoration期间__call__()仅被调用一次,此后从__call__()返回的decorated函数就可以在实际调用中使用了。
虽然这种机制有一定合理性—构造器在这里可获取decorator参数,但__call__()对象不能再作为decorated函数使用了。因此你必须使用__call__()执行decoration—可能第一次遇到这种与无参情况截然不同的方式你会比较吃惊,何况还必须编写和无参decorator完成不同的代码。
含decorator参数的decorator函数
最后,让我们看一个更复杂一点的decorator函数实现,它需要你处理所有细节:
def decoratorFunctionWithArguments(arg1, arg2, arg3):
def wrap(f):
print "Inside wrap()"
def wrapped_f(*args):
print "Inside wrapped_f()"
print "Decorator arguments:", arg1, arg2, arg3
f(*args)
print "After f(*args)"
return wrapped_f
return wrap
@decoratorFunctionWithArguments("hello", "world", 42)
def sayHello(a1, a2, a3, a4):
print 'sayHello arguments:', a1, a2, a3, a4
print "After decoration"
print "Preparing to call sayHello()"
sayHello("say", "hello", "argument", "list")
print "after first sayHello() call"
sayHello("a", "different", "set of", "arguments")
print "after second sayHello() call"
输出结果为:
Inside wrap()
After decoration
Preparing to call sayHello()
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: say hello argument list
After f(*args)
after first sayHello() call
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: a different set of arguments
After f(*args)
after second sayHello() call
decorator函数的返回值必须是一个封装待decorated函数的函数。也就是说,Python会保存返回函数然后在decoration期间调用,并传递待decorated函数。这也是为何有三层函数的原因:里面那个函数才是被替换的。
由于闭包,wrapped_f()有权访问decorator参数arg1, arg2 和arg3,而无需像在class版本中那样显式存储它们。然而,我也是在这里发现了“显胜于隐(explicit is better than implicit)”。即使该函数版本看起来要更加简洁紧凑,但我发现还是类版本容易理解,当然也就容易修改和维护。
我使用make已有很多年了。我只使用ant的原因是它可以创建速度更快的java build。但这两个构建系统都是以“问题是简单的”这个出发点考虑问题的,所以此后不久就发现真正需要的是一个程序设计语言来解决构建问题。可为时已晚,作为代价你需要付出大量艰辛来搞定问题。
在语言之上实现构建系统已有一些成果。Rake是一个十分成功的基于Ruby的领域特定语言(domain specific language, DSL)。还有不少项目也是用Python完成的,
多年来我一直想能有一个Python上的系统,其作用类似于一个薄薄的“胶合板”。这样可以得到一些依赖性上的支持,即使都是Python上的。因为如此一来你就不必在Python和Python以外的语言之间来回奔波,减少了思维上的分心。
最终发现decorators是解决该问题的最佳选择。我这里设计的仅是一个雏形,但很容易添加新特性,而且我已经开始将它作为The Python Book的构建系统了,也需要增加更多特性。更重要的是,我知道我能够做任何想做而make和ant做不到的事(没错,你可以扩展ant,但常常是得不偿失)。
尽管书中其余部分有一个Creative Commons Attribution-Share Alike license(Creative Commons相同方式共享署名许可),可该程序仅有一个Creative Commons Attribution许可,因为我想要人们能够在任何环境下使用它。很显然,如果你做了任何回馈于项目的改进方面的贡献,那就再好不过了。不过这不是使用和修改代码的前提条件。
语法
该构建系统提供的最重要和最便捷的特性就是依赖(dependencies)。你告诉它什么依赖于什么,如何更新这些依赖,这就叫做一个规则(rule)。因此decorator也被称为rule。decorator第一个参数是目标对象(target,需要更新的变量),其余参数都属于依赖(dependencies)。如果目标对象相对于依赖过时,函数就要对它进行更新。
下面是一个反映基本语法的简单例子:
@rule("file1.txt")
def file1():
"File doesn't exist; run rule"
file("file1.txt", 'w')
规则名称是file1,因为这也是函数名称。在这里例子里,目标对象是"file1.txt",不含依赖,因此规则仅检查file1.txt是否存在,如果不存在则运行起更新作用的函数。
注意docstring的使用:它由构建系统获取,当你输入build help,则以命令行方式显示规则描述。
@rule decorators仅对其绑定的函数产生影响,因此你可以很容易地在同一构建文件中将普通代码和规则混合使用。这是一个更新文件时间戳的函数,如果文件不存在则创建一份:
def touchOrCreate(f): # Ordinary function
"Bring file up to date; creates it if it doesn't exist"
if os.path.exists(f):
os.utime(f, None)
else:
file(f, 'w')
更典型的规则是将目标文件与一个或多个依赖文件进行关联:
@rule("target1.txt","dependency1.txt","dependency2.txt","dependency3.txt")
def target1():
"Brings target1.txt up to date with its dependencies"
touchOrCreate("target1.txt")
构建系统也允许加入多个目标,方法是将这些目标放入一个列表中:
@rule(["target1.txt", "target2.txt"], "dependency1.txt", "dependency2.txt")
def multipleBoth():
"Multiple targets and dependencies"
[touchOrCreate(f) for f in ["target1.txt", "target2.txt"]]
如果目标对象和依赖都不存在,规则通常这样运行:
@rule()
def clean():
"Remove all created files"
[os.remove(f) for f in allFiles if os.path.exists(f)]
这个例子中出现了alFiles数组,稍后作以介绍。
你也可以编写依赖于其他规则的规则:
@rule(None, target1, target2)
def target3():
"Always brings target1 and target2 up to date"
print target3
由于None是目标对象,这里不存在比较,但在检查target1和target2的规则过程中,它们将被更新。这在编写“所有(all)”规则时十分有用,下面例子中将涉及到。
构建器代码
通过使用decorators和一些恰当的设计模式,代码变得十分简洁。需要注意的是,代码__main__创建了一个名为“build.by”的示例文件(包含了你上面看到的例子)。当你第一次运行构建时,它创建了一个build.bat文件(在windows中)或build命令文件(在Unix/Linux/Cygwin中)。完整的解释说明附在代码后面:
# builder.py
import sys, os, stat
"""
Adds build rules atop Python, to replace make, etc.
by Bruce Eckel
License: Creative Commons with Attribution.
"""
def reportError(msg):
print >> sys.stderr, "Error:", msg
sys.exit(1)
class Dependency(object):
"Created by the decorator to represent a single dependency relation"
changed = True
unchanged = False
@staticmethod
def show(flag):
if flag: return "Updated"
return "Unchanged"
def __init__(self, target, dependency):
self.target = target
self.dependency = dependency
def __str__(self):
return "target: %s, dependency: %s" % (self.target, self.dependency)
@staticmethod
def create(target, dependency): # Simple Factory
if target == None:
return NoTarget(dependency)
if type(target) == str: # String means file name
if dependency == None:
return FileToNone(target, None)
if type(dependency) == str:
return FileToFile(target, dependency)
if type(dependency) == Dependency:
return FileToDependency(target, dependency)
reportError("No match found in create() for target: %s, dependency: %s"
% (target, dependency))
def updated(self):
"""
Call to determine whether this is up to date.
Returns 'changed' if it had to update itself.
"""
assert False, "Must override Dependency.updated() in derived class"
class NoTarget(Dependency): # Always call updated() on dependency
def __init__(self, dependency):
Dependency.__init__(self, None, dependency)
def updated(self):
if not self.dependency:
return Dependency.changed # (None, None) -> always run rule
return self.dependency.updated() # Must be a Dependency or subclass
class FileToNone(Dependency): # Run rule if file doesn't exist
def updated(self):
if not os.path.exists(self.target):
return Dependency.changed
return Dependency.unchanged
class FileToFile(Dependency): # Compare file datestamps
def updated(self):
if not os.path.exists(self.dependency):
reportError("%s does not exist" % self.dependency)
if not os.path.exists(self.target):
return Dependency.changed # If it doesn't exist it needs to be made
if os.path.getmtime(self.dependency) > os.path.getmtime(self.target):
return Dependency.changed
return Dependency.unchanged
class FileToDependency(Dependency): # Update if dependency object has changed
def updated(self):
if self.dependency.updated():
return Dependency.changed
if not os.path.exists(self.target):
return Dependency.changed # If it doesn't exist it needs to be made
return Dependency.unchanged
class rule(object):
"""
Decorator that turns a function into a build rule. First file or object in
decorator arglist is the target, remainder are dependencies.
"""
rules = []
default = None
class _Rule(object):
"""
Command pattern. name, dependencies, ruleUpdater and description are
all injected by class rule.
"""
def updated(self):
if Dependency.changed in [d.updated() for d in self.dependencies]:
self.ruleUpdater()
return Dependency.changed
return Dependency.unchanged
def __str__(self): return self.description
def __init__(self, *decoratorArgs):
"""
This constructor is called first when the decorated function is
defined, and captures the arguments passed to the decorator itself.
(Note Builder pattern)
"""
self._rule = rule._Rule()
decoratorArgs = list(decoratorArgs)
if decoratorArgs:
if len(decoratorArgs) == 1:
decoratorArgs.append(None)
target = decoratorArgs.pop(0)
if type(target) != list:
target = [target]
self._rule.dependencies = [Dependency.create(targ, dep)
for targ in target for dep in decoratorArgs]
else: # No arguments
self._rule.dependencies = [Dependency.create(None, None)]
def __call__(self, func):
"""
This is called right after the constructor, and is passed the function
object being decorated. The returned _rule object replaces the original
function.
"""
if func.__name__ in [r.name for r in rule.rules]:
reportError("@rule name %s must be unique" % func.__name__)
self._rule.name = func.__name__
self._rule.description = func.__doc__ or ""
self._rule.ruleUpdater = func
rule.rules.append(self._rule)
return self._rule # This is substituted as the decorated function
@staticmethod
def update(x):
if x == 0:
if rule.default:
return rule.default.updated()
else:
return rule.rules[0].updated()
# Look up by name
for r in rule.rules:
if x == r.name:
return r.updated()
raise KeyError
@staticmethod
def main():
"""
Produce command-line behavior
"""
if len(sys.argv) == 1:
print Dependency.show(rule.update(0))
try:
for arg in sys.argv[1:]:
print Dependency.show(rule.update(arg))
except KeyError:
print "Available rules are:/n"
for r in rule.rules:
if r == rule.default:
newline = " (Default if no rule is specified)/n"
else:
newline = "/n"
print "%s:%s/t%s/n" % (r.name, newline, r)
print "(Multiple targets will be updated in order)"
# Create "build" commands for Windows and Unix:
if not os.path.exists("build.bat"):
file("build.bat", 'w').write("python build.py %1 %2 %3 %4 %5 %6 %7")
if not os.path.exists("build"):
# Unless you can detect cygwin independently of Windows
file("build", 'w').write("python build.py $*")
os.chmod("build", stat.S_IEXEC)
############### Test/Usage Examples ###############
if __name__ == "__main__":
if not os.path.exists("build.py"):
file("build.py", 'w').write('''/
# Use cases: both test code and usage examples
from builder import rule
import os
@rule("file1.txt")
def file1():
"File doesn't exist; run rule"
file("file1.txt", 'w')
def touchOrCreate(f): # Ordinary function
"Bring file up to date; creates it if it doesn't exist"
if os.path.exists(f):
os.utime(f, None)
else:
file(f, 'w')
dependencies = ["dependency1.txt", "dependency2.txt",
"dependency3.txt", "dependency4.txt"]
targets = ["file1.txt", "target1.txt", "target2.txt"]
allFiles = targets + dependencies
@rule(allFiles)
def multipleTargets():
"Multiple files don't exist; run rule"
[file(f, 'w') for f in allFiles if not os.path.exists(f)]
@rule(["target1.txt", "target2.txt"], "dependency1.txt", "dependency2.txt")
def multipleBoth():
"Multiple targets and dependencies"
[touchOrCreate(f) for f in ["target1.txt", "target2.txt"]]
@rule("target1.txt","dependency1.txt","dependency2.txt","dependency3.txt")
def target1():
"Brings target1.txt up to date with its dependencies"
touchOrCreate("target1.txt")
@rule()
def updateDependency():
"Updates the timestamp on all dependency.* files"
[touchOrCreate(f) for f in allFiles if f.startswith("dependency")]
@rule()
def clean():
"Remove all created files"
[os.remove(f) for f in allFiles if os.path.exists(f)]
@rule()
def cleanTargets():
"Remove all target files"
[os.remove(f) for f in targets if os.path.exists(f)]
@rule("target2.txt", "dependency2.txt", "dependency4.txt")
def target2():
"Brings target2.txt up to date with its dependencies, or creates it"
touchOrCreate("target2.txt")
@rule(None, target1, target2)
def target3():
"Always brings target1 and target2 up to date"
print target3
@rule(None, clean, file1, multipleTargets, multipleBoth, target1,
updateDependency, target2, target3)
def all():
"Brings everything up to date"
print all
rule.default = all
rule.main() # Does the build, handles command-line arguments
''')
第一组类管理不同类型对象间的依赖。基类包含了一些通用代码,比如,当在派生类中没有显式重定义构造器则自动调用构造器(Python中一个节省代码的优秀特性)
Dependency的派生类管理依赖关系中的具体类型,并重定义updated()方法以根据依赖判断目标对象是否需要更新。这是一个Template Method(模版方法)设计模式的例子,updated()是模版方法,_Rule是上下文(context)。
如果你想创建一个新类型的依赖—即增加依赖和/或目标上的通配符—你就要定义新的Dependency子类。你会发现其余代码不需要更改,这是设计的一个独到之处(未来的变化相对独立)。
Dependency.create()是所谓的Simple Factory Method(简单工厂模式),因为它的所有工作是使创建所有Dependency子类型实现本地化。注意这里向前引用并不是问题,因为它就存在在一些语言里面。所以没必要使用GoF提供的更复杂的Factory Method完整实现(并不意味着完整版的Factory Method就没有用武到之地)。
注意,在FileToDependency中,我们可以声明self.dependency是Dependency的一个子类型,但在调用updated()时才会进行类型检查(有效性)。
一个rule Decorator
rule decorator使用了Builder(构造器)设计模式,其原因是一个规则的创建分为两步:构造器获取decorator参数,__call__()方法获取函数。
Builder产品是一个_Rule对象。像Dependency类一样,它包含一个updated()方法。每个_Rule对象包含一个dependencies列表和一个ruleUpdater()方法,后者在任何一个依赖过时时调用。_Rule还包含一个name(decorated函数名)和一个description(decorated函数的docstring)。(_Rule对象是一个Command(命令)模式的例子)。
_Rule的不寻常之处在于你在类中看不到任何初始化dependencies, ruleUpdater(),name和description的代码。它们是在Builder方法过程中使用Injection并由rule完成初始化的。其典型的替代方法是创建setter方法,但由于_Rule是内嵌到rule中,rule便“拥有了”_Rule,Injection也看起来更简单直接一些。
rule构造器首先创建了产品_Rule对象,然后处理decorator参数。它将decoratorArgs转换为一个list—因为我们需要对其对进行修改—并使decoratorArgs作为一个tuple出现。如果仅有一个参数,那就意味着用户仅指定了目标对象而没有依赖。因为Dependency.create()需要两个参数,我们将None加入到列表中。
目标对象通常是第一个参数,因此pop(0)将它弹出来,列表中剩下的就是依赖了。为了适应目标对象可能是一个列表的情况,单独的目标对象会被转换为列表。
现在任何可能的目标-依赖组合都调用了Dependency.create(),并且结果列表被注入到_Rule对象中。当出现不含参数的特殊情况时,就会创建一个None和None Dependency。
注意,rule构造器做的唯一事情是提取参数,它对具体的关联关系一无所知。这使得Dependency级别的内部得以存储专门知识。即使增加一个新的Dependency也会与该级别相互独立。
类似的思想也出现__call__()方法上,它用于获取decorated函数。我们将_Rule对象保存在一个称为rules的静态列表中。首先检查的是是否有重复规则名,然后获取并写入名字、文件字符串和函数本身。
注意,Builder“产品”、即_Rule对象作为rule.__call__()的结果返回,这意味着该对象—不含__call__()方法—替代了decorated函数。这是decorators比较少见的用法。正常情况下decorated函数被直接调用,但在这里decorated函数不会直接调用,只能通过_Rule对象调用。
运行系统
rule中的静态方法main()负责构建过程,并使用helper方法update()。如果不提供命令行参数,main()传递0至update(),如果后者完成设置则调用默认规则,否则调用定义好第一个规则。如果提供命令行参数,它将传递每一个参数到update()。
如果给定一个错误参数(比如help就是个典型例子),它将输出附加对应docstrings的所有规则。
最后,它将检查看build.bat和build命令文件是否存在,若不存在就创建它们。
第一次运行build.py时它将作为构建文件的起点运行。
改进
按照目前情况,这个系统仅完成了基本功能,仍有不完善之处。比如,在处理依赖方面它还没有make有的所有特性。另一方面,由于它是基于一个十分完善的程序设计语言构建的,你可以很容易地做想做的事。如果你发现自己一直在重复地写相同代码,则可以修改rule()来减少这种重复劳动。如果你拥有许可,请将类似的改进建议提交上来。
(原文链接网址:http://www.artima.com/weblogs/viewpost.jsp?thread=240808)