【Python】通过`Editable Install`模式详解,解决Python开发总是import出错的问题

摘要

田辛老师在很久以前,写过一篇关于Python的模块、包之间的内部关系的博客,叫做【Python】__init__.py 文件详解。 虽然我觉得这篇文章已经足够了, 但是还是有很多朋友碰到开发的过程中import包报错的问题。

今天, 田辛老师想介绍一个可编辑安装(Editable Install)模式, 方便快捷的解决这个问题。让开发者的思路集中在业务的实现上。

1. 可编辑安装(Editable Install)模式到底在解决什么问题?

在 Python 开发中,我们经常需要修改本地包的代码并实时测试效果。 而包这个概念可以说在Python开发中无处不在。 几乎每天都要修改, 总不能每个包都重新反复的安装吧。

可编辑安装(Editable Install)模式正是为解决这一痛点而生,它允许开发者在无需重新安装的情况下直接运行本地修改后的代码。

2. 原理

可编辑安装的本质是通过符号链接Symbolic Link)将包的源代码目录与 Python 环境中的包路径关联。当执行 pip install -e . 时:

  1. 创建链接:在系统目录(如 site-packages)生成一个指向本地源码目录的符号链接。
  2. 动态加载:Python 导入包时,实际加载的是源码目录中的文件,修改后立即生效。
  3. 依赖隔离:包的依赖仍通过 requirements.txt 或 pyproject.toml 管理,确保环境一致性。

3. 最简单的配置

3.1. 传统项目(setup.py

bash 复制代码
# 初始化项目结构
mkdir my_package && cd my_package
touch setup.py  # 包含包元数据(名称、版本、依赖等)

# 安装可编辑模式
pip install -e .

3.2. 现代项目(pyproject.toml

bash 复制代码
# pyproject.toml 示例
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "my_package"
version = "0.1.0"
dependencies = ["requests>=2.25"]
bash 复制代码
# 使用 PEP 660 标准安装
pip install -e .

4. 实际项目配置

4.1. 本次项目结构

本次项目,是针对各种AI功能的研究。 希望是通过统一项目结构便于小朋友发挥想象开发有趣的功能。

环境管理使用Conda

bash 复制代码
PROJECT_ROOT
├─bin				# 可执行文件/Shell/Batch
│ ├─ run.bat		# 启动streamlit服务
│ ├─ install.py		# `Editable Install` 安装自动化脚本
│ └─ uninstall.py	# `Editable Install` 卸载自动化脚本
├─config
├─docs				# 项目文档
├─input				# 输入文件
├─logs				# 日志文件
├─output			# 输出文件
├─reports			# 报告文件
├─resource			# 资源文件
│  ├─image
│  └─excel
├─src				# 项目代码,遵循PEP 517/518标准
│  ├─tdouya_ai_assistant
│  ├─aliyun			# 阿里云相关功能包
│  ├─basic			
│  ├─common
│  │  └─log_record  # 日志记录功能
│  ├─game			# 基于AI的游戏
│  ├─interface
│  │  └─pages	   
│  ├─ollama
│  ├─siliconflow
│  ├─tools			# 工具封装的包
│  └─volcengine
└─tests				# 测试代码

在这个项目中, 作为共通部分的common/log_record必然会被其他部分调用

4.2 创建必须的环境文件

4.2.1 pyproject.toml

Python生态不断演进的今天,pyproject.toml已成为现代项目不可或缺的配置中枢。这个遵循TOML语法的文件不仅统一了构建流程,更重塑了依赖管理和项目元数据的配置方式。本文将结合最新实践,为您深度解析这个配置文件的架构与应用。

bash 复制代码
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "Tdouya-AI-Assistant"          # 👈 用户指定项目名称
version = "0.1.1"
authors = [{name = "田辛老师", email = "[email protected]"}]  # 👈 用户信息
description = "基于AI的智能助手开发框架"
readme = "README.md"
requires-python = ">=3.12"

[tool.setuptools]
package-dir = { "" = "src" }  # 声明包根目录在src下

这里面比较重要的设定是最后一句:生命包的根目录在src目录下。 另外,这个文件中项目名必须为英文,中文亲测不行。

4.2.2 setup.py

Python生态中,setup.py曾是项目打包分发的标准配置文件,承载着将代码转化为可安装包的使命。尽管现代工具链已转向pyproject.toml,但理解这个经典文件仍是掌握Python打包历史的必修课。

python 复制代码
from setuptools import setup, find_packages

setup(
    name="Tdouya-AI-Assistant",  # 👈 必须与pyproject.toml一致
    version="0.1.1",
    author="田辛老师",
    author_email="[email protected]",
    description="基于AI的智能助手开发框架",
    packages=find_packages(where="src"),  # 指定包搜索路径
    package_dir={"": "src"},  # 声明包根目录
    install_requires=[
        "python-dotenv>=0.19.0", 
        "requests>=2.21.0",  
        "streamlit>=1.0.0", 
        "moviepy>=1.0.3",  
        "colorlog>=6.7.0",
        "langchain_ollama",
        "langchain_core",
        "langchain_community",
        "ollama",
        "dashscope"
    ],
    python_requires=">=3.12",
)

这样做的目的是,

  • 标准化项目元数据(名称、版本、作者等)
  • 声明Python版本要求
  • 定义包结构和依赖关系
  • 兼容现代构建工具(poetry/flit)和传统setuptools

4.2.3. 创建__init__.py文件

在各个模块的目录下, 创建__init__.py文件。

这个文件如何使用, 在我开始提到的文件中详细介绍过。 在Python开发中非常灵活, 比如,我之前提到封装的log_record包就把所有的代码放到__init__.py中了

4.2.4. 执行可编辑安装

在命令行中执行:

bash 复制代码
pip install -e .

这样做可以实现目标:

  • 将项目注册为开发模式包
  • 建立符号链接到Conda环境的site-packages
  • 实现代码修改即时生效

这样做的结果是在src目录下生成了一个符号链接的文件夹,如下图。

内容完全不用管, 只需要把这个文件放到Git忽略文件即可。

执行完成后, 可以通过下面的方式测试一下:

bash 复制代码
PS E:\BG10-TRN-AIT-AI编程> python -c "import tdouya_ai_assistant; print(f'项目版本: {tdouya_ai_assistant}')"            
项目版本: <module 'tdouya_ai_assistant' from 'E:\\BG10-TRN-AIT-AI编程\\src\\tdouya_ai_assistant\\__init__.py'>

5. 自动化

pip install -e . 这个写法,加上卸载的命令, 实在是太容易忘了, 尤其是对新人来说, 于是田辛老师贴心的给他们写了一个安装脚本,一个卸载脚本。主要的逻辑是, 找到项目根目录, 然后将根目录作为工作目录执行:

  • 安装 pip install -e .
  • 卸载 pip uninstall -y Tdouya-AI-Assistant

两个文件的源代码田老师已经给到大家了, 请参考:

5.1. 安装脚本

python 复制代码
# ==============================================
# 文件名: init.py
# 描述: 此脚本用于查找项目根路径,若项目未初始化则进行初始化操作,已初始化则给出提示。
# 作者: 田辛
# 创建日期: 2025-05-07
# ==============================================
# 导入操作系统相关功能模块,用于文件和目录操作
import os

# 导入子进程模块,用于创建新进程执行外部命令
import subprocess

# 导入系统相关功能模块,用于获取Python解释器信息
import sys

# 导入日期时间模块,用于获取当前时间
import datetime

# 获取当前脚本所在的绝对路径目录
script_dir = os.path.dirname(os.path.abspath(__file__))
# 初始化当前搜索目录为脚本所在目录
current_dir = script_dir

while True:
    # 定义项目已初始化时所需的文件列表
    required_files = [
        "setup.py",
        "pyproject.toml",
        "README.md",
        "CONTRIBUTING.md",
        "installed.lock",
    ]
    # 检查当前目录是否存在所有已初始化所需的文件
    if all(os.path.isfile(os.path.join(current_dir, f)) for f in required_files):
        print(
            f"田豆芽AI助手已安装, 若想再次安装请先卸载。卸载命令:python {script_dir}/uninstall.py"
        )
        break
    # 定义项目未初始化时所需的文件列表
    original_required_files = [
        "setup.py",
        "pyproject.toml",
        "README.md",
        "CONTRIBUTING.md",
    ]
    # 检查当前目录是否存在所有未初始化所需的文件
    if all(
        os.path.isfile(os.path.join(current_dir, f)) for f in original_required_files
    ):
        try:
            # 切换当前工作目录到项目根路径
            os.chdir(current_dir)
            # 使用当前Python解释器执行 `pip install -e .` 命令进行项目安装
            result = subprocess.run(
                [sys.executable, "-m", "pip", "install", "-e", "."],
                capture_output=True,
                text=True,
                check=True,
            )
            print(result.stdout)
            # 生成锁定文件路径
            lock_file_path = os.path.join(current_dir, "installed.lock")
            # 打开锁定文件并写入当前时间戳
            with open(lock_file_path, "w") as f:
                f.write(datetime.datetime.now().strftime("%Y%m%d %H:%M:%S"))
            print(f"田豆芽AI助手安装完成。")
        except subprocess.CalledProcessError as e:
            # 捕获子进程执行错误并输出错误信息
            print(f"执行 `pip install -e .` 命令时出错: {e.stderr}")
        except Exception as e:
            # 捕获其他异常并输出错误信息
            print(f"执行命令时出错: {e}")
        break
    else:
        # 获取当前目录的上一级目录
        new_dir = os.path.dirname(current_dir)
        # 检查是否已经回到根驱动器,如果是则表示未找到项目根路径
        if new_dir == current_dir[:3]:
            print("未找到项目根路径。")
            break
        # 更新当前搜索目录为上一级目录
        current_dir = new_dir

5.2. 卸载脚本

python 复制代码
# ==============================================
# 文件名: uninstall.py
# 描述: 此脚本用于查找项目根路径,执行卸载操作并删除锁定文件。
# 作者: 田辛
# 创建日期: 2025-05-07
# ==============================================
import os
import subprocess
import sys

# 获取当前脚本所在的绝对路径目录
script_dir = os.path.dirname(os.path.abspath(__file__))
# 初始化当前搜索目录为脚本所在目录
current_dir = script_dir

while True:
    # 定义项目已初始化时所需的文件列表
    required_files = [
        "setup.py",
        "pyproject.toml",
        "README.md",
        "CONTRIBUTING.md",
        "installed.lock",
    ]
    # 检查当前目录是否存在所有已初始化所需的文件
    if all(os.path.isfile(os.path.join(current_dir, f)) for f in required_files):
        try:
            # 切换当前工作目录到项目根路径
            os.chdir(current_dir)
            # 执行pip uninstall命令
            result = subprocess.run(
                [sys.executable, "-m", "pip", "uninstall", "-y", "Tdouya-AI-Assistant"],
                capture_output=True,
                text=True,
                check=True,
            )
            print(result.stdout)
            # 删除锁定文件
            lock_file_path = os.path.join(current_dir, "installed.lock")
            if os.path.isfile(lock_file_path):
                os.remove(lock_file_path)

            print("田豆芽AI助手卸载成功。")
        except subprocess.CalledProcessError as e:
            # 捕获子进程执行错误并输出错误信息
            print(f"执行 `pip uninstall Tdouya-AI-Assistant` 命令时出错: {e.stderr}")
        except Exception as e:
            # 捕获其他异常并输出错误信息
            print(f"执行命令时出错: {e}")
        break
    else:
        # 获取当前目录的上一级目录
        new_dir = os.path.dirname(current_dir)
        # 检查是否已经回到根驱动器,如果是则表示未找到项目根路径
        if new_dir == current_dir[:3]:
            print("未找到项目根路径。")
            break
        # 更新当前搜索目录为上一级目录
        current_dir = new_dir

6. 总结与展望:让开发回归创造本身

通过可编辑安装模式,我们成功将Python开发体验提升到新维度。这种模式带来的不仅是「修改即生效」的技术便利,更是开发思维的解放------开发者无需再为包管理分心,可全身心投入业务逻辑的创新。

6.1 核心价值重述

  • 效率革命:告别pip install的等待循环,代码修改秒级生效
  • 环境稳定:符号链接机制保障开发环境与生产环境的高度一致性
  • 架构自由:平级模块间自由引用,支持微服务化开发范式
  • 流程标准化:通过pyproject.toml+setup.py双配置实现现代与传统项目的兼容

6.2 最佳实践建议

  • 版本控制:将installed.lock文件纳入Git管理,避免重复安装
  • 环境隔离:为每个项目创建独立Conda环境,防止依赖污染
  • 自动化升级:定期执行pip list --outdated检查依赖更新
  • 跨平台适配:Windows用户需确认符号链接权限(需开启Developer Mode)

6.3 未来演进方向

随着Python打包工具链的持续演进,可编辑安装模式正呈现三大趋势:

  • 与IDE深度集成:PyCharm/VSCode已原生支持开发模式包提示
  • 容器化适配:在Docker开发环境中实现热重载(需结合volume挂载)
  • AI辅助调试:未来可能通过LLM自动分析循环依赖问题

6.4 致开发者

当您再次面对模块导入错误时,请记住:Python的包管理不应成为创新的枷锁。通过本文介绍的可编辑安装模式与自动化脚本,我们已为您搭建好高效开发的脚手架。现在,是时候将目光投向更远大的目标------让AI助手实现真正智能的交互,让游戏模块突破想象的边界。

相关推荐
Jay_2714 分钟前
python项目如何创建docker环境
开发语言·python·docker
xlsw_21 分钟前
MyBatis之测试添加功能
java·开发语言·mybatis
忘梓.30 分钟前
从二叉树到 STL:揭开 set 容器的本质与用法
开发语言·c++
老胖闲聊33 分钟前
Python Django完整教程与代码示例
数据库·python·django
爬虫程序猿37 分钟前
利用 Python 爬虫获取淘宝商品详情
开发语言·爬虫·python
noravinsc37 分钟前
django paramiko 跳转登录
后端·python·django
声声codeGrandMaster39 分钟前
Django之表格上传
后端·python·django
曹勖之1 小时前
在MATLAB中使用自定义的ROS2消息
开发语言·matlab·机器人·ros·simulink·ros2
元直数字电路验证1 小时前
Python数据分析及可视化中常用的6个库及函数(一)
python·numpy
waterHBO1 小时前
一个小小的 flask app, 几个小工具,拼凑一下
javascript·vscode·python·flask·web app·agent mode·vibe coding