请使用最新版本浏览器访问此演示文稿以获得更好体验。
在本章以前,我们一直将编写的代码放在一个 .py
源文件文件中,但已经尝试过使用 Python 的内建模块,如 math
和 random
。随着代码规模的增大,将不可避免地需要将代码放在多个 .py
源文件中,这能方便对代码的组织管理,这时就要使用模块的概念。
模块(Module)就是以 .py
结尾的 Python 源文件,它包含定义(变量、函数和类)和语句。模块是 Python 代码的组织单元,它实际上也是一种对象。模块对应一个名字空间,其中包含任意的对象。
要创建模块,只需将模块代码保存在文件扩展名为 .py
的文件中。以下是 say_hello.py
文件内容:
hello_words = {'en-US': 'Hello!', 'zh-CN': '您好!',
'zh-TW': '您好!', 'ja-JP': 'こんにちは',
'nl-NL': 'Hallo', 'ko-KR': '안녕하세요'}
def hello(lang='zh-CN'):
print(hello_words[lang])
该文件对应的模块名称为 say_hello
。模块命名和变量的命名习惯类似,即采用蛇形命名法。
要在其他文件中使用刚创建的模块,必须先使用 import
语句导入此模块,有多种不同的导入方法。
最常规的导入方式是 import module_name
,该语句会执行模块中的语句,进一步可以 module_name.name
的方式引用模块中的名称。如下为与 say_hello.py
同在一个目录下的 main.py
文件的内容:
import say_hello
say_hello.hello_words['it-IT'] = 'Ciao'
say_hello.hello('nl-NL') # Hallo
say_hello.hello('it-IT') # Ciao
在导入模块时,还可以在模块名后使用 import module_name as alias
为模块指定别名:
import say_hello as say
say.hello_words['it-IT'] = 'Ciao'
say.hello('nl-NL') # Hallo
say.hello('it-IT') # Ciao
可以使用 from module_name import name1, name2
的形式只导入模块中的特定名称到当前命名空间(namespace)。如果要同时导入多个名称,各项之间以逗号分割。如下所示:
from say_hello import hello_words, hello
hello_words['it-IT'] = 'Ciao'
hello('nl-NL') # Hallo
hello('it-IT') # Ciao
这时使用这些名称时不需要也不能使用前导的 module_name.
,即应该用 hello('nl-NL')
而不是 。say_hello.hello('nl-NL')
也可以使用 from module_name import *
的形式直接将模块中所有的公开名称(不以下划线 _
开头的名称)导入到当前命名空间:
from say_hello import *
hello_words['it-IT'] = 'Ciao'
hello('nl-NL') # Hallo
hello('it-IT') # Ciao
但这种方式常常会导入大量的未预料的名称,这些名称甚至会遮蔽已有的名称,因此通常不建议使用。
还可以使用 from module_name import name as alias
的形式为特定导入名称指定别名:
from say_hello import hello as hi
hi('nl-NL') # Hallo
当导入一个模块 module_name
时,会按如下顺序寻找该模块:
如果不是内置模块,会在 sys.path
目录列表中查找此模块,其查找顺序为:
PYTHONPATH
环境变量所列出的目录site-packages
目录,由 site
模块处理)。更多信息,请沿着以上链接进一步学习。
dir()
函数内置的 dir()
函数会返回经过排序的对象属性名称列表,可借此函数查看模块的属性:
import say_hello
print(dir()) # 没有实参时,返回当前本地作用域中的名称列表
# ['__annotations__', '__builtins__', '__cached__',
# '__doc__', '__file__', '__loader__', '__name__',
# '__package__', '__spec__', 'say_hello']
print(dir(say_hello)) # 有实参时,尝试返回该对象的有效属性列表
# ['__builtins__', '__cached__', '__doc__',
# '__file__', '__loader__', '__name__',
# '__package__', '__spec__', 'hello', 'hello_words']
可以进一步使用这些内置的名称,如下所示:
print(__name__) # __main__
print(__package__) # None
print(say_hello.__cached__)
# C:\Users\jin\PycharmProjects\module\__pycache__\say_hello.cpython-310.pyc
print(say_hello.__doc__) # None
print(say_hello.__file__)
# C:\Users\jin\PycharmProjects\module\say_hello.py
print(say_hello.__name__) # say_hello
使用 PyCharm 建立 Python 项目时,其默认生成的 main.py
文件具有以下代码:
if __name__ == '__main__':
...
'__main__'
始终是当前执行模块内置变量 __name__
的值。此 if
语句的条件确保 main.py
文件对应的 main
模块在被导入到其他模块后,其语句体不会被执行(因为被导入后该模块的 __name__
变量值是 'main'
而不是 '__main__'
)。这正是我们想要的,因为我们本不想让 main
模块被其他模块导入和执行,这使得该 if
语句的句体看起来像其他语言的 main()
入口函数。
sys
是 Python 中一个比较重要的内建模块,它提供一些和系统相关的变量和函数。其中 sys.modules
变量会存储已经加载的模块:
import say_hello, sys
print(sys.modules)
# {'sys': <module 'sys' (built-in)>,
# 'builtins': <module 'builtins' (built-in)>,
# ...
# 'say_hello': <module 'say_hello' from 'C:\\Users\\jin\\PycharmProjects\\module\\say_hello.py'>}
可以看出 sys.modules
是字典类型,它将模块名称映射到已经被加载的模块。可借此查看某一模块的信息:
print(sys.modules['say_hello'])
# <module 'say_hello' from 'C:\\Users\\jin\\PycharmProjects\\module\\say_hello.py'>
一个稍微有点规模的项目,其模块将不止一个,需要某种方法将这些相互关联的模块统一进行管理。对应于使用目录来管理文件,同样可以将多个模块文件所处的目录定义为一个新的东西:包,为了明确标示某个目录是一个包,需要在目录内放入一个 __init__.py
文件。
因此,简单来说,包(Package)是一个包含 __init__.py
文件的目录,是目录内模块的集合。包本身也是一个模块,相对来说,包内的模块又称为子模块(Submodule)。
如下面的目录结构中,my_pkg
目录对应一个包,而其中的 foo.py
和 bar.py
都对应一个子模块。
my_pkg/
├── __init__.py
├── bar.py
└── foo.py
注意包目录下必须有一个 __init__.py
文件,否则 Python 会把该目录当成普通目录而不是包了。最简单的情况下,该文件是一个空文件。该包的名称是 my_pkg
(注意不是 __init__
)。
进一步地,可以有多级目录,组成多级层次的包结构,其中的子目录也可以被称作是子包(Subpackage)。如下所示:
my_pkg/
├── __init__.py
├── bar.py
├── foo.py
├── sub_pkg1
│ ├── __init__.py
│ ├── bar.py
│ └── foo.py
└── sub_pkg2
├── __init__.py
├── bar.py
└── foo.py
以上的包 my_pkg
中又包含两个子包:sub_pkg1
和 sub_pkg2
。
当存在多层的树形结构后,就需要用多个点号来逐层表示包中的一些名称,如 my_pkg.sub_pkg2.foo.name
,具体使用方法取决于如何导入这些名称。外部代码可以通过如下方式导入包内定义的名称(其中方括号内的内容是可选的):
import package_name[.module_name [as alias]]
from package_name import module_name [as alias]
from package_name.module_name import name [as alias]
总之,包、包中模块、模块中的名称的导入方法基本上是一致的,可以灵活地导入这些名称,并为导入的名称指定别名。
为了便于说明如何导入包,假设我们项目的目录结构为:
my_project
├── main.py
└── my_pkg
├── __init__.py
├── bar.py
├── foo.py
├── sub_pkg1
│ ├── __init__.py
│ ├── bar.py
│ └── foo.py
└── sub_pkg2
├── __init__.py
├── bar.py
└── foo.py
即 main.py
对应项目的主模块。
当使用 import package_name.module_name
的形式导入包时,引用时必须使用模块的全名。假如 my_pkg.sub_pkg1.foo
模块中有一个函数 do_sth()
,则在 my_project/main.py
中可以按如下方法导入模块并使用此函数:
import my_pkg.sub_pkg1.foo
my_pkg.sub_pkg1.foo.do_sth() # 不能使用 foo.do_sth() 的形式
但当以 from package_name import module_name
形式导入时,引用时不必使用全名。如下所示:
from my_pkg.sub_pkg1 import foo
foo.do_sth() # 不能使用 my_pkg.sub_pkg1.foo.do_sth() 的形式
__init__.py
文件前面的各种包的 __init__.py
都是空文件。实际上,可以在该文件中放入包的初始化代码,当包本身或其所包含内容被导入时会执行其中的代码。不过一般会在该文件中定义一个 __all__
变量,它是一个字符串列表,列表中的每个元素是该包中包含的成员(模块、子包)名称。这样当在其他文件中用 from package_name import *
的形式导入该包的内容时,将只导入该列表中的成员。例如,假如 my_pkg/__init__.py
的内容如下:
__all__ = ['foo', 'bar']
则可以如下方式编写 my_project/main.py
:
from my_pkg import *
foo.do_sth() # 在上一行通过 * 导入的名称中已包含 foo
__init__.py
文件如果不想让他人以 from package_name import *
的形式导入包中特定的子模块,保持 __init__.py
为空文件即可。
实际上,也可以为其他不属于包的模块显式指定 __all__
变量,其列表中各项可以是变量、函数、类名称等定义项,从而控制以 from module_name import *
形式导入时具体导入的内容。
一个 Python 程序往往会使用别人已经编写好的代码库,这些代码库中包含包、模块和资源文件,并以某种机制打包、标注版本并发布到特定的软件仓库注册服务中心,以供人们下载和使用,这些代码库被称为发布包(Distribution Package),简称包(注意这里的包和上一节中的包并不是一回事)。
Python 有较多的软件仓库注册服务中心,如 PyPI、conda-forge,另外也有许多不同的包管理工具(或称依赖管理工具),如 pip 和 conda。PyPI(Python Package Index)是 Python 官方支持的软件仓库,相应地 pip 是 Python 默认安装和使用的包管理工具。本节将主要介绍如何利用 pip 从 PyPI 中下载和使用由 Python 社区开发和共享的发布包。
当我们以从 Python 官网下载安装包的方式安装 Python 后,默认也安装了 pip 工具,我们可以在命令行中通过如下两种方式验证 pip 是否安装,并查看其版本:
$ python -m pip --version
pip 23.2.1 from C:\Program Files\Python312\Lib\site-packages\pip (python 3.12)
$ pip --version
pip 23.2.1 from C:\Program Files\Python312\Lib\site-packages\pip (python 3.12)
第一个 python -m pip
命令使用 python
可执行文件将 pip
模块作为脚本运行,-m
选项告诉 Python 运行指定的模块作为脚本。第二个 pip
则使用 PATH
环境变量中的默认 Python 解释器执行 pip
模块,大多数情况下,两者的效果是一样的。
让我们从命令行输入 pip
、pip help
或 pip --help
,以查看该命令的简明帮助信息:
$ pip
Usage:
pip <command> [options]
Commands:
install Install packages.
download Download packages.
uninstall Uninstall packages.
freeze Output installed packages in requirements format.
inspect Inspect the python environment.
list List installed packages.
show Show information about installed packages.
check Verify installed packages have compatible dependencies.
config Manage local and global configuration.
search Search PyPI for packages.
cache Inspect and manage pip's wheel cache.
index Inspect information available from package indexes.
wheel Build wheels from your requirements.
hash Compute hashes of package archives.
completion A helper command used for command completion.
debug Show information useful for debugging.
help Show help for commands.
General Options:
-h, --help Show help.
--debug Let unhandled exceptions propagate outside the main subroutine, instead of logging them to stderr.
--isolated Run pip in an isolated mode, ignoring environment variables and user configuration.
--require-virtualenv Allow pip to only run in a virtual environment; exit with an error otherwise.
--python <python> Run pip with the specified Python interpreter.
-v, --verbose Give more output. Option is additive, and can be used up to 3 times.
-V, --version Show version and exit.
-q, --quiet Give less output. Option is additive, and can be used up to 3 times (corresponding to WARNING, ERROR, and CRITICAL logging levels).
--log <path> Path to a verbose appending log.
对于常使用命令行的人来说,以上信息已经一目了然了,尤其是其中的各个命令(Commands)明确列出了使用 pip 可以进行的各项操作。让我们依照这些命令,安装著名的科学计算软件包 numpy:
$ pip install numpy
Collecting numpy
Downloading numpy-1.26.2-cp312-cp312-win_amd64.whl.metadata (61 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 61.2/61.2 kB 155.4 kB/s eta 0:00:00
Downloading numpy-1.26.2-cp312-cp312-win_amd64.whl (15.5 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 15.5/15.5 MB 1.4 MB/s eta 0:00:00
Installing collected packages: numpy
Successfully installed numpy-1.26.2
很可能你的安装过程和上述不一样,比如对于 Windows 系统,可能出现如下结果:
$ pip install numpy
Defaulting to user installation because normal site-packages is not writeable
Collecting numpy
Downloading numpy-1.26.2-cp312-cp312-win_amd64.whl.metadata (61 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 61.2/61.2 kB 181.4 kB/s eta 0:00:00
Downloading numpy-1.26.2-cp312-cp312-win_amd64.whl (15.5 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 15.5/15.5 MB 434.9 kB/s eta 0:00:00
Installing collected packages: numpy
WARNING: The script f2py.exe is installed in 'C:\Users\jin\AppData\Roaming\Python\Python312\Scripts' which is not on PATH.
Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed numpy-1.26.2
警告信息很可能是因为你在安装 Python 时使用管理员权限为所有用户安装(默认安装到 C:\Program Files\Python312
目录),同时自动将 C:\Program Files\Python312\Scripts\
目录添加到 PATH
环境变量中;但在使用 pip 命令时,因不具备管理员权限,只能将包安装在用户脚本目录中,而该目录并没有被添加到 PATH
中,可能导致安装的程序没法被找到。对于这种情况,一般使用管理员权限启动命令提示符;也可以自行将警告信息中提示的用户脚本目录添加到 PATH
中。
另外,你在安装包时很可能遇到网络超时错误,这是因为 PyPI 服务器在国外,从国内访问的速度一般很低,这时可以设置使 pip 使用国内镜像源。国内有很多 PyPI 镜像源:
要临时使用某个镜像源安装某个包,可以使用类似如下方法:
$ pip install -i https://pypi.tuna.tsinghua.edu.cn/simple some-package
要将某个源设置为默认源,则可以运行命令:
$ pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
关于 pip 工具使用的更多细节,请参见其官方文档。
另外,请注意,我们这里只是以最简单的方式安装 Python 软件包,还有其他更灵活、更强大但也更复杂的方法,我们也可以把自己编写的 Python 软件在 PyPI 上开源发布,所有这一切,请参见官方的 Python 打包指南文档。
要求:
.py
源文件,请将这些源文件放在名称为 安模作业02-07-学号-姓名
的目录中,并将该目录打包为 ch07-学号-姓名.zip
形式的压缩文件,通过电子邮件以附件形式发给任课教师。安模作业02-07-学号-姓名
的形式。