1+1=10

记记笔记,放松一下...

Python Import机制乱谈(一)

在 Python 的发展历程中,import 机制经历了多次重要的变更和演进。各种细节复杂无比,让人倍感头疼...

例子

通过一些小例子梳理一下?

注意,本文所用环境:Windows 下 Python3.12。

例子1:先认识一下关键词

  • import
  • from
  • as
  • *

随便导入一个标准库:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import os.path
import os.path as op1
from os import path
from os import path as op2
# from os import *

print(os.path)
print(op1)
print(path)
print(op2)

文件保存在 a1.py,四种方式均导入同一个Module,直接执行

1
python a1.py

结果如下:

1
2
3
4
<module 'ntpath' (frozen)>
<module 'ntpath' (frozen)>
<module 'ntpath' (frozen)>
<module 'ntpath' (frozen)>

除了 from xxx import * 一般不建议使用之外,其他可以灵活使用。

每个模块都有 __file____package__ 两个属性,可以输出:

1
2
print(os.path.__file__)
print(os.path.__package__)

结果类似下面:

1
C:\Python312\Lib\ntpath.py

嗯,__package__ 可以为空。

例子2:简单模组导入

文件系统结构:

1
2
a2.py
m1.py

定义一个 Module m1.py

1
2
def add(a, b):
    return a + b

同目录下的应用程序 a2.py 中导入并使用它:

1
2
3
4
5
6
7
8
9
import m1
import m1 as M1
from m1 import add
from m1 import add as add2

print(m1.add(1,1))
print(M1.add(1,1))
print(add(1,1))
print(add2(1,1))

注意:不是相对路径导入!!之所以能找到 m1.py,是因为 a2.py 执行时其所在路径被加入到了 sys.path 路径中,可以输出看看:

1
2
import sys
print(sys.path)

结果类似下面这样:

1
['E:\\tt\\mypythontests', 'C:\\Python312\\python312.zip', 'C:\\Python312\\DLLs', 'C:\\Python312\\Lib', 'C:\\Python312', 'C:\\Python312\\Lib\\site-packages']

当然,如果愿意,也可以把m1.py丢到上面列出的其他文件夹中,或者直接放到哪个zip文件里面。

例子3:简单的包导入

文件系统结构:

1
2
a3.py
p1/__init__.py

创建文件夹p1并定义一个包文件 p1/__init__.py

1
2
def add(a, b):
    return a + b

在p1的同级目录下的应用程序 a3.py 中导入并使用它:

1
2
3
4
5
6
7
import p1
from p1 import add

print(p1.add(1, 1))
print(add(1, 1))

print(p1.__path__)

和前面的使用没有差异,只不过它 了一个属性__path__,用于定位包内的子包或模块。

例子4:简单的命名空间包导入

文件系统结构:

1
2
3
a4.py
d1_1/p2/addition.py
d1_2/p2/subtraction.py

在两个不同的位置下的p2文件夹中,分别创建

  • p2/addition.py
1
2
def add(a: int, b: int):
    return a + b
  • p2/subtraction.py
1
2
def subtract(a: int, b: int):
    return a - b

在d1、d2的同级目录下的应用程序 a4.py 中导入并使用它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), 'd1_1'))
sys.path.append(os.path.join(os.path.dirname(__file__), 'd1_2'))

import p2.addition
import p2.subtraction

print(p2.__path__)

print(p2.addition.add(1,1))
print(p2.subtraction.subtract(2,1))

注意,p2包的所有路径都需要添加的sys.path中。

输出结果(注意看这种情况下的其__path__属性):

1
2
3
_NamespacePath(['E:\\tt\\mypythontests\\d1_1\\p2', 'E:\\tt\\mypythontests\\d1_2\\p2'])
2
1

例子5:包内相对导入

文件系统结构:

1
2
3
a5.py
p3/addition.py
p3/do_addition.py

p3/do_addition.py 中实现加法:

1
2
def do_add(a: int, b: int):
    return a + b

p3/addition.py 中调用它来实现加法接口:

1
2
3
4
from .do_addition import do_add

def add(a: int, b: int):
    return do_add(a, b)

或者

1
2
3
4
from . import do_addition

def add(a: int, b: int):
    return do_addition.do_add(a, b)

在包外的应用程序a5.py中,正常使用它:

1
2
3
import p3.addition

print(p3.addition.add(1,1))

相对导入只能使用from .xxx import yyy 这种带有from的形式(根据层级可以有2个或3个点),而不能用 import .xxx。特别的,它不能在执行脚本中使用,比如在a5.py中不能使用相对导入,因为执行时其 __name__ 不是一个路径。

相对导入的背景,见 PEP328。这个变化在将python代码从python2移植到到python3还真的头疼。

例子6:模组作为脚本执行

文件系统结构:

1
2
p4/m2.py
p4/m3.py

其中,p4/m2.py定义一个函数:

1
2
def add(a: int, b: int):
    return a + b

p4/m3.py中使用它:

1
2
3
4
from .m2 import add

print(add(1,1))
print(__name__)

此时,由于存在相对导入,如果试图直接运行它:

1
python p4/m3.py

将直接遇到异常

1
2
3
4
Traceback (most recent call last):
  File "E:\tt\mypythontests\p4\m3.py", line 1, in <module>
    from .m2 import add
ImportError: attempted relative import with no known parent package

要正常运行它,需要作为模块运行:

1
python -m p4.m3

当然,此时,其__name__也不再是 __main__,输出结果:

1
2
2
__main__

如果将 p4/m3.py 重命名为 p4/__main__.py,执行会更简单:

1
python -m p4

模块(module)与 package(包)概念

import 用于导入一个 module,多个 module 组织成 package。

模块(module)

关于 Module,手册中如是说

An object that serves as an organizational unit of Python code. Modules have a namespace containing arbitrary Python objects. Modules are loaded into Python by the process of importing.

  • 是Pyton代码的组织单元
  • 有一个可包含任何Python对象的命名空间
  • 通过import机制导入

Module的常见形式:

  • debao.py
  • debao.pyc
  • debao.pyd【Windows only】
  • debao.so 【unix/linux/MacOS】
  • debao/__init__.py
  • debao.zip

曾经有过 debao.pyo 这种带优化的pyc文件,现在已经统一到 .pyc文件中。

按位置,大致可以分为:

  • 内置库(Builtin Libraries):编译进解释器
  • 标准库(Standard Libraries):位于Lib
  • Site库(Site-specific Libraries):位于Lib/site-packages
  • 其他:PYTHONPATH 或 sys.path 路径下
  • 用户自定义模块

包(package)

关于 Package,手册中如是说

A Python module which can contain submodules or recursively, subpackages. Technically, a package is a Python module with a path attribute.

  • 是一个Module(一个包含 __path__ 属性的Module)
  • 包含其他子Module和子Package

Package 又分为:

  • 常规包(regular package):也叫传统包(traditional package),比如,包含__init__.py文件的文件夹就是一个常规包。【Python1.5 正式引入Pakcage】
  • 命名空间包(namespace package):作为子package的容器存在,不包含__init__.py文件。详见PEP420。【Python3.3 正式引入命名空间Package】

模块文件名称.pyc、.pyd(.so)

.pyc 缓存

.pyc 文件是 Python 编译后的字节码文件,它包含了 Python 解释器可以直接执行的字节码。在 Python 程序首次导入一个模块时,Python 会自动将该模块编译成 .pyc 文件,并将其存储在一个特定的目录下(如 __pycache__ 目录)

比如:在前面的例子中,会生成如下缓存文件:

1
2
__pycache__/m2.cpython-312.pyc
__pycache__/m2.cpython-312.pyc

而在python早期时,.pyc 都是都是和 .py 文件肩并肩的,且没有.cpython-312这样的东西存在。

PEP3147: PYC Repository Directories中,详细描述了.pyc 文件为什么放置到__历史。

格式

1
<模块名>.<版本信息>.pyc

有助于不同版本python的缓存信息可以共存。

.pyd/.so 文件

在Windows下,一个pyd模块,完整的文件名到底怎么命名?

  • debao.pyd
  • debao.abi3.pyd
  • debao.cp312-win_amd64.pyd

后缀到底应该是什么?参考PEP0720通过EXTENSION_SUFFIXES可以看出端倪:

1
2
3
>>> import importlib
>>> importlib.machinery.EXTENSION_SUFFIXES
['.cp312-win_amd64.pyd', '.pyd']

Linux下结果:['.cpython-312-x86_64-linux-gnu.so', '.abi3.so', '.so']

更完整的一些列表?

1
2
>>> importlib.machinery.all_suffixes()
['.py', '.pyw', '.pyc', '.cp312-win_amd64.pyd', '.pyd']

Linux下结果:['.py', '.pyc', '.cpython-312-x86_64-linux-gnu.so', '.abi3.so', '.so']

abi3

比较奇怪,Linux下面出现 abi3 字样,而Windows下面却没有。这个和PythonC API 稳定性 相关:

  • Stable API
  • UnStable API:从Python3.12到Python3.12可能会变化

对稳定API,在Windows下使用时,需要连接到 Python3.dll 而不是 Python312.dll。

参考

Python