春江暮客

春江暮客的个人学习分享网站

Python 相对导入报错:用 -m、包入口和 Linux 启动脚本修复

2026-11-02 技术
Python 相对导入报错:用 -m、包入口和 Linux 启动脚本修复

把一个 Python 脚本拆成包后,运行 python3 triage_demo/cli.py,却报 ImportError: attempted relative import with no known parent package。文件明明都在,问题往往不是少装了依赖,而是启动方式没有建立包上下文。

本文面向初级到中级读者,用一个文本行数工具复现错误,再改成 python3 -m 入口。最后加一个固定工作目录的 Linux 启动脚本,并验证换目录、空格文件名和失败退出;不安装第三方包,不修改系统配置。

1. 方法:区分文件入口与模块入口

Python 包内相对导入说明解释了点号导入与当前包的关系。这里的 from .helpers import count_lines 指的是同一个包中的 helpers,不是“当前工作目录下的文件”。

  1. python3 triage_demo/cli.py 把文件作为入口,本例没有可供相对导入使用的父包。
  2. python3 -m triage_demo.cli 通过模块名启动,保留 triage_demo 包上下文。
  3. python3 -m triage_demo 则运行包里的 __main__.py,可以作为统一命令入口。

-m 官方文档要求使用模块名,不加 .py 或路径分隔符。本文在未安装包的源码目录中运行,不依赖全局 PYTHONPATH,也不在业务代码里插入 sys.path。

2. 准备环境与最小包

需要已有的 Python 3.9 或更新版本,以及 Linux 的 sh、bash 或 zsh。以下示例在 Python 3.11.4 上验证;所有文件都放进新建的演示目录。重复实验时换一个目录名。

  1. 在同一个终端中运行:
python3 --version
python3 --help
mkdir relative-import-demo
cd relative-import-demo
demo_root="$PWD"
mkdir triage_demo data
: > triage_demo/__init__.py
printf 'alpha\nbeta\ngamma\n' > data/items.txt
  1. 把下面保存为 triage_demo/helpers.py。按文本行计数,空行也算一行,不是 CSV 记录统计器:
from pathlib import Path


def count_lines(filename):
    with Path(filename).open(encoding="utf-8") as source:
        return sum(1 for _ in source)
  1. 保存为 triage_demo/cli.py:
import argparse
import sys

from .helpers import count_lines


def main():
    parser = argparse.ArgumentParser(description="Count UTF-8 text lines.")
    parser.add_argument("input", help="text file to read")
    args = parser.parse_args()
    try:
        lines = count_lines(args.input)
    except (OSError, UnicodeError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(f"lines={lines}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

__init__.py 在这个例子中是空文件,用来构成普通包。main() 处理命令行,读取失败向 stderr 写入错误并返回 1;缺少参数由 argparse 返回 2。若要扩展多个操作,可参考 argparse 子命令与参数校验。

3. 复现错误,再用 -m 修复

  1. 保持在 relative-import-demo,故意直接运行文件:
if python3 triage_demo/cli.py data/items.txt; then
  printf 'Unexpected success: inspect the package and environment\n'
else
  printf 'Expected failure: direct file execution lost package context\n'
fi

traceback 应包含:

ImportError: attempted relative import with no known parent package

if 包住了这次预期失败,避免开启 set -e 的 shell 提前退出。仅仅添加 if __name__ == "__main__" 不会修复包上下文;本例已经有入口保护,仍在导入处失败。

  1. 不改导入代码,改用模块名启动:
python3 -m triage_demo.cli --help
python3 -m triage_demo.cli data/items.txt

第一条显示帮助,第二条输出 lines=3,退出码为 0。想确认正常导入时的模块名和父包,可运行:

python3 -c 'import triage_demo.cli as m; print(m.__name__); print(m.__package__)'

预期分别是 triage_demo.cli 和 triage_demo。不要把相对导入改成裸 from helpers import ... 来勉强适配一种启动方式;这会改变模块身份和查找位置。

4. 增加包入口与稳定的 Linux 启动脚本

  1. 把下面保存为 triage_demo/__main__.py。它只转交给已有的 main(),不复制业务逻辑:
from .cli import main

if __name__ == "__main__":
    raise SystemExit(main())

Python 的包入口说明介绍了 __main__.py。本例加上入口保护,使导入这个文件时也不会解析命令行或启动任务。

python3 -m triage_demo data/items.txt

仍应输出 lines=3。-m 不会自动寻找任意磁盘目录中的项目。 本文的包没有安装,必须从包目录的父目录启动;在其他目录运行同样的命令,可能报找不到 triage_demo。

  1. 在演示目录根部保存 launch.sh:
#!/bin/sh
set -eu
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
cd -- "$project_dir"
exec "${PYTHON:-python3}" -m triage_demo "$@"

脚本先切到自身所在目录,再以模块名执行;"$@" 保留参数边界,exec 让 Python 的退出码直接传给调用者。PYTHON 可指定一个已有解释器的绝对路径,未指定时使用 PATH 中的 python3。它是单个可执行文件路径,不是可以混入参数的命令字符串。

  1. 从另一个工作目录调用:
chmod u+x launch.sh
(
  cd /
  sh "$demo_root/launch.sh" data/items.txt
)

预期仍是 lines=3。括号让 cd / 只影响子 shell。这个启动脚本约定输入相对路径从项目根目录计算;若要读取调用者目录里的文件,请传绝对路径。只执行可信项目,不通过符号链接启动本例脚本,也不要把它当作适用于所有打包布局的安装器。

真实定时任务还要固定解释器并处理环境差异,见 Linux 定时任务路径与环境排查。本文只演示启动,不修改调度器。

5. 自动验证成功与失败分支

把下面保存为根目录的 verify_demo.py,与 launch.sh 同级。它读取前面创建的包,在自己的临时目录放测试输入,结束后自动清理;不会删除你的演示目录。

from pathlib import Path
import os
import subprocess
import sys
import tempfile

root = Path(__file__).resolve().parent
env = os.environ.copy()
for key in list(env):
    if key.startswith("PYTHON"):
        del env[key]
env["PYTHON"] = sys.executable


def run(args, cwd):
    return subprocess.run(
        args, cwd=cwd, env=env, capture_output=True,
        text=True, timeout=10, check=False,
    )


with tempfile.TemporaryDirectory(prefix="module-entry-test-") as tmp:
    outside = Path(tmp)
    source = outside / "input with spaces.txt"
    source.write_text("alpha\n\ngamma\n", encoding="utf-8")
    path = str(source)

    direct = run([sys.executable, "triage_demo/cli.py", path], root)
    assert direct.returncode == 1 and not direct.stdout, direct
    assert "attempted relative import" in direct.stderr, direct

    for module in ["triage_demo.cli", "triage_demo"]:
        good = run([sys.executable, "-m", module, path], root)
        assert (good.returncode, good.stdout, good.stderr) == (0, "lines=3\n", ""), good

    foreign = run([sys.executable, "-m", "triage_demo", path], outside)
    assert foreign.returncode != 0 and "No module named triage_demo" in foreign.stderr, foreign

    wrapped = run(["sh", str(root / "launch.sh"), path], outside)
    assert (wrapped.returncode, wrapped.stdout, wrapped.stderr) == (0, "lines=3\n", ""), wrapped

    help_result = run([sys.executable, "-m", "triage_demo", "--help"], root)
    assert help_result.returncode == 0 and "Count UTF-8 text lines." in help_result.stdout, help_result
    no_arg = run([sys.executable, "-m", "triage_demo"], root)
    assert no_arg.returncode == 2 and "required" in no_arg.stderr, no_arg
    missing = run([sys.executable, "-m", "triage_demo", str(outside / "missing.txt")], root)
    assert missing.returncode == 1 and missing.stderr.startswith("ERROR:"), missing
    bad = outside / "bad.txt"
    bad.write_bytes(b"\xff")
    encoding = run([sys.executable, "-m", "triage_demo", str(bad)], root)
    assert encoding.returncode == 1 and "ERROR:" in encoding.stderr, encoding
    imported = run([sys.executable, "-c", "import triage_demo.cli; import triage_demo.__main__; print('import OK')"], root)
    assert (imported.returncode, imported.stdout, imported.stderr) == (0, "import OK\n", ""), imported

print("OK: direct failure, module entry, package entry, foreign cwd, launcher, help, errors, safe import")
python3 verify_demo.py

预期输出:

OK: direct failure, module entry, package entry, foreign cwd, launcher, help, errors, safe import

测试检查了直接执行失败、两种模块入口成功、错误工作目录失败、启动脚本成功、空格路径、空行计数、帮助、缺少参数、文件不存在、坏 UTF-8 和安全导入。子进程有 10 秒等待上限,测试移除了继承的 PYTHON* 设置,再为启动脚本指定当前解释器,以隔离外部路径设置的干扰。

6. 常见错误与具体修复

现象 处理
attempted relative import with no known parent package 从项目根目录用 python3 -m triage_demo.cli,不要直接执行包内文件。
No module named triage_demo 确认当前目录包含 triage_demo/;改用绝对路径调用 launch.sh。已安装项目则检查对应解释器环境。
No module named triage_demo.__main__ 创建第 4 节的 __main__.py,或直接运行 -m triage_demo.cli。
attempted relative import beyond top-level package 检查点号层数和真实包层级;换启动方式不能修复写错的包关系。
包能加载,但读不到输入文件 本文启动脚本以项目根目录解释相对路径,改传输入的绝对路径。
模块名称正确,来源却不对 检查本地同名文件或包,按模块遮蔽与导入路径排查确认实际来源。

不要为了一个导入错误全局安装同名包,也不要批量删除 __pycache__。先核对命令、工作目录和完整 traceback,再判断是入口问题、模块遮蔽,还是依赖环境问题。

7. 小结

包内相对导入需要正确的包上下文,文件存在不代表直接执行包内文件就是正确入口。保留业务模块,用 -m 和一个薄 __main__.py 统一启动,再通过固定工作目录的脚本对接自动化。用失败复现和成功断言一起验证,能避免“终端能跑、定时任务找不到包”的反复排障。

标签

1024 12306 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management filecmp filenames files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics multiprocessing mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode urllib usdt utf-8 uv venv vhhbert vite web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它