17.4 主脚本和main模块的设计

    最上层的主脚本会完成应用程序的执行。在一些情况下,会有多个主脚本,因为应用会做多种不同的事情。如何写最上层主脚本,主要有以下3种方式。

    • 对于小应用来说,可以使用python3.3some _ script.py来运行程序。这也是在大多数例子中所介绍的方式。
    • 对于更大一些的应用,会使用一个或多个文件,使用OS chmod +x命令标记为可执行文件。可以将这些可执行文件放在Python的scripts目录中,与setup.py安装文件放在一起。然后使用命令行通过运行some _ script.py来执行程序。
    • 对于复杂的应用,可以在程序包中添加一个main.py模块。为了使接口简洁,标准库中提供了 runpy模块和-m命令行选项来使用这种特殊命名的模块。可以使用python3.3 -m some _ app来运行。

    我们会详细介绍后两种方式。

    17.4.1 创建可执行脚本文件

    使用可执行脚本文件时,分为两个步骤:使得它可执行并包含一个#!(“shebang”)行。接下来会具体介绍。

    我们使用了 chmod +x some _ script.py来标记可执行脚本。然后,包含了一行shebang

    #!/usr/bin/env python3.3

    这一行会引导 OS 使用命名的程序来执行脚本文件。这样一来,就使用了/usr/ bin/env程序来查找python3.3程序,进而运行了脚本。Python3.3程序将提供脚本文件作为输入。

    一旦脚本文件被标记为可执行——并且包含第1行——就可以在命令行使用some_ script.py来运行脚本。

    对于更复杂的应用,最上层脚本可以用于完成其他模块和包的导入。保持这些最上层可执行脚本文件尽可能的简单,这点是重要的。有关可执行脚本文件的设计,之前已经强调过了。

    保持脚本模块尽可能的小。 脚本模块不应该包含新的或独特的代码。它应该总是完成已有代码的导入。 没有单独存在的程序。
    我们的设计目标中必须总是包含组合的思想以及扩展性。如果程序的一部分包含在Python库中,而在脚本目录中包含另外一些部分,这种情况是尴尬的。主脚本文件应该尽可能的简短,如下代码所示。
    import simulation
    with simulation.Logging_Config():
      with simulation.Application_Config() as config:
        main= simulation.Simulate_Command()
        main.config= config
        main.run()

    一切实际工作的代码都是从一个叫作simulation的模块中导入的。在这个模块中不存在唯一或特有的代码。

    17.4.2 创建main模块

    为了使用runpy接口,可以使用简单的实现。向我们应用中最上层的包中添加一个小的 main /py模块。之前已经强调过了有关最上层可执行脚本文件的设计。

    总应该通过对一个应用进行重构来创建更大、更复杂组合的应用。如果在 main .py中包含了一些功能,就需要将它们提到其他模块中,以清晰的、可导入的名称来命名,这样就可以被其他应用重用。

    main .py模块应该看起来像以下代码这样精简。

    import simulation
    with simulation.Logging_Config():
      with simulation.Application_Config() as config:
        main= simulation.Simulate_Command()
        main.config= config
        main.run()

    我们对创建工作的上下文的过程进行了最大程度的化简。所有的实际工作都是从包中导入的。并且,我们假设 main .py模块永不会被导入。

    以上所示就是 main模块中应该有的一切。我们的目的是让我们的应用能够具有最大化的重用能力。

    17.4.3 大规模编程

    在以上的示例中,介绍了为什么不能将特有的实际工作代码放入 main .py模块中。我们将对现有的包进行扩展来演示一个示例。

    可以想象我们有一个泛型的静态包,命名为stats和一个最上层的 main .py模块。它的实现是基于命令行接口,对指定的CSV文件进行描述性统计。这个应用包含了如下所示的命令行API。

    python3.3 -m stats -c 10 some_file.csv

    命令行中通过使用-c选项来指定要分析的列。同时文件名在命令行中作为一个位置参数被提供。

    进一步假设,我们出现了重大的设计问题。我们在stats/ main .py模块中定义了一个高层次的函数analyze()

    我们的目的是将它与21点模拟进行结合。由于出现了设计失误,因此它不能很好地工作,或许我们认为可以这样做。

    import stats
    import simulation
    import types
    def sim_and_analyze():
      with simulation.Application_Config() as config_sim:
        config_sim.outputfile= "some_file.csv"
        s = simulation.Simulate()
        s.run()
      config_stats= types.SimpleNamespace( column=10, input="some_file.
    csv" )
      stats.analyze( config_stats )

    使用了stats.analyze()来假设外层的接口是包的一部分,不是 main .py的一部分。这种通过在 main ()中定义函数实现的简单组合造成了不必要的复杂度。

    我们希望避免被迫这样做。

    def analyze( column, filename ):
      import subprocess
      subprocess.check_call( "python3.3 -m stats -c {0} {1}".format(
        column, filename) )

    我们应该不必通过命令行API来创建可组合的Python应用。为了创建一个已有应用的合理组成成分,我们需要对stats/ main .py进行重构,移除模块中的所有定义并把它们放入包中从而能够在全局范围内使用。