Skip to main content

模块

info

绝对导入和相对导入为 Python 包的组织提供了清晰的规则。明确的导入方式让代码的依赖关系更加清晰,避免了命名冲突。

# 绝对导入
from mypackage.submodule import function

# 相对导入
from .submodule import function
from ..parentmodule import other_function

PEP 328 – 导入:多行和绝对/相对导入

模块

模块表示一个或多个具有相关功能的 Python 代码的集合。

最小的模块是单个 Python 可执行文件。最常见的是 .py 后缀结尾的文件。

较大的模块往往是一个文件夹,内含多个 Python 可执行文件、文件夹与资源等等。

info

Python 的 import 不止能导入 .py 后缀结尾的文件

.pyd 是 Windows 特有的文件格式。它的作用和 .dll 相似,可直接导入而不需要 ctypes 模块。

.pyc 是由 py 文件经过编译后生成的二进制文件,py 文件变成 .pyc 文件后,加载的速度有所提高,并且可以实现源码隐藏。

Python 的 import 通过 importlib 模块自动处理这些不同格式的文件。

.so.dll 分别是 Linux 和 Windows 的动态链接库,可使用 ctypes 模块导入。

.pyx 是 Cython 的源代码文件,支持 Python 与 C 代码混合编程。可以将 .pyx 文件编译为纯 C 文件,但需要注意的是,这个 C 文件通常是用来创建一个 Python 扩展模块的,它不能独立运行。需要进一步编译为 .so.dll.pyd 文件。

.pyi 文件是 Python 接口文件(Python Interface file),也称为存根文件(stub file)。它包含模块的类型提示,但不包含实际的实现代码。当一个模块是用 C 或 Cython 编写的(例如,编译成 .pyd.so 文件),它的源代码不是 Python 格式,因此无法直接被 MyPy、Pyright 等类型检查器或 IDE 识别。这时,开发者会创建一个对应的 .pyi 文件来提供类型信息。

from,import,as

在导入模块时,可以使用fromimportas关键字来导入模块中的指定内容。

通过import关键字导入指定的模块。

通过from关键字从模块中导入指定内容。

通过as关键字给函数别名。

module.py
def foo():
print('goodbye, world!')
test.py
import module # 导入整个模块文件
module.foo()# 输出goodbye, world!


from module import foo # 从模块中导入foo函数
foo() # 输出goodbye, world!

from module import foo as foo2 # 导入模块中的foo函数,并别名为foo2
foo2() # 输出goodbye, world!

__name__ 属性

__name__ 属性是一个特殊的属性,用于获取模块的名称。

module.py
print(__name__)
"""
当模块被直接运行时,__name__的值为__main__,即便在多线程和协程中也是__main__
3.14多进程中,会将子进程的__name__设为__mp_main__

当模块被导入时,__name__的值为模块的名称,即module

如果你希望在模块被直接运行时执行一些代码,被导入时则不执行,可以这样写:
"""
if __name__ == "__main__":
print("This is the main module")

模块的其他特殊属性(魔法变量)

除了 __name__ 外,Python 模块还有一系列由解释器自动设置或约定的特殊属性,常用于内省、文档和加载逻辑。

属性说明
__doc__模块的文档字符串(即文件开头的多行字符串),若未定义则为 None
__file__加载该模块时使用的文件路径(若从文件加载)。交互式环境、内置模块或动态生成的模块可能没有此属性。
__package__模块所属的包名。顶层模块为 '',包内模块为包名(如 mypkg.sub);若无法确定则为 None
__path__仅对有效,为包内搜索子模块的路径列表(通常为包目录路径)。普通模块无此属性。
__loader__加载该模块的 loader 对象,属于 importlib.abc.Loader
__spec__该模块的 ModuleSpec 对象,描述模块名、loader、来源等,由 importlib 使用。
__dict__模块的命名空间(全局变量、函数、类等)组成的字典。
__annotations__模块级变量注解组成的字典(PEP 526),未注解则为空 dict。
__builtins__在模块全局作用域中,指向内置名称(通常为 builtins 模块或其中 dict)。
查看模块的部分魔法变量
# 在任意 .py 文件中或交互式环境中:
import sys
print(__name__) # 当前模块名或 '__main__'
print(__doc__) # 本模块的 docstring
print(__file__) # 本模块文件路径(若存在)
print(__package__) # 所属包名或 ''
print(__loader__) # 加载本模块的 loader
print(__spec__.name) # 模块名
print("__path__" in dir(sys.modules[__name__])) # 普通模块通常为 False

项目中可用 test/run.pytest/test.py 做对照实验:test.py 定义了一个带 docstring 的模块,run.pyimport test 后分别打印「当前模块 run」和「被导入模块 test」的 __name____doc____file____package____loader____spec__.name 以及是否包含 __path__。在项目根目录执行 python test/run.py 即可查看输出。

info
  • 内置模块(如 sysbuiltins)往往没有 __file__,因为不是从磁盘上的 .py 文件加载的。
  • 使用 importlib.util.spec_from_loader 等动态创建的模块,其 __file__ 可能为 None 或由 loader 自定义。
  • 包(含 __path__ 的模块)的 __file__ 通常指向该包的 __init__.py 所在目录。

__init__.py

通常情况下,当使用 import 语句导入模块后,Python 会按照以下顺序查找指定的模块文件:

当前目录,即当前执行的程序文件所在目录下查找;

到 PYTHONPATH(环境变量)下的每个目录中查找;

到 Python 默认的安装目录下查找。

以上所有涉及到的目录,都保存在标准模块 sys 的 sys.path 变量中,通过此变量我们可以看到指定程序文件支持查找的所有目录。

换句话说,如果要导入的模块没有存储在 sys.path 显示的目录中,那么导入该模块并运行程序时,Python 解释器就会抛出 ModuleNotFoundError(未找到模块)异常。

解决“Python 找不到指定模块”的方法可以是:

  • 向 sys.path 中临时添加模块文件存储位置的完整路径;
  • 将模块放在 sys.path 变量中已包含的模块加载路径中;
  • 设置 PYTHONPATH 系统环境变量。

具体区别可以创建下面的文件结构来了解:

top/
├── __init__.py
├── second.py
└── second_copy.py
info

在 Python 中,__init__.py 文件是一个特殊的文件,它标志着一个目录是一个 Python 包(package)。当 Python 解释器在导入一个模块时,如果发现一个目录里包含了 init.py 文件,它就会将这个目录当作一个包来处理。

__init__.py 文件本身可以为空,但它也可以包含初始化包的代码。例如,你可以在里面定义变量、函数,或者导入子模块,以便在包被导入时自动执行这些代码。这使得你可以控制包的初始化行为,比如设置包级别的配置或简化子模块的导入路径。

second_copy.py
import sys
print(sys.argv)
second.py
import sys
print(sys.argv)
from .second_copy import *

两种运行脚本的方式,以及对应的输出:

  1. 使用 python -m top.second 运行

    PS C:\Users\jiang\Desktop> python -m top.second
    ['C:\\Users\\jiang\\Desktop\\top\\second.py']
    ['C:\\Users\\jiang\\Desktop\\top\\second.py']
    • 输出两次相同的 sys.argv,显示脚本的完整路径。
    • 没有错误,脚本正常运行。
  2. 直接运行 python top\second.py

    PS C:\Users\jiang\Desktop> python top\second.py
    ['top\\second.py']
    Traceback (most recent call last):
    File "C:\Users\jiang\Desktop\top\second.py", line 3, in <module>
    from .second_copy import *
    ImportError: attempted relative import with no known parent package
    • 仅输出了 sys.argv 一次,显示的是相对路径 top\second.py
    • 然后抛出了 ImportError,提示“尝试进行相对导入,但没有已知的父包”。

为什么会有这样的差异?

问题的核心在于 Python 如何处理这两种运行方式,以及它们对模块结构和相对导入的影响。

1. python -m top.second 的行为
  • 运行方式:使用 -m 标志告诉 Python 将 top.second 作为一个模块运行。这里,top 被识别为一个包,second 是该包中的一个模块。
  • 包上下文:Python 会正确设置包的层次结构。由于当前工作目录是 C:\Users\jiang\Desktop,Python 知道 top 是一个包,并且 second.py 是其中的模块。
  • 相对导入:在 second.py 中,from .second_copy import * 是一个相对导入,. 表示当前包(即 top)。因为 Python 已经建立了包上下文,它能找到同一目录下的 second_copy.py,导入成功。
  • sys.argv 的值:在这种模式下,sys.argv[0] 被设置为脚本的完整路径,即 C:\\Users\\jiang\\Desktop\\top\\second.py
    • 首先,second.py 打印这个值。
    • 然后,导入 second_copy.py 时,second_copy.py 也打印 sys.argv,因为 sys.argv 是全局的,不会因模块不同而改变,所以输出两次相同的结果。
2. python top\second.py 的行为
  • 运行方式:直接通过文件路径运行 second.py,即将其作为独立的脚本执行,而不是作为一个包中的模块。
  • 包上下文缺失:在这种情况下,Python 不会将 top 视为一个包,而是直接运行 second.py 作为主模块(__main__)。因此,没有定义任何“父包”。
  • 相对导入失败second.py 中的 from .second_copy import * 依赖于包结构,但由于缺少包上下文,Python 不知道 . 代表什么,导致抛出 ImportError: attempted relative import with no known parent package
  • sys.argv 的值:在这里,sys.argv[0] 是命令行中提供的路径,即 top\second.py(相对于当前工作目录 C:\Users\jiang\Desktop)。
  • second.py 打印这个值后,尝试执行相对导入时失败,因此程序终止,second_copy.py 的代码未被执行。

__main__.py

接下来我们添加一个__main__.py文件。它相当于是为包提供一个脚本入口。

top/
├── __init__.py
├── __main__.py
├── second.py
└── second_copy.py
second_copy.py
def add_one(x):
return x + 1

包内模块正常相互引用。

second.py
from second_copy import add_one

def add_one_and_mul_two(x):
return add_one(x) * 2

__main__.py 作为入口文件,当输入python -m 项目名便会触发该文件,整个项目即可化身一个脚本。可以接收参数,并调用包内模块。

__main__.py
import argparse
from second import add_one_and_mul_two

parser = argparse.ArgumentParser()
parser.add_argument("x", type=int)
args = parser.parse_args()
print(add_one_and_mul_two(args.x))

运行方式:

python -m top 100

# 先加1再乘2,输出:
# 202

垃圾回收机制

如果持续不断加载数据,调用函数模块,计算机的内存会溢出,Python 的垃圾回收机制采用引用计数机制,当一个对象的引用数为 0 时,它就会被垃圾回收机制回收。

import sys
# 生以下四种情况的时候,该对象的引用计数器+1
a= "hello" # 对象被创建  
b=a # 对象被引用 
def func(a):
return
func(a) # 对象被作为参数,传到函数中
List=[a,"a","b",2] # 对象作为一个元素,存储在容器中  
sys.getrefcount(a)
# python系统内部很多地方都在使用一些常用的对象,这些对象在python解释器启动时就被创建出来。

#发生以下四种情况时,该对象的引用计数器**-1**

#该对象的别名被显式销毁时 
del a
#该对象的引别名被赋予新的对象,  
a = "world"
#个对象离开它的作用域,例如 func函数执行完毕时,函数里面的局部变量的引用计数器就会减一(但是全局变量不会)
#该元素从容器中删除时,或者容器被销毁时。
b = a # 当前计数器为2
del b # 删除变量b:b对应的对象的引用计数器-1 (此时计数器为1)
del a # 删除变量a:a对应的对象的引用计数器-1 (此时引用计数器为0)

# 当引用计数器为0 时,意味着没有人再使用这个对象,这个对象就变成垃圾,垃圾回收。
# 回收:1. 对象从refchain的链表移除。
# 2. 将对象进行销毁,内存归还给操作系统,可用内存就增加。
sys.getrefcount(a)

内置函数

__import__ 函数

函数签名:__import__(name, globals=None, locals=None, fromlist=(), level=0) -> module

参数说明:

  • name:要导入的模块名
  • globals:全局命名空间
  • locals:局部命名空间
  • fromlist:要导入的属性列表
  • level:导入级别

返回值:

  • 返回导入的模块
math = __import__('math')

print(math.sqrt(16)) # 4.0

Python 导入 C 模块

info

Python社区的发展依赖于开放、透明的决策过程。每个PEP都经过充分讨论,这种民主化的开发方式保证了Python的长期健康发展。

当你遇到"为什么Python要这样设计"的问题时,PEP往往能给你答案。

PEP 1 – PEP目的和指南

Python 的底层是 C 写的(实际上大部分高级编程语言都是 C 写的)因此Python可以调用以下C/C++文件类型:

  • C源代码文件(.c)
  • C++源代码文件(.cpp、.cxx、.cc)
  • 编译后的共享库(Linux/Unix的.so、Windows的.dll、macOS的.dylib)
  • 编译后的静态库(Linux/Unix的.a、Windows的.lib)

因此互相调用的逻辑主要是:数据类型转换、编译库的链接、接收返回值。

python+c/c++混合编程如:

原生的 Python.h

cython

pybind11:pytorch 也采用该方法

ctypes、cffi、SWIG、Boost.Pytho 等

但不论是哪个方法,大致的流程都是:转换数据类型->编译代码->生成编译后的文件(.pyd .pyc .pyo .so .dll 等)

代码示例

一个求某个数可以分解为多少个质数之和代码,其中最核心的代码是判断一个数是否为质数。我们使用C语言实现这个被频繁调用的功能。

prime.c
#include <math.h>

int is_prime(int n) {
if (n < 2) return 0;
for (int i = 2; i <= (int)sqrt(n); i++) {
if (n % i == 0) return 0;
}
return 1;
}

编译:gcc -shared -o prime.so prime.c

import ctypes
import timeit

# 加载C动态库
prime_lib = ctypes.CDLL('./prime.so')
is_prime = prime_lib.is_prime
is_prime.argtypes = [ctypes.c_int]
is_prime.restype = ctypes.c_int

def count_prime_pairs(n):
count = 0
for i in range(2, n // 2 + 1):
if is_prime(i) and is_prime(n - i):
count += 1
return count

def is_prime_py(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True

def count_prime_pairs_py(n):
count = 0
for i in range(2, n // 2 + 1):
if is_prime_py(i) and is_prime_py(n - i):
count += 1
return count

if __name__ == "__main__":
n = int(input("输入一个正整数: "))
print("C混合版结果:", count_prime_pairs(n))
print("纯Python结果:", count_prime_pairs_py(n))

# 性能对比
py_time = timeit.timeit(lambda: count_prime_pairs_py(n), number=100)
c_time = timeit.timeit(lambda: count_prime_pairs(n), number=100)
print(f"Pure Python: {py_time:.4f} seconds")
print(f"Python+C: {c_time:.4f} seconds")
tip

想把Python代码封装为可执行文件,可以使用PyInstaller。

官方文档:https://www.pyinstaller.org/

命令行:

# 直接封装
pyinstaller -F app.py
# 指定图标
pyinstaller -F -i app.ico app.py
# 指定图标 不展示终端框
pyinstaller -F -i app.ico app.py --noconsole
# 将数据文件添加到捆绑包中,中间使用分号分隔,前面是源目录地址,后面是目的目录地址
pyinstaller -F -i app.ico app.py --add-data="C:\mediapipe\modules;mediapipe/modules" --noconsole

标准库推荐

  • ensurepip: 确保 pip 安装器可用,在 Python 安装时自动安装 pip。
  • venv: 创建和管理虚拟环境,隔离项目依赖,避免包冲突。
  • importlib: 提供动态导入模块的接口,支持以编程方式加载、重载模块。
  • argparse: 构建命令行接口的标准解决方案。
  • getpass: 安全获取用户密码输入,输入时不回显到终端。
  • locale: 提供本地化/国际化支持,处理不同语言、地区的数字、日期、货币格式。