在开发过程中,我们无法把所有代码、资源都放在同一个文件中。因此,模块导入在编码中是很常见的。无论是C++、Java,还是Python、Go。
可以把不同功能、不同模块进行分离,当使用的时候,可以通过import
关键字在一个模块中使用另外一个模块提供的能力,这能够大大提升代码的开发效率。
尤其是对于Python这种对于模块、工具包依赖较强的编程语言,这一点更为突出。
模块导入,这一点在Python中最为常见的东西对于很多人来说都不屑一顾,但是,你真的彻底理解Python中的import
吗?
我可以断言,绝大多数Python开发人员只是使用,却不知所以然。
本文,就来详细、彻底的介绍一下Python模块导入的使用。
模块与工具包
模块(modules)和工具包(packages)这2个概念首当其冲,经常被Python开发者混为一谈。
虽然,二者有很多相同之处,但是还是存在一定差异。因此,要想彻底理解import
,首先就需要理解模块与工具包的异同点。
模块
Python官网对于模块的定义如下:
模块具有一个包含任意Python对象的命名空间,通常用作Python代码组织单位的对象。
实际上,一个模块通常对应一个.py
文件,模块的真正功能是可以将其导入到其他代码中,并重复使用,例如:
>>> import math
>>> math.pi
3.141592653589793
第一行代码,通过import
把math模块导入到代码中,通过math.pi来调用pi这一属性。
这里需要注意,这里写的math.pi
不仅是单纯的pi
,它还把math
充当所有属性保持统一的命名空间。命名空间对于保持代码的可读性和组织性非常有用。
你可以利用 dir
来查看命名空间的内容:
>>> import math
>>> dir()
['__annotations__', '__builtins__', ..., 'math']
>>> dir(math)
['__doc__', ..., 'nan', 'pi', 'pow', ...]
除了上述直接导入,我们还可以导入模块下特定的部分:
>>> from math import pi
>>> pi
3.141592653589793
>>> math.pi
NameError: name 'math' is not defined
请注意,这里对比于前一种方式已经发生了一些转变。这里的pi
是放置在全局命名空间内,而不是math
的命名空间内。
包
同样,首先看一下Python官网对工具包的定义:
一个Python模块,可以包含子模块或递归地包含子包。从技术上讲,包是具有
__path__
属性的Python模块。
从定义上可以看出,包仍然是模块。但是,它们还是有一定的区别。
从编码上来讲,Python包需要在目录下创建一个名为__init__.py
的文件。
导入模块时,通常不会导入子模块和子包,但是,你可以通过添加__init__.py
来将需要导入的子模块和子包囊括进去。
绝对导入与相对导入
from ... import ...
这种导入当时在代码中经常会遇到,假如,我们有如下工程:
world/
│
├── africa/
│ ├── __init__.py
│ └── zimbabwe.py
│
├── europe/
│ ├── __init__.py
│ ├── greece.py
│ ├── norway.py
│ └── spain.py
│
└── __init__.py
当想要导入africa时可以这样:
from world import africa
也可以这样:
from . import africa
那么这里面的**点(.)**代表什么含义?
这里的点(.)就是一种相对导入,你可以理解为从当前包中导入africa。
相反,绝对导入语句中,需要明确命名当前包:
from world import africa
在编码过程中,你可以选择绝对导入,也可以选择相对导入。只不过,PEP 8风格指南中,建议使用绝对导入。
Python导入路径
这是一个需要重点理解的问题,很多开发者从接触Python开始就是用PyCharm,它对于导入路径已经进行了默认的配置,因此,开发者很难遇到无法导入的问题。
但是,当切换到VS Code、Sublime这些需要较多自行配置的开发工具之后,会发现无法导入,或者因为导入工具包带来的调用错误问题。
Python是如何找到它要导入的模块和包的?
你可以试着输出sys.path
,你会发现输出列表中主要包含如下3个部分的位置:
- 当前脚本目录
- PYTHON_PATH环境变量
- 其他与安装相关的目录
通常情况下,Python将对列表进行从头开发遍历,从每个位置中寻找给定的模块,直到第一个匹配为止。
由于脚本所在目录始终被排在列表的第一位,因此,导入模块时它会首先从当前目录下进行寻找。
所以,一定不要把自己的代码文件名称与工具包重名。
例如,当前目录有一个名为math.py
的文件:
# math.py
def double(number):
return 2 * number
这时候,你导入可以按照预期工作:
>>> import math
>>> math.double(3.14)
6.28
但是,它已经覆盖了Python自带的math标准库。如果我们误认为导入的是标准math模块,去调用pi
、sqrt
这些方法,则会报错:
>>> math.pi
Traceback (most recent call last):
File "", line 1, in
AttributeError: module 'math' has no attribute 'pi'>>> math'math' from 'math.py'>
因此,为了避免这个问题,一定小心自己开发代码文件的命名。
创建并安装本地工具包
在Python开发过程中,经常会用到pip
安装来自PyPI仓库的工具包,它可以用于全局的工程项目。
除了从仓库下载安装工具包,还可以自行在本地创建工具包,并完成安装。
创建本地安装包只需要创建setup.cfg
和setup.py
两个项目就行:
# setup.cfg
[metadata]
name = local_structure
version = 0.1.0
[options]
packages = structure
# setup.py
import setuptools
setuptools.setup()
这里的版本名称和版本号可以自行选择。
然后,可以执行下方命令,把创建的安装包安装到本地:
$ python -m pip install -e .
导入样式
为了保持代码的可读性和可维护性,PEP 8提出了一些针对模块导入的规则:
- 将导入放在文件的顶部
- 每个导入要分行
- 将导入分组:首先是标准库导入,然后是第三方导入,最后是本地应用程序或库导入
- 在每个组中按字母顺序排序导入
- 绝对导入优先于相对导入
- 避免使用通配符导入
from module import *
# Standard library imports
import sys
from typing import Dict, List
# Third party imports
import feedparser
import html2text
# Reader imports
from reader import URL
资源导入
除了模块和工具包,代码开发过程中,还会经常用到外部资源包,针对外部资源包的导入,可以使用importlib.resources
。
它是Python 3.7中的标准模块,使用它有2点好处:
- 使得导入方式更加一致
- 可以更轻松地访问其他包中的资源文件
例如,
>>> from importlib import resources
>>> with resources.open_text("books", "alice_in_wonderland.txt") as fid:
... alice = fid.readlines()
动态导入
Python是一门动态语言,这也是它的主要特点之一。
动态语言使得你可以在程序运行的时候做很多事情,可以添加属性、重新定义方法、更改模块的文档字符串。
例如,通过修改print()函数,使它不做任何操作:
>>> print("Hello dynamic world!")
Hello dynamic world!
>>> # Redefine the built-in print()
>>> print = lambda *args, **kwargs: None
>>> print("Hush, everybody!")
>>> # Nothing is printed
在上述示例中,print函数就被匿名函数重新定义了。
除了这种方法,还有更为易用的动态导入方式,就是利用importlib
。
先来看一段示例,
# docreader.py
import importlib
module_name = input("Name of module? ")
module = importlib.import_module(module_name)
print(module.__doc__)
import_module()
返回可以绑定到任何变量的模块对象。然后,您可以将该变量视为常规导入的模块。
$ python docreader.py
Name of module? math
This module is always available. It provides access to the
mathematical functions defined by the C standard.
$ python docreader.py
Name of module? csv
CSV parsing and writing.
在每种情况下,该模块都是通过动态导入的import_module()
。
周期性导入
如果两个或者多个模块互相导入时,就会发生周期性导入。
例如,有两个模块yin.py
和yang.py
:
# yin.py
print(f"Hello from yin")
import yang
print(f"Goodbye from yin")
# yang.py
print(f"Hello from yang")
import yin
print(f"Goodbye from yang")
尝试在交互式命令行下导入yin
时会发生下面情况:
>>> import yin
Hello from yin
Hello from yang
Goodbye from yang
Goodbye from yin
有些同学会疑惑,这样互相导入,难道不会无限循环下去吗?
这得益于Python的模块缓存机制,在导入yin
之后,会首先把它加入到缓存中,后续再导入,会先去参考缓存区域,避免无限循环。
推荐阅读
或许,这是最强大的一款Python GUI工具
8个小技巧教你提升Python代码质量
5款最强且免费的Python IDE
福利
最近我花费了半个月的时间,整理了1份理论+实践的计算机视觉入门教程,这或许是你见过最好的一份CV教程之一。独家打造、完全免费,需要的同学可以扫码添加我的个人微信,发送“CV”获取~