17.1 设计一个模块

    模块是Python中实现和重用的单元。所有的Python编程都是在模块层面提供的。类是面向对象设计和编程的基础。模块——类的集合——是在Python中更高层面上的可重用单元。

    一个Python模块是一个文件,文件扩展名必须为.py。在.py之前的文件名必须为一个有效的Python名。在Python语言参考的第2.3节中,为我们提供了命名的完整定义。其中的一点是:在ASCII范围(U+0001..U+007F)内,标识符的有效字符与Python 2.x相同:大小写字母(A~Z)、下划线,以及不能作为标识符开始的数字(0~9)。

    在OS文件中允许使用比在Python名称中更多ASCII范围内字符,这一点复杂度可以被忽略。文件名(不包括.py)将作为模块名称。

    每次创建一个.py文件,就创建了一个模块。通常,创建一个Python文件时不需要做太多的设计。为了创建可重用的模块,在本章中,会介绍一些设计上的考虑因素。

    出于私有的目的,Python也会创建.pyc和.pyo文件;忽略它们就可以了。有些程序员试图去使用.pyc文件作为一种已编译的对象代码来替代.py文件,以达到对源代码保密的目的,以至于浪费了很多脑细胞。我们需要强调一点,.pyc文件可以很容易地被反编译,无法达到任何保密的目的。如果需要阻止对应用的任何逆向工程,可能需要考虑换一种语言。

    17.1.1 一些模块设计的方法

    有关Python模块的设计,有3种常见的设计方案。

    • 库模块:它们意味着要被导入的部分,包括类、函数以及一些创建全局变量的赋值语句。它们不包括任何实际的工作,因此不必担心在导入时会产生副作用的问题。我们会介绍两种用例。
      • 全局模块:一些模块的设计将被导入作用于全局范围,创建一个模块命名空间,包含了所有项的集合。
      • 项集合:一些模块被设计为包含一些独立项的导入,而不是创建一个模块对象。
    • 主要脚本模块:它们意味着从命令行执行。它们包含的不仅是类和函数定义,还包括做实际工作的语句,可能会产生副作用。由于副作用的存在,它们不能做导入。如果试图导入一个主要脚本模块,它将被执行——做实际的工作,可能会更新文件或在运行时做模块被设计要做的事情。
    • 条件脚本模块:这些模块有两个用例:它们可以被导入并且也可以从命令行执行。这些模块将包含主要导入的开关,正如在Python标准库中28.4节中所介绍的main——最高级别的脚本环境。

    以下是基于库文档被简化后的条件脚本。

    if name == "main":
      main()

    main ()函数做了脚本的工作。这个设计支持两种情况:runimport。当模块从命令行运行时,它执行了main()并且做了所期望的工作。当模块被导入时,函数不会被执行,模块只是用于提供定义,而没有做实际的工作。

    建议使用更复杂的方式,如第16章“使用命令行”中所介绍的。

    if name == "main":
      with Logging_Config():
        with Application_Config() as config:
          main= Simulate_Command()
          main.config= config
          main.run()

    这样做的目的是反映出以下这个设计的基本要素。

    导入模块时,应该只有很少的副作用。

    对于导入来说,创建一些模块级别的变量是可以接受的副作用。而实际工作——访问网络资源、打印输出、更新文件以及其他的副作用——在导入模块时不应该发生。

    没有使用name == "main"的主要脚本模块通常是糟糕的,因为它不会被导入或重用。更糟的是,文档工具很难与主要脚本模块一起工作,而且很难测试。如果使用文档工具导入模块,就会导致一些不可预见的事情发生。类似地,在测试时要避免将导入模块作为测试的一个步骤。

    17.1.2 模块VS类

    在定义模块和类时,会涉及以下几点。

    • 模块和类在Python中都有一个名称。模块通常使用以小写字母开头的名称,类通常使用以大写字母开头的名称。
    • 模块和类的定义都是包含了对象的命名空间。
    • 模块在全局命名空间 sys.modules 中是单例的对象。类定义在命名空间中是唯一的,要么在全局命名空间main中或是一些本地的命名空间。类不是单例,定义可以被替换。一旦完成了导入,模块不能再次被导入,除非被删除。
    • 在命名空间中,类或模块的定义可以被作为语句序列来执行。
    • 模块中函数的定义等价于类定义中的静态方法。
    • 模块中类的定义等价于另一个类中的类定义。

    在模块和类之间有两点明显的区别。

    • 不能创建模块的实例,它总是单例的,但可以创建类的多个实例。
    • 在模块的赋值语句中将创建在模块命名空间内的全局变量,它可以在整个模块中被使用。类定义中的赋值语句将在类命名空间中创建一个变量,它需要一个限定词来区分全局变量。
    模块与类相似。模块、包和类都可用于封装数据并将属性和一些操作进行处理,被存入对象中。

    模块和类很相似,在它们之间做选择会需要在设计上做一些决策和权衡。在大多数情况下,instance of是决定的因素。模块的单例功能意味着使用的模块(或包)中所包含的类和函数只会被定义一次,即使导入多次也是一样的结果。

    然而,有些模块具备类的风格。例如,logging模块,通常在其他模块中被导入。单例功能意味着日志配置只需要设置一次,会应用到所有的模块中。

    类似的,对于配置模块,也会在多个地方被导入。这时单例意味着配置可被导入到任意模块中,但它们是全局的。

    但编写的程序使用的是单一连接的数据库,包含多个函数的模块与单例的类是类似的。数据库访问层可以通过应用进行导入,但它将成为一个单例的、全局的共享对象。

    17.1.3 模块中应该包含的内容

    Python模块中有一个典型的组织结构。关于这一点,在PEP8中有一些定义,可以参见http://www.python.org/dev/peps/pep-0008/

    模块的第1行可以是以#!为开头的注释,用于标注版本号,如下所示。

    #!/usr/bin/env python3.3

    这样会有助于 OS 的工具进行相关操作,例如以 bash 为可执行的脚本文件来查找Python解释器。在Windows中,这行代码将为#!C:\Python3\python.exe

    更早的Python模块会包含一行注释来标识文本的编码格式,如下所示。

    # -- coding: utf-8 --

    编码格式注释在Python3中不是必需的,OS的编码信息已经足够了。早期的Python实现会假设文件都是以ASCII编码的,对于没有使用ASCII编码的文件,就需要使用编码格式注释来进行说明。

    模块中接下来的几行应该为3层引号的模块文档字符串,用于定义模块文件中的内容。和Python中其他文档字符串一样,在文本的第1段要进行总结说明。接下来要对模块内容、目的以及使用作完整的定义和说明。可以包含RST标记语言,这样就可以使用文档工具基于文档字符串生成优雅的输出。我们会在第18章“质量和文档”对这一点进行说明。

    在写完文档字符串之后,就可以添加版本信息了,如下所示。

    version = "2.7.18"

    这是为了确定在程序的其他位置所引用的版本号一致。它的定义是在文档字符串之后、模块体之前。然后是模块的import语句。一般地,它们出现在模块的前面。

    import语句之后,接下来是模块中类和函数中变量的定义。它们没有固定的顺序,但要确保程序能够正确地运行并要考虑到代码的可读性。

    Java和C++倾向于每个文件定义一个类。这样的限制不够明智。它不适用于Python,也更不是一种法则。

    如果文件包含了多个类,那么可能会认为这个模块有点不好维护。如果发现自己使用了很大的注释块来将模块分成几个部分,这意味着这个模块的复杂度已经超出了它的范围。当有多个模块时,可以使用包来管理。

    在一些模块中另一种常见的功能是在模块命名空间内创建对象。使用有状态的模块变量(例如类级别的属性)就不是一个好想法。缺乏对这些变量的可见度会造成困惑。

    有时,使用全局模块很方便。在logging模块中大量地使用了这一点。另一个例子是random模块创建Random类默认实例的方式。这使得很多模块级别的函数能够为随机数提供简单的API。我们不必去创建random.Random的实例。