目录
Python模块概述
Python模块的搜索路径
Python模块导入语句
import
from import
from import as
import as
函数dir()
把模块当做脚本执行
重载模块
Python包
Python包的初始化
从Python包中导入*
子包
小结
这篇文章我们一起探索下Python的模块和包——两种模块化编程的重要实现方式。
模块化编程指的是把大而笨重的任务分解成独立的、小的、更便于操纵的子任务的过程。这样,像堆砌石块一样,单独的模块可以组装起来,构建大型的应用程序。
在大型的应用程序中使用模块化编程方式有以下优点:
简化:与要聚焦在整个问题不同,一个模块通常只聚焦在处理问题中相对小的一部分。如果你正在开发一个模块,那么你的大脑要处理的问题域要小得多,这使得开发更加简单,且不容易犯错。
可维护:模块的设计通常遵循对于不同的问题域,有清晰的逻辑边界的原则。如果模块的设计将相互依赖降低到最小,修改一个模块对整个应用程序的其他部分的影响就会最小(你甚至可以在不了解模块以外的东西的情况下修改这个模块)。这使得一个多人团队共同协作开发一个大型应用程序更加切实可行。
可重用:在某个模块中定义的功能很容易被应用程序的其他部分复用(通过定义合适的接口)。这避免了重复造轮子。
作用域:一个模块通常定义一个单独的命名空间,这避免程序的不同部分中的标识符冲突。(Python之禅中的信条之一就是——命名空间非常棒,我们要多用!)
函数、模块、包,都是Python中促进代码模块化的组成部分。
Python模块概述
实际上在Python中有三种方式实现一个模块:
可以用Python写一个模块;
可以用C实现一个模块,然后在运行时以动态库的方式加载,例如正则表达式模块re;
内建模块是包含在Python解释器里的,例如:itertools
以上三种模块的访问方式,是相同的,都是通过import语句。
本文我们主要讨论用Python写的模块。用Python写的模块非常酷,因为它简单直接,很容易写。你要做的仅仅是创建一个包含合法Python代码的文件,并以.py为扩展名命名。就是这么简单,你不需要使用额外的语法。
例如,你创建了一个叫mod.py的文件,内容如下:
mod.py
mod.py定义了几个对象:
s,一个数组
a,一个列表
foo,一个函数
Foo,一个类
按照下面的方式导入模块,我们就可以访问这些对象:
模块的搜索路径
仍以上面的例子为例,我们看下,当执行下面的语句时,发生了什么:
当Python解释器执行这一语句时,它将在指定的目录列表中搜索mod.py文件,指定的目录列表按照以下方式获得:
输入脚本所在的目录,如果是解释器是交互式运行,则选择当前目录;
在环境变量PYTONPATH中定义的目录列表(虽然PYTHONPATH的格式与操作系统有关,不过可以参考环境变量PATH);
Python在安装的时候就指定的与安装目录有关的目录列表。
模块的搜索路径可以通过一个叫sys的模块中的sys.path语句获取:
备注:sys.path的实际内容与安装方式有关,在你的电脑上可能与上述有所不同。
因此,要想让解释器找到你的模块,你需要做以下任意一件事:
将mod.py放在输入脚本所在的目录,或者,如果是交互式环境,放在当前目录。
修改环境变量PYTHONPATH使得mod.py所在的目录包含在PYTHONPATH目录列表中,或者将mod.py放到PYTHONPATH定义的目录中。
将mod.py放到安装Python的相关目录。取决于你的操作系统,你可能对该目录有写权限,也可能没有。
事实上,还有一种方式:你可以把mod.py放在任意目录,在运行时修改sys.path的值,使它包含mod.py所在目录。例如:你可以把mod.py放在C:Usersjohn,然后执行以下语句:
模块导入后,你将能通过模块的__file__属性知道它的位置。
__file__的目录部分必须在sys.path的目录列表中。
模块导入语句
调用方通过import语句访问模块的内容,有以下几种方式导入模块内容。
import
这是最简单的导入模块的方式。请注意,这并不代表调用者可以直接访问模块的内容,每个模块都有它的私有符号表,作为模块中定义的对象的全局符号表。所以说,每个模块定义了一个单独的命名空间。
语句import 仅仅把放进调用方的符号表中,模块中定义的对象仍然在模块的私有符号表中。对于调用方来说,只有通过“.”修饰符加上作为前缀,才能访问模块中的对象,下面将会加以说明。
在使用import语句声明后,mod被加入本地符号表中。因此,mod在调用方的本地上下文中有了含义:
但是s和foo仍然在模块的私有符号表中,在本地上下文中仍然是不具有意义的:
要想在本地上下文环境中访问模块中的对象,对象的名字要加上前缀mod:
如果想用一个import语句导入多个模块,需要用逗号分隔开:
from import
另一种导入方式允许从模块中直接导入单独的对象到调用方的符号表中:
执行上面的语句,可以在调用方的环境中直接引用,不需要使用前缀:
因为这条语句直接将模块中的对象名称加载到调用方的符号表中,任何同名的对象都会被覆盖:
甚至你可以通过下面的语句快速导入模块中的所有对象:
这条语句将会把中任何除了以“_”开头的名称外的其他对象名称导入调用方的符号表中。例如:
在大规模生产代码中,这种做法并不推荐。因为你这样做是在往本地符号表中大量增加名称。除非你对模块中的对象名称都很熟悉,确信模块中所有对象名称的引入不会引起命名冲突,否则你很可能会在不经意间覆盖已经存在的对象名称。但是,当你在使用交互解释器测试或者探索学习的时候,这一语法非常顺手。因为你不需要很多的输入,就可以访问模块中的任何对象。
from import as
我们也可以将模块中的对象导入,并以别名的方式命名,加入本地符号表中:
这样,我们可以直接导入模块对象名称,而不用担心与原有对象的名称冲突:
import as
你也可以导入整个模块,并对这个模块使用别名:
也可以在函数定义中导入模块内容。只有在函数被调用的时候,导入才会发生:
然而,在Python3中,不允许在函数定义中使用import *进行导入:
最后,我们可以使用try...except ImportError语句捕获导入模块失败的异常:
dir()函数
内建函数dir()返回某个命名空间里的一个名称列表。在参数缺省时,它返回当下本地符号表中按字母排序的名称列表:
请注意首次调用dir()是如何在解释器执行的时候列出命名空间中预定义并已经存在的名称的。随着新变量的定义(qux, Bar, x),再调用dir()的时候,它们就出现在列表中了。
这有助于使我们知道,当我们执行导入操作后,哪些对象名称被添加到命名空间中了:
当使用模块名称作为参数时,dir()列出该模块下的所有名称:
把模块当做脚本执行
任何包含模块的.py文件都是一个脚本,都可以像脚本一样执行。
mod.py文件定义如下:
可以以脚本的方式运行:
没有报错,显然是可以运行的。当然,上面的例子没有什么实际意义,因为模块只定义了对象,没有做任何事情,也没有产生任何输出。
我们修改mod.py,让它产生输出,并以脚本方式运行:
mod.py
现在更有意思一些了:
但是,当作为模块导入的时候,它也产生了输出:
这可能并不是你想要的。导入一个模块的时候产生输出并不常见。如果能够区分文件是当成模块导入还是当成脚本运行不是更好吗?答案显然是是。
当文件当成模块导入的时候,保留变量__name__将设置成模块名称,当文件当成独立的脚本运行的时候,保留变量将赋值为__main__。基于这样的事实,你就可以实时改变文件的行为:
mod.py
现在,如果你当成脚本运行,将得到如下结果:
如果你当成模块导入,将得到如下结果:
为了测试其中的函数,模块通常被设计成可以当成单独的脚本运行,这就是单元测试。例如,你创建了一个模块fact.py,其中包含函数fact,如下:
fact.py
这个文件可以当成模块,从其中导入函数fact:
但是,你也可以把它当脚本运行,在命令行中输入一个整型参数用来测试:
重载模块
出于效率的考虑,在解释器的一个会话周期里,模块只会加载一次。对于组成模块的主要部分,函数和类的定义来说这是没问题的。但是模块中也会有通常用于初始化的可执行语句。要注意,这些执行语句只有模块首次导入的时候才会执行。
考虑下面的模块mod.py:
mod.py
在随后的导入语句中,print()并没有被执行。(不仅仅赋值语句,打印语句也是。)
如果你对模块进行了改动,想要重新载入模块,你必须重启解释器,或者使用模块importlib中的函数reload():
Python包
假设你开发了一个包含许多模块的大型应用程序,随着模块数量的增长,如果你把他们堆在一个目录,那么你将很难管理它们,尤其当它们的名称或者函数功能相近的时候。你可能想要一种分组和管理它们的方法。
包使用点操作定义了一种分层结构的模块命名空间。就像模块避免全局变量的冲突一样,包也避免了模块名称之间的冲突。
创建一个包的方法十分直观,它利用了操作系统固有的文件分层体系。考虑下面的文件组织:
此处有一个叫pkg的文件夹,里面包含两个模块mod1.py和mod2.py,模块内容分别如下:
mod1.py
mod2.py
对于这一结构,如果pkg这个目录能被搜索到(该目录是sys.path中的一个),那么我们可以使用早已熟悉的语法,通过点操作符pkg.mod1,pkg.mod2访问模块:
你也可以这样导入模块:
理论上说,也可以直接导入包:
但是我们一般避免这么做。从语法上说这样的语句没有问题,但是它实际上什么都没做,它并没有把模块的信息添加到本地命名空间中:
要真正导入模块或模块中的内容,你还是得用上面的其他方法。
包的初始化
如果在包目录中放置一个叫__init__.py的文件,那么包或包中的模块被导入的时候,就会执行该文件。这样可以执行包初始化的代码,比如初始化包级别的数据。
例如,考虑下面的文件__init__.py:
__init__.py
我们把这个文件加入到包目录中,如下所示:
当包导入的时候,全局列表A被初始化:
包中的模块可以通过反过来导入这个全局变量来访问它:
mod1.py
__init__.py也可以用于自动导入包中的模块。像前面看到的一样,import pkg仅仅把包名pkg放到了调用方的本地符号表中,而包中的模块名却没有。但是如果你的__init__.py包含下面的内容:
__init__.py
那么当你执行import pkg的时候,pkg中的模块就被自动导入了:
备注:大多数Python文档中指出,创建包的时候,包目录下必须放置__file__.py文件。这在过去是对的。过去__init__.py必须存在来证明包被定义了。__init__.py中可以包含初始化代码或者是空的,但是该文件必须存在。
从Python3.3开始,引入了隐式命名空间包,它允许在创建包的时候不包含__init__.py文件。如果包有初始化操作,这个文件可以存在,但不是必须的。
从模块中导入*
为了接下来的讨论,前面定义的包扩展一下,包含另外几个模块:
包中定义了4个模块,内容如下:
mod1.py
mod2.py
mod3.py
mod4.py
你已经知道,当使用import *导入模块的时候,模块中的除了以下划线开头外的所有对象名称都被添加到本地符号表中:
对于包来说,也有类似的语句:
这将发生什么呢?
嗯哼,还不够。你可能期望解释器搜索包目录,找到所有包含的模块,并一一导入。然而事实并非这样。
实际上,当你使用from
import *语句的时候,Python遵循这样的原则:如果__init__.py文件中包含一个__all__变量,这个变量是一个包含模块名称的列表,那么出现在这个变量中的模块将被导入。
对于当前的例子,假设你创建了一个包含以下内容的__init__.py文件:
pkg/__init__.py
那么执行from pkg import *将导入四个模块:
使用import*对于导入包相比导入模块并没有更明显的优势。但是这样用至少使得使用者在使用import *的时候能够控制包导入时具体导入哪些模块。事实上,它的一个主要作用是禁止导入所有模块,就像你看到的一样,如果__all__不定义,它一个模块也不导入。
顺便提一下,也可以在模块中使用__all__,其作用类似,就是定义哪些对象被导入。例如,修改mod1.py:
mod1.py
现在从pkg.mod1中import*只会导入包含在__all__中的对象:
函数foo()被导入,而类Foo没有被导入,因为后者不包含在列表__all__中。
总之,__all__可以让包和模块来控制使用import*的时候究竟导入什么,区别是它们的默认行为是不同的:
对于包来说,如果__all__未定义,那么import *不导入任何模块;
对于模块来说,如果__all__未定义,那么import *导入所有对象名称(除了下划线开头的以外)。
子包
Python包可以以任意深度嵌套子包。我们对包pkg做如下修改:
4个模块(mod1.py, mod2.py, mod3.py, mod4.py)的定义保持不变,但是与原来不同,现在4个模块不是放在同一个包下面,而是分成两部分分别放在两个子包目录sub_pkg1, sub_pkg2下面。
导入方式和之前的一样,语法是类似的,只不过需要在包和子包之间用点操作符连接:
此外,一个子包中的某个模块可以访问兄弟子包中的对象(如果你需要的话。)例如,假设你想在mod3中导入和执行函数foo()(在模块mod1定义),你可以使用绝对路径导入:
pkg/sub_pkg2/mod3.py
或者你也可以使用相对路径导入,其中“..”表示包的上一层。从子包sub_pkg2的模块mod3.py看:
..表示父包pkg
..sub_pkg1表示父包pkg的子包sub_pkg1
pkg/sub_pkg2/mod3.py
小结
从这篇教程中,你可以了解到以下内容:
如何创建一个模块;
Python解释器从哪些目录搜索模块;
通过导入语句怎么访问模块中的对象;
如何创建一个可以作为单独脚本执行的模块;
如何把模块组织进包和子包;
如何控制包的初始化
免费PDF下载:Python cheap sheet(https://realpython.com/optins/view/python-cheat-sheet-short/ )
希望本教程能帮你更好地了解Python内建函数和许多市面上的第三方库函数的访问方法。
另外,如果你在开发自己的应用程序,创建自己的模块和包有助于将你的代码模块化,使得编码、维护和调试更加容易。
如果你想学习更多,查阅python.org上的文档:
导入系统(https://docs.python.org/3/reference/import.html )
Python教程:模块(https://docs.python.org/3/tutorial/modules.html )
我爱Python,Python使我快乐!
英文原文:https://realpython.com/python-modules-packages/
译者:周游
领取专属 10元无门槛券
私享最新 技术干货