16.5 创建顶层main()函数
在第13章“配置文件和持久化”中,我们介绍了两个应用程序配置设计模式。
- 全局特性映射:在前面的例子中,我们用ArgumentParser创建的Namespace对象实现了全局特性映射。
- 对象创建:对象创建的目的是基于配置参数创建需要的对象实例,实际上就是将全局特性映射降级为main()函数中的局部特性映射并且不会保存特性。
在前面的部分中,我们向你展示的是使用局部的Namespace对象来收集所有的参数。从这里开始,我们可以创建必要的应用程序对象,它们将会做真正的应用程序工作。这两种设计模式不是对立的,而是互补的。我们用Namespace收集一组一致的值,然后基于这个Namespace中的值创建不同的对象。
这样我们就需要为顶层函数进行设计。在介绍实现方法之前,我们需要为这个函数起一个合适的名字,有两种方法可以命名这个函数。
- 命名它为main(),因为这是作为整个应用程序起点的通用术语。
- 不要命名为main(),因为main()不明确,所以从长远来看没有意义。
我们也认为这里不需要二选一,我们应该同时做到以上两点。定义一个名为verb noun()短语的顶层函数很好地描述了操作。增加一行main= verb noun来提供一个可以帮助其他开发者了解应用程序工作方式的main()函数。
这个包含两个部分的实现使得我们可以通过扩展来改变main()的定义。我们可以添加函数并且重新为main分配名称。作为维持程序稳定和日益增加的API的需要,老的函数名仍然被保留。
下面是一个顶层的应用程序脚本,它基于配置Namespace对象创建多个对象。
import ast
import csv
def simulate_blackjack( config ):
dealer_rule= {'Hit17': Hit17, 'Stand17': Stand17,}config.dealer_rule
split_rule= {'ReSplit': ReSplit, 'NoReSplit': NoReSplit, 'NoReSplitAces': NoReSplitAces,
}config.split_rule
try:
payout= ast.literal_eval( config.payout )
assert len(payout) == 2
except Exception as e:
raise Exception( "Invalid payout {0}".format(config.payout) )
from e
table= Table( decks=config.decks, limit=config.limit,
dealer=dealer_rule,
split=split_rule, payout=payout )
player_rule= {'SomeStrategy': SomeStrategy,
'AnotherStrategy': AnotherStrategy,}config.player_rule
betting_rule= {"Flat":Flat,"Martingale":Martingale, "OneThreeTwoSix": OneThreeTwoSix,
}config.betting_rule
player= Player( play=player_rule, betting=betting_rule,
rounds=config.rounds, stake=config.stake )
simulate= Simulate( table, player, config.samples )
with open(config.outputfile, "w", newline="") as target:
wtr= csv.writer( target )
wtr.writerows( simulate )
这个函数依赖于外部通过配置属性提供的Namespace对象。因为它没有被命名为main(),所以我们将来可以把它改为和main意义不同的函数。
我们创建多个所需的对象——Table、Player和Simulate。将基于配置参数的初始值为这些对象进行配置。
事实上,我们已经完成了实际工作。在所有的对象创建完成后,真正的工作是那行突出显示的:wtr.writerows( simulate )。程序90%的时间都会花在这里,生成示例并且将它们写入需求的文件中。
GUI应用程序也遵循类似的模式,它们进入主循环来处理GUI事件,这个模式也可以应用于进入主循环处理请求的服务器。
我们依赖于需要将配置对象作为参数传递。这是减少依赖项的测试策略。这个顶层的simulate _ blackjack()函数不依赖于配置创建的细节。然后,我们可以在应用程序脚本中使用这个函数。
if name == "main":
logging.config.dictConfig( yaml.load("logging.config") )
config5= gather_configuration()
simulate_blackjack( config5 )
logging.shutdown()
这是业务间关系分离的做法。我们将应用程序的工作嵌套进两个层次的模块中。
外层的模块通过日志定义。我们在所有其他应用程序模块的外部定义了日志记录机制,这样做是为了确保在不同的顶层模块、类和函数试图配置日志记录机制时没有冲突。如果应用程序的任何特定部分尝试配置日志记录机制,那么这样的修改会导致冲突。尤其是,当我们介绍将应用程序融合成更大的复合处理单元时,需要确保这两个被融合的应用程序不会导致日志记录配置的冲突。
内层的模块是通过应用程序配置定义的。我们不希望独立的应用程序模块间有冲突,而是希望允许命令行API在不影响应用程序的前提下演化。我们希望可以将应用程序的处理流程嵌入独立的环境中,可能通过multiprocessing或者一个RESTful网络服务器来定义。
16.5.1 确保配置遵循DRY原则
在参数解析器的创建和使用参数配置应用程序之间,可能会有违背DRY的地方。我们用一些重复的键创建参数。
我们可以通过创建一些全局内部配置消除这种重复。例如,我们可能这样定义全局配置。
dealer_rule_map = { "Hit17": Hit17, "Stand17", Stand17 }
我们可以用它来创建参数解析器。
parser.add_argument( "—dealerhit", action="store", default="Hit17",
choices=dealer_rule_map.keys(), dest='dealer_rule')
我们可以用它创建工作对象。
dealer_rule= dealer_rule_mapconfig.dealer_rule
这种做法消除了重复。当程序持续演变时,它允许我们在某处添加新的类定义和参数键映射,它也允许我们像下面这样创建外部API的简写形式或者重写外部API。
dealer_rule_map = { "H17": Hit17, "S17": Stand17 }
从命令行(或配置文件)字符串到应用程序类的映射有4种类型。使用这些内置的映射可以简化simulate _ blackjack()函数。
16.5.2 管理嵌套的配置上下文
在某种程度上,嵌套上下文的出现意味着顶层的脚本看起来应该类似下面的代码。
if name == "main":
with Logging_Config():
with Application_Config() as config:
simulate_blackjack( config )
我们添加了两个上下文管理器。更多的信息,参见第5章“可调用对象和上下文的使用”。下面是两个上下文管理器。
class LoggingConfig:
def enter( self, filename="logging.config" ):
logging.config.dictConfig( yaml.load(filename) )
def exit( self, *exc ):
logging.shutdown()
class ApplicationConfig:
def enter( self ):
# Build os.environ defaults.
# Load files.
# Build ChainMap from environs and files.
# Parse command-line arguments.
return namespace
def __exit( self, *exc ):
pass
Logging _ Config上下文管理器配置日志记录。它同时也确保了当应用程序结束时会正确地关闭日志。
Application _ Config上下文管理器可以从一系列的文件中获取配置信息和命令行参数。在本例中,使用上下文管理器不是必需的。但是,使用它为我们留下了可扩展的余地。
这种设计模式可能明确围绕应用程序启动和关闭的各种关系。尽管对于大多数应用程序,这样的设计可能有点复杂,但是这种设计与Python中上下文管理器的思想一致,同时随着应用程序逐渐增长,它也会为我们提供很多帮助。
当面对持续增长和扩展的应用程序时,我们通常会使用大规模编程技术。对于这种技术,将可更改的应用程序处理上下文与很少更改的处理上下文分离是非常重要的。
