如何在 Python app 的 sub-packages 间共享配置文件
结论
项目只需保留一个 config.ini,由包根目录中的 Python 模块统一读取。其他子包直接导入该配置模块,无需重复读取配置文件。
按照当前目录结构,one_kind_of_motor.py 应使用:
from ..CONFIG import config
也可以改用绝对导入:
from name_of_my_app.CONFIG import config
程序需要以包的形式启动。进入 name_of_my_app 的上一级目录后运行:
python -m name_of_my_app.main
以下方式会直接执行脚本:
python name_of_my_app/main.py
在 VS Code 中直接运行 one_kind_of_motor.py 也是如此。Python 会把该文件视为独立脚本,无法判断它属于哪个包,.. 因而没有明确含义,最终报错:
ImportError: attempted relative import with no known parent package
推荐的目录结构
Python 模块名通常采用小写,可以将 CONFIG.py 改为 config.py:
project_root/
└── name_of_my_app/
├── __init__.py
├── main.py
├── config.py
├── config.ini
├── motors/
│ ├── __init__.py
│ └── one_kind_of_motor.py
└── measurement_devices/
├── __init__.py
└── one_kind_of_measurement_device.py
project_root 是 name_of_my_app 的上一级目录,运行命令时应位于这个目录。
统一读取配置
假设 config.ini 内容如下:
[motor]
port = COM3
speed = 1200
enabled = yes
[measurement_device]
port = COM4
sample_rate = 100
在 config.py 中读取配置:
from configparser import ConfigParser
from pathlib import Path
CONFIG_FILE = Path(__file__).resolve().with_name("config.ini")
config = ConfigParser()
loaded_files = config.read(CONFIG_FILE, encoding="utf-8")
if not loaded_files:
raise FileNotFoundError(f"Configuration file not found: {CONFIG_FILE}")
Path(__file__) 指向 config.py 所在的位置,以此查找 config.ini,不会受到当前工作目录的影响。不要只写:
config.read("config.ini")
这段代码会从当前工作目录中查找文件。一旦换到其他目录启动程序,就可能找不到配置。
Python 第一次导入 name_of_my_app.config 时,会执行模块中的代码,并把模块缓存在 sys.modules 中。之后使用同一个完整模块名导入,通常会直接复用已经加载的模块,各个硬件模块不会分别读取一次配置文件。
在 motors 中使用配置
motors/one_kind_of_motor.py 可以写成:
from ..config import config
class Motor:
def __init__(self):
self.port = config.get("motor", "port")
self.speed = config.getint("motor", "speed")
self.enabled = config.getboolean("motor", "enabled")
def connect(self):
print(f"Connecting to motor on {self.port}")
这里的 .. 会从当前包 name_of_my_app.motors 返回上一级包 name_of_my_app,再导入其中的 config 模块。
也可以使用绝对导入:
from name_of_my_app.config import config
绝对导入能直接看出模块的来源,项目规模扩大后一般也更容易维护。
在 measurement_devices 中使用配置
measurement_devices/one_kind_of_measurement_device.py 可以这样写:
from ..config import config
class MeasurementDevice:
def __init__(self):
self.port = config.get("measurement_device", "port")
self.sample_rate = config.getint(
"measurement_device",
"sample_rate",
)
两个子包导入的是同一个配置模块,使用的也是同一个 ConfigParser 对象。
main.py 的写法
采用相对导入时,main.py 可以写成:
from .motors.one_kind_of_motor import Motor
from .measurement_devices.one_kind_of_measurement_device import (
MeasurementDevice,
)
def main():
motor = Motor()
device = MeasurementDevice()
print(motor.port)
print(device.port)
if __name__ == "__main__":
main()
从 project_root 运行:
python -m name_of_my_app.main
也可以将导入语句全部改为绝对导入:
from name_of_my_app.motors.one_kind_of_motor import Motor
from name_of_my_app.measurement_devices.one_kind_of_measurement_device import (
MeasurementDevice,
)
启动时仍建议使用:
python -m name_of_my_app.main
.、.. 和 ... 分别表示什么
在 name_of_my_app.motors.one_kind_of_motor 中:
from . import something
. 表示当前包:
name_of_my_app.motors
使用:
from .. import something
.. 表示上一级包:
name_of_my_app
而:
from ... import something
... 会继续向上一层查找。按照当前结构,这已经超出顶层包 name_of_my_app,通常会报错:
ImportError: attempted relative import beyond top-level package
因此,从 motors 导入包根目录中的配置模块时,应使用两个点:
from .. import config
也可以直接导入模块中的对象:
from ..config import config
相对导入能否工作还取决于模块的启动方式。直接执行 one_kind_of_motor.py 时,模块名会变成 __main__,Python 无法判断 .. 对应哪个父包,于是报告 “no known parent package”。
为什么添加 __init__.py 后仍然报错
__init__.py 可以让目录具备普通 Python 包的结构,但不会改变文件的启动方式。
例如,直接执行下面的命令仍可能失败:
python name_of_my_app/motors/one_kind_of_motor.py
此时 Python 仍将该文件作为独立入口运行,没有通过包名加载它。
如果确实需要单独运行这个模块,可以在 project_root 中执行:
python -m name_of_my_app.motors.one_kind_of_motor
硬件实现模块通常由 main.py 作为库调用,一般不需要分别作为脚本启动。
在 VS Code 中以模块方式启动
VS Code 的运行按钮或部分 Code Runner 配置会直接执行当前文件。可以在 .vscode/launch.json 中将入口设置为模块:
{
"version": "0.2.0",
"configurations": [
{
"name": "Run hardware app",
"type": "debugpy",
"request": "launch",
"module": "name_of_my_app.main",
"cwd": "${workspaceFolder}"
}
]
}
以上配置假设 VS Code 打开的是 project_root。入口应使用:
"module": "name_of_my_app.main"
不要使用:
"program": "${file}"
也可以直接在 VS Code 终端中运行:
python -m name_of_my_app.main
Windows、Linux 和 macOS 都遵循相同的导入规则。
注意事项
- 各个硬件模块无需重复解析同一个
config.ini。 - 不建议通过修改
sys.path绕过包结构问题,例如手动追加父目录。这类代码依赖运行位置,后续很难排查。 - 配置文件路径应根据
__file__计算,避免依赖当前工作目录。 ConfigParser.getint()、getfloat()和getboolean()可以直接完成常用的类型转换。ConfigParser是可修改对象。如果配置仅供读取,应避免业务模块随意改写。- 如果以后将项目制作成可安装的 wheel,需要把
config.ini声明为 package data。只把文件放在源码目录中,不一定会自动打包。 - 如果
config.ini保存的是需要由用户修改的运行配置,更适合将文件放在用户指定的位置,再通过命令行参数或环境变量传入路径。随程序发布的配置则更适合作为默认值。
当前项目可以由 config.py 读取一次配置,子包通过 from ..config import config 使用配置,再以 python -m name_of_my_app.main 启动程序。
备注:内容仅供参考。
