Python跨文件调用函数的5种实战场景:从同级目录到上级文件夹
Python跨文件调用函数的5种实战场景:从同级目录到上级文件夹
刚接触Python项目时,很多人都会遇到一个看似简单却让人头疼的问题:明明函数就在那里,为什么调用时总是报ModuleNotFoundError?我记得自己第一次尝试在一个项目中组织代码时,把不同功能的函数分到了不同文件里,结果在main.py里怎么都导入不了隔壁文件夹里的工具函数。那种感觉就像你知道钥匙就在隔壁房间,但门被锁上了。
实际上,Python的模块导入系统设计得相当优雅,只是需要理解它的规则。这篇文章就是为你准备的实战指南,我会用真实的项目结构作为例子,带你一步步解决从简单到复杂的跨文件调用问题。无论你是刚开始学习Python的新手,还是已经有一定经验但在这个问题上踩过坑的开发者,都能在这里找到清晰的解决方案。
1. 理解Python的模块与包系统
在深入具体调用方法之前,我们需要先搞清楚Python是如何组织代码的。这就像你要在一个大型图书馆里找书,如果不知道图书分类系统,就会迷失在书架之间。
1.1 模块、包与普通文件夹的区别
很多人刚开始会混淆这三个概念,其实它们的区别很明确:
- 模块(Module):一个
.py文件就是一个模块。比如utils.py就是一个名为utils的模块。 - 包(Package):一个包含
__init__.py文件的文件夹。这个文件可以是空的,也可以包含初始化代码。 - 普通文件夹:没有
__init__.py文件的文件夹,Python不会把它当作包来处理。
注意:从Python 3.3开始,
__init__.py对于命名空间包(Namespace Package)不再是必需的,但对于常规包,保持这个文件仍然是好习惯。
让我用一个实际的例子来说明。假设我们有一个电商项目的目录结构:
ecommerce_project/
├── __init__.py
├── main.py
├── utils/
│ ├── __init__.py
│ ├── validators.py
│ └── formatters.py
├── models/
│ ├── __init__.py
│ ├── product.py
│ └── user.py
└── services/
├── __init__.py
├── payment.py
└── shipping.py
在这个结构中:
ecommerce_project是一个包(因为有__init__.py)utils、models、services都是子包validators.py、product.py、payment.py等都是模块
1.2 Python的导入搜索路径
当你在代码中写import something时,Python会按照特定顺序搜索这个模块。这个搜索路径存储在sys.path列表中。你可以通过以下代码查看当前的搜索路径:
import sys
print("当前Python搜索路径:")
for path in sys.path:
print(f" - {path}")
典型的输出可能像这样:
当前Python搜索路径:
- /home/user/ecommerce_project
- /usr/lib/python3.9
- /usr/lib/python3.9/lib-dynload
- /home/user/.local/lib/python3.9/site-packages
- /usr/local/lib/python3.9/dist-packages
- /usr/lib/python3/dist-packages
Python会按顺序在这些路径中查找你要导入的模块。第一个找到的就会被使用。这就是为什么有时候即使文件存在,也会报ModuleNotFoundError——文件所在的目录不在搜索路径中。
1.3 相对导入与绝对导入
Python支持两种导入方式,理解它们的区别很重要:
绝对导入:从项目根目录或已安装的包开始指定完整路径
from utils.validators import validate_email
from models.product import Product
相对导入:使用点号表示相对位置(只能在包内部使用)
from .validators import validate_email # 同一包内的模块
from ..models.product import Product # 上级包中的模块
相对导入在组织大型项目时很有用,但它有一个限制:只能在作为包一部分的模块中使用。如果你直接运行一个使用相对导入的脚本(如python my_module.py),会收到错误提示。
2. 同级目录下的函数调用
这是最简单也是最常见的情况。当两个文件在同一个目录下时,Python的导入机制几乎总是能正常工作。
2.1 基础导入方式
假设我们有一个简单的项目结构:
project/
├── calculator.py
└── main.py
calculator.py的内容:
def add(a, b):
"""返回两个数的和"""
return a + b
def multiply(a, b):
"""返回两个数的乘积"""
return a * b
def power(base, exponent):
"""返回base的exponent次幂"""
return base ** exponent
在main.py中调用这些函数,有几种不同的方式:
方式一:导入整个模块
import calculator
result = calculator.add(5, 3)
print(f"5 + 3 = {result}") # 输出: 5 + 3 = 8
product = calculator.multiply(4, 6)
print(f"4 × 6 = {product}") # 输出: 4 × 6 = 24
方式二:导入特定函数
from calculator import add, multiply
sum_result = add(10, 20)
print(f"10 + 20 = {sum_result}") # 输出: 10 + 20 = 30
方式三:使用别名
from calculator import power as pow_func
result = pow_func(2, 8)
print(f"2的8次方是: {result}") # 输出: 2的8次方是: 256
2.2 实际项目中的最佳实践
在真实项目中,我通常遵循这些原则:
- 按功能组织导入:将相关的导入分组,标准库导入在前,第三方库其次,最后是自己的模块
- 避免使用通配符导入:
from module import *会让代码难以理解和调试 - 使用有意义的别名:特别是当模块名很长或与现有名称冲突时
下面是一个实际项目中的导入示例:
# 标准库导入
import os
import sys
from datetime import datetime
from typing import List, Dict, Optional
# 第三方库导入
import pandas as pd
import numpy as np
from sqlalchemy import create_engine
# 本地模块导入
from data_processor import clean_data, normalize_columns
from report_generator import generate_summary_report
from config import DATABASE_URL, LOG_LEVEL
2.3 处理循环导入问题
有时候,两个模块需要相互导入对方的函数,这就形成了循环导入。Python可以处理简单的循环导入,但复杂的循环导入会导致问题。
假设有两个文件:
user.py:
from post import get_user_posts
class User:
def __init__(self, name):
self.name = name
def get_recent_posts(self):
# 这里需要导入Post相关功能
return get_user_posts(self.name)
post.py:
from user import User
class Post:
def __init__(self, content, author):
self.content = content
self.author = author
def get_user_posts(username):
# 这里需要User类
user = User(username)
# ... 获取帖子的逻辑
这种情况会导致导入错误。解决方法有几种:
- 重构代码:将公共功能提取到第三个模块中
- 局部导入:在函数内部导入需要的模块
- 使用类型提示的字符串字面量(Python 3.7+)
对于上面的例子,我们可以这样修改post.py:
# 在文件顶部移除 from user import User
class Post:
def __init__(self, content, author):
self.content = content
self.author = author
def get_user_posts(username):
# 局部导入,避免循环导入
from user import User
user = User(username)
# ... 获取帖子的逻辑
或者使用类型提示的字符串字面量:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from user import User
class Post:
def __init__(self, content, author: 'User'):
self.content = content
self.author = author
3. 调用子目录中的函数
当项目规模增长时,我们自然会把相关功能组织到子目录中。这是Python包系统真正发挥作用的地方。
3.1 标准包结构导入
让我们回到之前的电商项目例子。假设我们想在main.py中调用utils/validators.py中的函数:
utils/validators.py:
def validate_email(email: str) -> bool:
"""验证电子邮件地址格式"""
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
def validate_phone(phone: str) -> bool:
"""验证手机号码格式(简单版本)"""
import re
# 移除所有非数字字符
digits = re.sub(r'\D', '', phone)
return len(digits) >= 10
def validate_password(password: str) -> dict:
"""验证密码强度,返回包含检查结果的字典"""
result = {
'length_ok': len(password) >= 8,
'has_upper': any(c.isupper() for c in password),
'has_lower': any(c.islower() for c in password),
'has_digit': any(c.isdigit() for c in password),
'has_special': any(not c.isalnum() for c in password)
}
result['all_checks_passed'] = all(result.values())
return result
现在从main.py中调用这些函数:
方式一:直接导入函数
from utils.validators import validate_email, validate_password
# 测试电子邮件验证
test_emails = [
"user@example.com",
"invalid-email",
"name@company.co.uk",
"missing@tld"
]
print("电子邮件验证测试:")
for email in test_emails:
is_valid = validate_email(email)
print(f" {email:30} -> {'有效' if is_valid else '无效'}")
方式二:通过包导入
import utils.validators as validators
# 测试密码强度验证
passwords = ["weak", "Strong123", "Very$tr0ngP@ss"]
print("\n密码强度验证测试:")
for pwd in passwords:
result = validators.validate_password(pwd)
print(f"\n密码: {pwd}")
for check, passed in result.items():
status = "✓" if passed else "✗"
print(f" {check:20} {status}")
3.2 使用__init__.py简化导入
__init__.py文件不仅仅是一个标记文件,它还可以用来简化包的导入接口。我们可以通过在这个文件中导入子模块的内容,让用户更容易访问它们。
utils/__init__.py:
"""
工具函数包
"""
from .validators import (
validate_email,
validate_phone,
validate_password
)
from .formatters import (
format_currency,
format_date,
truncate_text
)
# 可以定义包级别的变量
PACKAGE_VERSION = "1.0.0"
# 或者定义包级别的函数
def get_version():
"""返回工具包的版本信息"""
return f"utils package version {PACKAGE_VERSION}"
现在,我们可以用更简洁的方式导入:
from utils import validate_email, format_currency
# 或者导入整个包
import utils
print(f"工具包版本: {utils.get_version()}")
print(f"100美元格式化为: {utils.format_currency(100, 'USD')}")
3.3 处理多层嵌套的子目录
对于更深层的目录结构,导入方式类似。假设我们有这样的结构:
project/
├── main.py
└── core/
├── __init__.py
├── database/
│ ├── __init__.py
│ ├── connection.py
│ └── queries.py
└── analytics/
├── __init__.py
├── metrics.py
└── visualizations.py
从main.py导入database/queries.py中的函数:
from core.database.queries import get_user_by_id, get_recent_orders
# 或者使用相对导入(在包内部)
# 在core/analytics/metrics.py中导入database/queries.py:
# from ..database.queries import get_user_by_id
3.4 实际案例:Web应用中的模块组织
让我们看一个Flask Web应用的典型结构:
flask_app/
├── app.py
├── config.py
├── requirements.txt
├── static/
├── templates/
└── app/
├── __init__.py
├── models/
│ ├── __init__.py
│ ├── user.py
│ └── post.py
├── views/
│ ├── __init__.py
│ ├── auth.py
│ └── blog.py
├── forms/
│ ├── __init__.py
│ ├── login_form.py
│ └── post_form.py
└── utils/
├── __init__.py
├── validators.py
└── helpers.py
在app/__init__.py中,我们通常会这样组织:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
db = SQLAlchemy()
login_manager = LoginManager()
def create_app(config_class='config.Config'):
app = Flask(__name__)
app.config.from_object(config_class)
# 初始化扩展
db.init_app(app)
login_manager.init_app(app)
# 注册蓝图
from app.views.auth import auth_bp
from app.views.blog import blog_bp
app.register_blueprint(auth_bp)
app.register_blueprint(blog_bp)
return app
然后在app/views/auth.py中,我们可以这样导入其他模块:
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required
from app import db, login_manager
from app.models.user import User
from app.forms.login_form import LoginForm
from app.utils.validators import validate_email
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# 使用导入的验证函数
if not validate_email(form.email.data):
flash('无效的电子邮件地址', 'error')
return render_template('auth/login.html', form=form)
user = User.query.filter_by(email=form.email.data).first()
if user and user.check_password(form.password.data):
login_user(user)
return redirect(url_for('blog.index'))
return render_template('auth/login.html', form=form)
这种结构清晰地将不同功能的代码分开,同时通过合理的导入机制让它们能够协同工作。
4. 调用上级目录中的函数
这是Python初学者经常遇到问题的地方。当你需要从子目录中的文件调用上级目录中的函数时,标准的导入方式不再有效,因为上级目录不在Python的搜索路径中。
4.1 理解问题所在
假设我们有这样的目录结构:
project/
├── common_utils.py
├── config.py
└── modules/
├── __init__.py
└── processor.py
在modules/processor.py中,我们想要使用common_utils.py和config.py中的函数。如果直接尝试导入:
# 在modules/processor.py中
import common_utils # 这会失败!
你会得到ModuleNotFoundError: No module named 'common_utils'。这是因为Python只在sys.path列出的目录中搜索模块,而project目录的父目录(包含common_utils.py的目录)不在这个列表中。
4.2 解决方案:动态修改sys.path
最直接的解决方案是在运行时将上级目录添加到Python的搜索路径中。有几种方法可以实现:
方法一:使用绝对路径(不推荐)
import sys
sys.path.append('/home/user/project') # 绝对路径
import common_utils
这种方法的问题很明显:路径是硬编码的,代码无法在不同机器或不同目录位置正常工作。
方法二:使用相对路径和os模块(推荐)
import os
import sys
# 获取当前文件的目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 获取上级目录
parent_dir = os.path.dirname(current_dir)
# 将上级目录添加到搜索路径
sys.path.insert(0, parent_dir)
# 现在可以导入了
import common_utils
import config
让我们分解一下这段代码:
__file__是当前文件的路径os.path.abspath(__file__)获取绝对路径os.path.dirname()获取父目录sys.path.insert(0, ...)将路径插入到搜索路径的开头(这样Python会优先搜索这个目录)
方法三:创建工具函数 在实际项目中,我通常会创建一个专门处理路径的工具函数:
# 在modules/path_utils.py中
import os
import sys
from pathlib import Path
def add_parent_to_path(levels_up=1):
"""
将指定层级的上级目录添加到Python路径中
参数:
levels_up: 向上回溯的层级数,默认为1(父目录)
"""
current_file = Path(__file__).resolve()
target_dir = current_file.parents[levels_up]
sys.path.insert(0, str(target_dir))
return target_dir
然后在需要的地方使用:
from .path_utils import add_parent_to_path
# 添加父目录到路径
add_parent_to_path(1)
# 现在可以导入上级目录中的模块了
import common_utils
import config
# 如果需要上两级目录
add_parent_to_path(2)
4.3 实际项目示例
让我们看一个更复杂的例子。假设我们有一个数据科学项目的结构:
data_science_project/
├── config.py
├── data_loader.py
├── utils/
│ ├── __init__.py
│ ├── preprocessing.py
│ └── visualization.py
├── notebooks/
│ ├── exploration.ipynb
│ └── modeling.ipynb
└── scripts/
├── __init__.py
├── train_model.py
└── evaluate.py
在scripts/train_model.py中,我们需要使用项目根目录下的config.py和data_loader.py,以及utils/目录中的函数:
# scripts/train_model.py
import os
import sys
from pathlib import Path
# 将项目根目录添加到Python路径
project_root = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(project_root))
# 现在可以导入项目根目录和utils中的模块
import config
import data_loader
from utils.preprocessing import clean_data, normalize_features
from utils.visualization import plot_training_history
def train_model():
"""训练机器学习模型"""
# 加载配置
model_config = config.get_model_config()
# 加载数据
raw_data = data_loader.load_dataset(config.DATASET_PATH)
# 预处理数据
cleaned_data = clean_data(raw_data)
X, y = normalize_features(cleaned_data)
# 训练模型(简化示例)
print(f"使用配置训练模型: {model_config}")
print(f"数据形状: X={X.shape}, y={y.shape}")
# 这里实际会调用机器学习库如scikit-learn
# model = SomeModel(**model_config)
# history = model.fit(X, y)
# 可视化训练历史
# plot_training_history(history)
return "模型训练完成"
if __name__ == "__main__":
result = train_model()
print(result)
4.4 处理多层上级目录
有时候,你可能需要访问不止一级的上级目录。使用pathlib模块可以让这变得简单:
from pathlib import Path
# 获取当前文件的绝对路径
current_file = Path(__file__).resolve()
# 获取不同层级的上级目录
parent_dir = current_file.parent # 直接父目录
grandparent_dir = current_file.parents[1] # 上两级目录
great_grandparent_dir = current_file.parents[2] # 上三级目录
# 使用parents属性可以方便地获取任意层级的上级目录
project_root = current_file.parents[2] # 假设项目根目录在上两级
# 添加到sys.path
import sys
sys.path.insert(0, str(project_root))
4.5 在Jupyter Notebook中处理导入
在Jupyter Notebook中,情况稍有不同,因为notebook的运行目录可能不是文件所在目录。这里有一个可靠的方法:
# 在notebook的单元格中
import os
import sys
from pathlib import Path
# 方法1:使用绝对路径(如果知道项目结构)
project_path = Path.cwd().parent # 假设notebook在项目子目录中
sys.path.insert(0, str(project_path))
# 方法2:使用相对路径(更灵活)
notebook_path = Path().resolve()
# 假设项目根目录在notebook的上两级
project_root = notebook_path.parents[1]
sys.path.insert(0, str(project_root))
# 现在可以导入项目模块
import config
from utils.preprocessing import clean_data
我经常在notebook的开头添加这样一个单元格,确保所有必要的路径都已正确设置。
5. 执行另一个Python文件
有时候,我们不仅需要调用另一个文件中的函数,还需要直接执行整个Python文件。这在脚本编排、任务调度和构建工具链时特别有用。
5.1 使用os.system执行外部命令
最简单的方法是使用os.system(),它会在系统shell中执行命令:
import os
# 执行另一个Python脚本
os.system('python other_script.py')
# 传递参数
os.system('python process_data.py --input data.csv --output results.json')
# 在特定目录中执行
os.system('cd /path/to/project && python main.py')
然而,这种方法有几个缺点:
- 依赖于系统shell,在不同操作系统上行为可能不同
- 难以捕获输出或错误信息
- 安全性问题(如果命令来自不可信来源)
5.2 使用subprocess模块(推荐)
subprocess模块提供了更强大、更安全的执行外部命令的方式:
基本用法:
import subprocess
# 执行脚本并捕获输出
result = subprocess.run(
['python', 'other_script.py'],
capture_output=True,
text=True
)
print(f"返回码: {result.returncode}")
print(f"标准输出:\n{result.stdout}")
if result.stderr:
print(f"标准错误:\n{result.stderr}")
传递参数:
import subprocess
# 执行带参数的脚本
args = [
'python',
'data_pipeline.py',
'--input', 'raw_data.csv',
'--output', 'processed_data.parquet',
'--verbose'
]
result = subprocess.run(args, capture_output=True, text=True)
if result.returncode == 0:
print("脚本执行成功")
print(f"输出: {result.stdout}")
else:
print(f"脚本执行失败,错误: {result.stderr}")
设置工作目录:
import subprocess
from pathlib import Path
# 指定工作目录
project_root = Path(__file__).resolve().parent
result = subprocess.run(
['python', 'scripts/process.py'],
cwd=project_root, # 设置工作目录
capture_output=True,
text=True
)
5.3 动态导入并执行模块
如果你需要执行另一个Python文件并访问其中的变量或函数,而不仅仅是运行它,可以使用动态导入:
方法一:使用importlib
import importlib.util
import sys
def import_module_from_path(module_name, file_path):
"""
从指定路径导入模块
参数:
module_name: 模块名称
file_path: 模块文件路径
"""
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
# 使用示例
other_module = import_module_from_path('my_module', '/path/to/my_module.py')
# 现在可以访问模块中的内容
if hasattr(other_module, 'some_function'):
result = other_module.some_function()
print(f"函数执行结果: {result}")
if hasattr(other_module, 'SOME_CONSTANT'):
print(f"常量值: {other_module.SOME_CONSTANT}")
方法二:直接执行文件并获取全局变量
def execute_file_and_get_globals(file_path):
"""
执行Python文件并返回其全局命名空间
参数:
file_path: 要执行的Python文件路径
返回:
文件的全局命名空间字典
"""
globals_dict = {}
with open(file_path, 'r', encoding='utf-8') as f:
code = f.read()
# 执行代码
exec(code, globals_dict)
return globals_dict
# 使用示例
config_globals = execute_file_and_get_globals('config.py')
# 访问配置变量
db_host = config_globals.get('DATABASE_HOST', 'localhost')
db_port = config_globals.get('DATABASE_PORT', 5432)
print(f"数据库配置: {db_host}:{db_port}")
# 如果文件中有函数,也可以调用
if 'initialize_database' in config_globals:
config_globals['initialize_database']()
5.4 实际应用场景
场景一:配置管理 在大型项目中,经常需要根据环境加载不同的配置文件:
import importlib.util
from pathlib import Path
def load_config(environment='development'):
"""
根据环境加载配置文件
参数:
environment: 环境名称,如'development'、'testing'、'production'
"""
config_dir = Path(__file__).parent / 'config'
config_file = config_dir / f'{environment}.py'
if not config_file.exists():
raise FileNotFoundError(f"配置文件不存在: {config_file}")
# 动态导入配置文件
spec = importlib.util.spec_from_file_location(f'config_{environment}', config_file)
config_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(config_module)
# 收集所有大写的配置变量
config = {}
for key in dir(config_module):
if key.isupper() and not key.startswith('_'):
config[key] = getattr(config_module, key)
return config
# 使用示例
dev_config = load_config('development')
prod_config = load_config('production')
print(f"开发环境数据库: {dev_config.get('DATABASE_URL')}")
print(f"生产环境数据库: {prod_config.get('DATABASE_URL')}")
场景二:插件系统 实现一个简单的插件系统,动态加载和执行插件:
# plugin_loader.py
import importlib.util
from pathlib import Path
from typing import List, Dict, Any
class PluginLoader:
def __init__(self, plugins_dir: str):
self.plugins_dir = Path(plugins_dir)
self.plugins = {}
def discover_plugins(self):
"""发现所有插件"""
plugin_files = self.plugins_dir.glob('*.py')
for plugin_file in plugin_files:
if plugin_file.name == '__init__.py':
continue
plugin_name = plugin_file.stem
self.load_plugin(plugin_name, plugin_file)
def load_plugin(self, plugin_name: str, plugin_file: Path):
"""加载单个插件"""
spec = importlib.util.spec_from_file_location(
f'plugins.{plugin_name}',
plugin_file
)
plugin_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(plugin_module)
# 检查插件是否有必要的接口
if hasattr(plugin_module, 'Plugin'):
plugin_class = plugin_module.Plugin
self.plugins[plugin_name] = plugin_class()
print(f"已加载插件: {plugin_name}")
else:
print(f"警告: {plugin_name} 没有Plugin类,跳过")
def execute_plugin(self, plugin_name: str, *args, **kwargs) -> Any:
"""执行插件"""
if plugin_name not in self.plugins:
raise ValueError(f"插件未找到: {plugin_name}")
plugin = self.plugins[plugin_name]
return plugin.execute(*args, **kwargs)
def list_plugins(self) -> List[str]:
"""列出所有已加载的插件"""
return list(self.plugins.keys())
# 使用示例
if __name__ == "__main__":
loader = PluginLoader('plugins')
loader.discover_plugins()
print(f"发现的插件: {loader.list_plugins()}")
# 执行插件
for plugin_name in loader.list_plugins():
try:
result = loader.execute_plugin(plugin_name)
print(f"插件 {plugin_name} 执行结果: {result}")
except Exception as e:
print(f"插件 {plugin_name} 执行失败: {e}")
场景三:测试脚本编排 在自动化测试中,经常需要按特定顺序执行多个测试脚本:
# test_runner.py
import subprocess
import time
from datetime import datetime
from pathlib import Path
from typing import List, Dict
class TestRunner:
def __init__(self, test_dir: str):
self.test_dir = Path(test_dir)
self.results = []
def discover_tests(self) -> List[Path]:
"""发现所有测试脚本"""
test_files = []
# 查找所有test_*.py文件
for pattern in ['test_*.py', '*_test.py']:
test_files.extend(self.test_dir.rglob(pattern))
# 按文件名排序,确保执行顺序一致
test_files.sort()
return test_files
def run_test(self, test_file: Path) -> Dict:
"""运行单个测试脚本"""
start_time = time.time()
result = subprocess.run(
['python', str(test_file)],
capture_output=True,
text=True
)
end_time = time.time()
duration = end_time - start_time
test_result = {
'test_file': test_file.name,
'path': str(test_file),
'returncode': result.returncode,
'stdout': result.stdout,
'stderr': result.stderr,
'duration': duration,
'timestamp': datetime.now().isoformat(),
'success': result.returncode == 0
}
return test_result
def run_all_tests(self):
"""运行所有测试"""
test_files = self.discover_tests()
print(f"发现 {len(test_files)} 个测试文件")
print("=" * 50)
for i, test_file in enumerate(test_files, 1):
print(f"运行测试 [{i}/{len(test_files)}]: {test_file.name}")
result = self.run_test(test_file)
self.results.append(result)
status = "✓ 通过" if result['success'] else "✗ 失败"
print(f" 状态: {status}, 耗时: {result['duration']:.2f}秒")
if result['stderr']:
print(f" 错误输出: {result['stderr'][:200]}...")
self.generate_report()
def generate_report(self):
"""生成测试报告"""
total = len(self.results)
passed = sum(1 for r in self.results if r['success'])
failed = total - passed
print("\n" + "=" * 50)
print("测试报告")
print("=" * 50)
print(f"总计: {total}, 通过: {passed}, 失败: {failed}")
if failed > 0:
print("\n失败的测试:")
for result in self.results:
if not result['success']:
print(f" - {result['test_file']}")
if result['stderr']:
print(f" 错误: {result['stderr'][:100]}")
# 使用示例
if __name__ == "__main__":
runner = TestRunner('tests')
runner.run_all_tests()
这些方法覆盖了从简单到复杂的各种场景,你可以根据具体需求选择合适的方法。在实际项目中,我通常优先使用subprocess.run()来执行独立脚本,而对于需要交互或共享数据的场景,则使用动态导入。关键是要理解每种方法的优缺点,并根据具体情况做出选择。
更多推荐
所有评论(0)