PAYATHON 2026

如何在 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 启动程序。

备注:内容仅供参考。