您的位置:首页 > 编程语言 > Python开发

Python Decorators(三):基于Decorator的构建系统

2009-04-14 14:53 344 查看
Python Decorators III: A Decorator-Based Build System
October 26, 2008



我使用make已有很多年了。我只使用ant的原因是它可以创建速度更快的java build。但这两个构建系统都是以“问题是简单的”这个出发点考虑问题的,所以此后不久就发现真正需要的是一个程序设计语言来解决构建问题。可为时已晚,作为代价你需要付出大量艰辛来搞定问题。

在语言之上实现构建系统已有一些成果。Rake是一个十分成功的基于Ruby的领域特定语言(domain specific language, DSL)。还有不少项目也是用Python完成的,
多年来我一直想能有一个Python上的系统,其作用类似于一个薄薄的“胶合板”。这样可以得到一些依赖性上的支持,即使都是Python上的。因为如此一来你就不必在Python和Python以外的语言之间来回奔波,减少了思维上的分心。
最终发现decorators是解决该问题的最佳选择。我这里设计的仅是一个雏形,但很容易添加新特性,而且我已经开始将它作为The Python Book的构建系统了,也需要增加更多特性。更重要的是,我知道我能够做任何想做而makeant做不到的事(没错,你可以扩展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.dependencyDependency的一个子类型,但在调用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(), namedescription的代码。它们是在Builder方法过程中使用Injection并由rule完成初始化的。其典型的替代方法是创建setter方法,但由于_Rule是内嵌到rule中,rule便“拥有了”_Rule,Injection也看起来更简单直接一些。
rule构造器首先创建了产品_Rule对象,然后处理decorator参数。它将decoratorArgs转换为一个list—因为我们需要对其对进行修改—并使decoratorArgs作为一个tuple出现。如果仅有一个参数,那就意味着用户仅指定了目标对象而没有依赖。因为Dependency.create()需要两个参数,我们将None加入到列表中。
目标对象通常是第一个参数,因此pop(0)将它弹出来,列表中剩下的就是依赖了。为了适应目标对象可能是一个列表的情况,单独的目标对象会被转换为列表。
现在任何可能的目标-依赖组合都调用了Dependency.create(),并且结果列表被注入到_Rule对象中。当出现不含参数的特殊情况时,就会创建一个NoneNone 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.batbuild命令文件是否存在,若不存在就创建它们。
第一次运行build.py时它将作为构建文件的起点运行。

改进

按照目前情况,这个系统仅完成了基本功能,仍有不完善之处。比如,在处理依赖方面它还没有make有的所有特性。另一方面,由于它是基于一个十分完善的程序设计语言构建的,你可以很容易地做想做的事。如果你发现自己一直在重复地写相同代码,则可以修改rule()来减少这种重复劳动。如果你拥有许可,请将类似的改进建议提交上来。

下一节内容

在本系列文章的最后一节中,我们将讨论class decorators和你是否能够decorate一个对象。

(原文链接网址:http://www.artima.com/weblogs/viewpost.jsp?thread=241209
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: