17.3 包的设计

    设计包的一个重要原则是不要设计。在Zen of Python中这样提到:

    “平坦好过嵌套。”

    Python标准库中也可以看到这一点。库的结构相对平坦,只有少数嵌套的模块。深度嵌套的包可能被过度使用了。我们要对过分嵌套保持怀疑。

    一个包由一个目录和一个init.py文件组成。目录名称必须为适当的Python名称,OS的名称中包含了很多在Python的命名中不允许使用的字符。

    通常使用以下3种方式来进行包的设计。

    • 对于简单的包,使用一个目录和一个空的init.py文件。这个包的名称将被作为内部模块名称的限定词,例如以下代码。
    import package.module
    • 一个模块中的包可以包含一个init.py文件作为模块的定义,可以从包目录中导入其他模块。或者,它可以作为包含了最上层模块和被限定的子模块的大规模设计中的一部分。可以使用以下代码进行导入。
    import package
    • 目录是init.py文件中可替代的一种实现方式,可使用如下代码。
    import package

    第 1 种方式相对简单。一旦添加了init.py文件,就完成了包的创建。其他两种方式涉及更多方面,接下来会具体介绍。

    17.3.1 包与模块的混合设计

    在一些情况中,设计演化成相当复杂的模块——这时使用单一的文件是一个糟糕的主意。可能需要将这个复杂的模块重构为一个包含了多个小模块的包。

    这样一来,包的结构就像以下代码所示这样简单。以下是命名为blackjack包目录中的init.py文件。

    """Blackjack package"""
    from blackjack.cards import Shoe
    from blackjack.player import Strategy_1, Strategy_2
    from blackjack.casino import ReSplit, NoReSplit, NoReSplitAces,
    Hit17, Stand17
    from blackjack.simulator import Table, Player, Simulate
    from betting import Flat, Martingale, OneThreeTwoSix

    以上代码演示了如何创建一个模块风格的包,它实际上是一个组件,每个部分是从其他子模块中导入的。然后在全局应用中可以执行以下代码。

    from blackjack import *
    table= Table( decks=6, limit=500, dealer=Hit17(),
        split=NoReSplitAces(), payout=(3,2) )
    player= Player( play=Strategy_1(), betting=Martingale(), rounds=100,
    stake=100 )
    simulate= Simulate( table, player, 100 )
    for result in simulate:
      print( result )

    以上代码演示了我们如何使用from blackjack import *来定义源于其他包的一些类,而且有一个全局的blackjack包,包含了以下模块。

    • 在blackjack.cards包中包含了Card、Deck和Shoe的定义。
    • 在blackjack.player包中包含了打牌的多种策略。
    • 在blackjack.casino包中包含了用于自定义牌场规则的一些类。
    • 在blackjack.simulator包中包含了最上层的模拟工具。
    • 在betting包中,也包含了应用需要的不同玩牌策略,它们对于21点游戏不是唯一的,但是对于每个游戏都适用。

    这个包的架构或许可以简单地优化我们的设计。如果每个模块都简单一些或者更内聚,它的可读性会更强并且更容易理解。将每个模块隔离,升级起来更容易。

    17.3.2 使用多种实现进行包的设计

    在一些情况中,会包含一个最上层的init.py文件,需要在包目录不同的实现中做选择。决定可以基于平台、CPU架构或者OS库的可用性。

    关于包设计的不同实现,以下有两种常用和一种不太常见的设计方式。

    • 检查platform或sys来决定实现的细节以及使用if语句来决定要导入的内容。
    • 试图使用import并使用try语句块来捕捉异常,在异常中对不同配置的情形做判断。
    • 这种方式不太常见,应用可通过检查配置参数来决定要导入的内容。这种方式有些复杂。在导入应用配置和基于配置导入其他应用模块之间会存在先后顺序的问题。抛开先后顺序的复杂度,导入会容易很多。

    以下是some algorithm包的_init.py,它的实现基于平台信息。

    import platform
    bits, linkage = platform.architecture()
    if bits == '64bit':
      from some_algorithm.long_version import
    else:
      from some_algorithm.short_version import

    它使用了platform模块来获取平台的架构信息。这里存在一个顺序依赖,但对标准库模块的依赖好过对复杂的应用配置模块依赖。

    我们将在some algorithm包中提供两个模块,long version模块提供了一种64位的实现;short _ version模块提供了另外一种实现。设计必须具有模块同构性,这和类的同构性是类似的。两种模块都要包含具有相同的名称的API的类和函数。

    如果两个模块的文件中都定义了名为SomeClass的类,就可以在应用中使用如下代码。

    import some_algorithm
    process= some_algorithm.SomeClass()

    我们就可以像导入模块一样导入some _ algoritm包。包会查找一种比较合适的实现并提供所需类和函数的定义。

    用于替代if语句的另一种方式是使用try语句来查找可用的实现方式。当有不同的分支时,这种技术可以很好地工作。而往往一个具有平台特殊性的分支会包含一些在平台内唯一的文件。

    在第14章“Logging和Warning模块”中,在为配置文件错误事件提供预警的上下文中,我们演示了这种设计方式。对于一些情形,追踪不同的配置信息算不上一次预警,因为不同的配置是一种设计的功能。

    这里是一个some algorithm包的_init.py,基于包中模块文件的可用性选择一种实现方式。

    try:
      from some_algorithm.long_version import
    except ImportError as e:
      from some_algorithm.short_version import

    它依赖于两个不同的分支,要么包含some algorithm/long version.py文件,要么包含some algorithm/short version.py文件。如果没有找到some algorithm.long version模块,那么short _ version将被导入。

    它并不能被扩展成支持超过两种或 3 种不同的实现。随着选择数量的增加,except语句块将产生深度的嵌套。只能将每个try包裹在if中来创建平坦式设计。