购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.3 模块和包

现在我们知道了如何创建类和实例化对象,但是应该如何组织它们呢?对于小的程序来说,我们可以把所有的类都放到一个文件里,然后在文件最后添加一小段代码,让它们交互起来。然而,随着项目规模的增长,很难从众多类中找出我们需要修改的那一个。这时就需要 模块 module )了。模块就是Python文件,仅此而已。我们的小程序中的一个文件就是一个模块,两个Python文件就是两个模块。如果在同一个目录下有两个文件,则我们可以从其中一个模块中导入类到另一个模块中使用。

Python的模块名就是不包含.py后缀的文件名,比如有文件叫model.py,那么模块的名称就是model。Python通过查找当前目录和已经安装的包所在的目录来寻找模块。

import语句用于从模块中导入其他模块、特定的类或函数。我们在前面章节的Point类的示例中已经见到过。我们用import语句访问Python内置的math模块,并在我们的distance计算中使用它的hypot()函数。我们来看一个新示例。

如果我正在创建一个电子商务系统,则可能需要在数据库中存储很多数据。我们可以把所有与数据库操作相关的类和函数都放到一个独立的文件中(可以取名为database.py)。然后,其他的模块(如客户模块、产品信息模块和库存模块)就可以从database模块中导入这些类,从而对数据库进行操作。

我们从创建一个database.py模块开始,其中包含一个Database类,还有另一个模块products.py,用于处理产品相关的查询。products模块中的类需要实例化来自database.py的Database类,用来查询数据库中的产品表。

import语句用于访问Database类的语法有几种写法。第一种是导入整个模块:

这种写法是导入整个database模块,它会创建一个database命名空间,database模块中的任何类或方法都可以通过database.<something>来使用。

另一种写法,可以用from…import语法直接导入我们需要的某个类:

这种写法只从database模块中导入了Database类,我们在代码中可以直接用Database而不需要添加database.前缀。如果我们只有少量的模块和少量的类或方法,则这种写法不用在代码中添加模块前缀,会比较简单。但如果我们有很多模块、很多类或方法,则这种写法会让我们搞不清某个类或方法来自哪个模块,从而造成混淆。

如果因为某些原因,products已经有一个名为Database的类,而我们不想将这两个类名搞混,则可以将导入的类重命名:

我们也可以在一个语句中一次导入多个条目。如果我们的database模块中还包含一个Query类,则可以同时导入两个类:

我们也可以用下面的语法一次性地导入database模块中的所有类和函数,这样我们就可以在代码中直接使用模块中的所有类和函数而不用加模块前缀:

不要这样做! 每个有经验的Python程序员都会告诉你永远不要用这种语法(有些人会告诉你在某些情况下这种写法很有用,但我们不认同)。有一种方法,可以让你知道为什么要避免这种语法,你可以用这种语法写一些代码,然后过两年再回头看你的代码。但我们还是省点时间吧,不用把烂代码保存两年,现在就告诉你:不要用这种写法。

我们要避免这种写法的原因有如下几个。

· 当我们在文件的开头用from database import Database明确地导入database中的类时,可以清楚地看到Database这个类来自哪里。我们可能在文件的400行之后才用到db=Database(),可以通过import语句快速找到Database类的来源。然后,如果我们想弄清楚如何使用Database类,则可以浏览它所在的源文件(或者在交互式解释器中导入模块,用help(database.Database)命令查看帮助信息)。然而,如果你使用了from database import*语法,就需要花费更多的时间去找出这个类的位置。维护这样的代码简直就是一场噩梦。

· 如果存在有冲突的名称,我们就完蛋了。假设我们有两个模块,它们都有Database类。使用from module_1 import*和from module_2 import*意味着第二个import语句中的Database类会覆盖第一个import语句中的Database类。如果我们使用import module_1和import module_2,则可以使用模块名来区分module_1.Database和module_2.Database。

· 除此之外,如果用前面的两种导入语法,大多数编辑器能够提供额外的功能,例如代码补全、跳转到类定义的位置或者查看注释等。但是import*语法通常会搞乱编辑器的这些功能。

· 用import*语法也会将预料之外的对象带入我们的局部命名空间。除非模块中使用__all__变量来限定对外暴露的接口,import除了会导入所有模块自己定义的类和函数,也会导入这个模块本身所导入的所有类或模块。

模块中用到的每一个变量名(包括类名和函数名)都应该被明确地定义出处,不管是在模块中定义的还是从其他模块中导入的。不应该有凭空出现的魔法变量。我们应该总是能够立刻识别出当前命名空间中变量名的来源。我们可以向你保证,如果你用了这个邪恶的语法,总有一天你会遇到让你极度抓狂的时刻,这个类到底是从哪里来的?

说点好玩的,在交互式解释器中输入import this。它会打印一首诗(其中包含几个只有程序员才懂的笑话),诗的内容主要是Python大师们所推崇的程序设计理念。其中有一句“Explicit is better than implicit”(显式优于隐式)正好和本节讨论的内容相关。相比from module import*这样的隐式语法,使用显式的方式引入模块中的名称会让你的代码更易于理解。

2.3.1 组织模块

随着项目中的模块变得越来越多,我们可能会想要添加另一层抽象,为模块层级添加某种嵌套等级。但是,我们不能将模块添加到模块中,毕竟一个文件只能容纳一个文件,而模块就只是Python文件而已。

文件可以存储在目录下,模块也可以。一个包( package )是一个目录下模块的集合。包的名称就是目录的名称。我们只需要在目录下添加一个名为__init__.py的文件(通常是空文件),就可以告诉Python这个目录是一个包。如果忘记添加这个文件,我们就没办法从目录中导入模块了。

现在把我们的模块放入工作目录下的ecommerce包中,工作目录下还包含用于启动项目的main.py文件。此外,在ecommerce包中还包含一个用于处理各种支付方式的payments包。

在创建包的层级结构时,我们要多加小心。Python社区一般建议:扁平结构优于层级结构。在这个示例中,我们需要创建一个具有层级结构的包,这是因为我们有多种具有一定共性的支付方式,把它们放在一个包中会更清晰。

这个目录层级如下所示,源代码通常放在项目目录的src目录下(src是source的缩写):

src是项目目录下的其中一个目录。除了src,一般还会有docs、tests等目录用于存放文档和测试文件。目录下常常还会有用于 mypy 等工具的配置文件。我们会在第13章中再次讨论这个主题。

在引入包中的模块或类的时候,我们必须注意包的层级结构。在Python 3中,有两种导入模块的方法:绝对导入和相对导入。接下来我们分别学习它们。

绝对导入

绝对导入 是指定我们想要导入的模块、函数或类的完整路径。如果我们需要访问products模块中的Product类,则可以用下面这些语法进行绝对导入:

或者,我们可以指定导入包中模块内的特定的类:

或者,我们可以导入包中的整个模块:

import语句用点运算符(period operator)来分割包和模块。一个包是包含了模块名称的命名空间,就像一个对象是包含了属性名称的命名空间。

绝对导入的写法可以在任何模块中运行。我们可以用这一语法在main.py、database模块或某个paytments下的模块中成功实例化Product类。确实,只要这些包存在于当前Python环境中,就可以导入它们。例如,这些包也可以被安装到Python的site-packages目录下,或者通过修改PYTHONPATH环境变量来动态地告诉Python在导入时到哪些目录下搜索包和模块。

既然有这么多选择,那么我们该用哪种语法呢?这取决于你的个人爱好和具体的应用。如果products模块下有数十个我们需要用的类和函数,那么我们通常会先用from ecommerce import products语法来导入模块名,然后通过products.Product的形式访问每个类。如果products模块下只有一两个类是我们需要的,那么可以直接用from ecommerce.products import Product语法导入。你可以选择任何一种方式,重要的是要让你的代码容易阅读和扩展。

相对导入

在很深的包层级结构中使用同一个包下的相关模块时,指定长长的完整路径看起来有点儿愚蠢,这时就需要 相对导入 relative imports )。相对导入基于当前模块的相对位置来定位要导入的类、函数或模块。它只在复杂的包结构中各个模块之间相互导入的时候才有意义。

例如,如果我们想在products模块中导入与之相邻的database模块的Database类,就可以使用相对导入:

database前面的点号的意思是,“使用当前包内的database模块”。在这个示例中,当前包就是包含我们正在编辑的products.py文件所在的包,也就是ecommerce包。

如果我们正在编辑ecommerce.payments包中的stripe模块,则可能想要使用父包中的database包。这可以非常简单地用两个点号来实现,就像这样:

我们可以用更多的点号来访问更高的层级,但应该注意包的层级太多是一种不好的设计。当然,我们也可以先退回到上级包,再回到其他下级包。在ecommerce.contact包中有一个email模块,下面的语句可以向payments.stripe模块导入email模块内的send_email函数:

这里的导入使用了两个点号,也就是说payments.strip包的上一层,然后用正常的package.module语法回到contact包的email模块。

相对导入并不是很有用。在之前提到的“Python之禅”( Zen of Python ,可以通过在交互式解释器中运行import this获得)中也提出“扁平结构优于层级结构”。Python的标准库都相对扁平,只有少量的包,有层级的包就更少了。如果你熟悉Java,则会知道Java的包通常有很深的层级结构,但这是Python社区想要避免的。相对导入只在特定情况下比较有用,比如不同的包中有相同名称的模块时。如果你需要使用超过两个点来定位更上一层的包,则这通常意味着你应该把你的代码重新设计得更扁平一些。

通过整个包导入

最后,我们可以直接通过包导入代码,而不需要使用包中的模块。我们将会看到,虽然导入的是一个模块中的变量,但导入时不需要使用模块名。在这个示例中,我们的ecommerce包中有两个模块文件,名为database.py和products.py。database模块包含一个db变量,很多其他的模块都需要访问这个变量。如果可以用from ecommerce import db而不是from ecommerce.database import db导入代码,岂不是很方便?

还记得那个将目录定义为包的__init__.py文件吗?这个文件可以包含任何变量或类的声明,它们可以作为包的一部分被使用。在我们的示例中,如果ecommerce/__init__.py文件包含如下这行:

我们就可以使用如下的代码在main.py或其他任何文件中直接访问db属性:

可以将ecommerce/__init__.py文件看作ecommerce.py文件,就像这个文件是一个模块而不代表一个包。如果你将所有的代码放到同一个模块中,后来又决定将其拆分为一个包中的多个模块,可能会很有用。你可以把对外的接口都放在新包的__init__.py文件中,这样对外界模块来说,还是和同一个模块打交道,虽然代码被组织在多个模块或子包中。

但是,我们建议不要将太多代码放到__init__.py文件中。这个文件中不应该有具体的业务逻辑,就像from x import*语法一样,这样做会导致我们找不到某个变量的出处,直到最后检查__init__.py文件。

学习了模块的基本知识,我们现在看看模块中应该包含什么。规则其实很灵活(不像其他编程语言)。如果你熟悉Java,你会看到Python可以更自由、更清晰地组织有意义的代码。

2.3.2 组织模块内容

Python模块是一个重点概念。每一个应用或者Web服务都有至少一个模块,甚至最简单的Python脚本也是一个模块。在任何一个模块中,我们可以定义变量、类或函数,可以用非常方便的形式存储全局变量,并且不会引起命名空间上的冲突。例如,我们在不同的模块中导入了Database类并且进行了实例化,但是更合理的做法是只有一个全局的database对象(导入自database模块)。database模块看起来应该是这样的:

然后我们可以用前面讨论过的任意一个导入方法获取db对象,例如:

前面这个模块的问题在于,db对象会在导入模块时立即被创建,通常是在程序启动的时候。这通常并不是最理想的,因为连接数据库可能需要一点儿时间,这会降低启动速度,或者因数据库连接不可用而导致启动失败,因为我们需要读取配置文件。我们可以推迟创建数据库,直到真正需要的时候通过调用initialize_database()函数来创建模块层级变量:

类型提示Optional[Database]告诉 mypy 工具变量db可能为None,也可能有Database类的实例。Optional是在typing模块中定义的。它方便我们在程序的其他地方判定db变量是否为None。

global关键字告诉Python,initialize_database()方法内部的db变量就是我们刚刚在模块层级定义的全局变量。如果我们没有指定其为全局变量,Python会创建一个新的局部变量,它在函数退出时就会被丢弃,而不会改变模块层级变量的值。

我们还需要做出一个改动。我们需要引入整个database模块。我们不能直接引入模块内的db对象,因为它可能还没有被初始化。我们在db变量拥有有意义的值之前要确保已经执行了函数database.initialize_database()。如果我们要访问db对象,我们可以使用database.db。

一个常见的做法是,通过一个函数返回当前的数据库对象。我们在需要访问数据库的地方引入这个函数:

正如以上示例说明的,所有模块层级的代码都会在导入的时候立即执行。然而,使用class或def定义的类或函数代码,导入时只会创建相关的类或函数,内部代码只有在被调用时才会执行。对于要执行的脚本来说,有时候这可能会引起一些麻烦(例如,我们电子商务示例中的主脚本)。通常我们会先写一个有用的程序,然后发现需要从其他程序的模块中导入某些函数或类。然而,一旦我们导入它们,所有模块层级的代码就都会立即执行。如果我们没有注意,可能会执行被导入模块中的脚本代码,而实际上我们只想要访问其中的几个函数。

为了解决这一问题,我们通常将脚本代码放到一个函数中(根据惯例,这个函数一般被叫作main()),只有在将模块作为脚本运行时才会执行这一函数,在被其他脚本导入时则不会执行这一函数。我们可以通过把对main()函数的调用放在一个条件语句下来实现这一目的,如下所示:

这样写,Point类就可以被其他模块引入,而不用担心导入时触发脚本代码。只有我们直接执行这个模块时,才会调用main()函数。

这是因为,每个模块都有一个特殊变量__name__(记住,Python用双下画线标记特殊变量,如类中的__init__方法)存储模块被导入时的名称。如果直接通过python module.py执行这一模块,也就是说模块没有被导入,__name__将被赋值为字符串“__main__”。

把这当作一个原则:将所有的脚本代码包含在if__name__=="__main__":下,以防你写的函数将来有可能被其他模块导入。

方法定义在类里,类定义在模块里,模块存在于包中。这就是全部规则吗?

实际上并不是。这只是Python程序中一种典型的顺序,但并不是唯一可能出现的情况。类可以在任何地方被定义,通常在模块层级定义,但是也可以在一个函数或方法的内部定义,就像这样:

我们定义了一个Formatter类来代表格式化类的抽象。我们还没有使用过抽象基类(abstract base class,abc),这些会在第6章中详细讲解。在上面的代码中,我们提供了一个没有具体实现的format()方法,它有完善的类型提示,这样 mypy 可以明确知道方法的目的。

在format_string()函数内部,我们创建了一个内部类,它继承了Formatter类。这种继承关系指明了内部类所需要包含的方法。format_string()的参数formatter是Formatter类型的,内部类DefautFormatter又继承自Formatter类,这就把它们关联了起来,确保符合参数的类型要求。

我们可以这样执行这个函数:

format_string()函数接收一个字符串和一个可选的Formatter对象作为参数,然后将Formatter的格式应用到字符串上。如果没有提供Formatter实例,则自己在内部创建一个DefaultFormatter实例。由于这个类是在函数的内部创建的,从函数外部是无法访问这个类的。类似地,函数也可以被定义在其他函数内部,一般来说,任何Python语句都可以在任何时间执行。

这些内部类和函数通常是一次性的对象,不需要在模块层级用到它们,或者只有在某个方法内它们才是有意义的。但是,在Python的代码中,这种用法并不常见。

我们已经学习了如何创建类和模块。有了这些核心技能,我们就可以开始考虑如何创建有用的软件去解决实际问题。但是当程序或服务变得越来越大时,我们经常会碰到边界问题。我们需要确保对象尊重彼此的内部隐私,避免混淆纠缠,使复杂的软件变成一碗相互纠缠的意大利面条。我们希望每个类都是一个封装得很好的馄饨。接下来,让我们看看组织软件创建良好设计的另一个方面。 w5AJapTBwmlPTAyMsYutJzEOpzlxwwOdROyki7xjq9d2lRfDiNnBWXveGKanLuXX

点击中间区域
呼出菜单
上一章
目录
下一章
×

打开