Python:如何让file2中的动态导入传递回调用它的file1?
动态导入模块并传递到调用脚本的解决方案
问题背景
我编写了一个工具,可自动安装依赖并重启脚本,无需用户排查错误手动安装。理想用法如下:
# file1.py from file2 import import_and_install succ = import_and_install("numpy") # 导入numpy succ |= import_and_install("cv2", "opencv-python") # 导入cv2(对应包名opencv-python) succ |= import_and_install("openpyxl", alias="xl") # 导入openpyxl并命名为xl
但实际调用后发现,file2中函数执行的导入无法传递回file1,只能改用以下繁琐的写法:
# alternate_file1.py try: print("IMPORT LIBS") print("") import numpy import cv2 import openpyxl as xl except: succ = import_and_install("numpy") # 导入numpy succ |= import_and_install("cv2", "opencv-python") # 导入cv2 succ |= import_and_install("openpyxl", alias="xl") # 导入openpyxl if not succ == 4096: exit(1)
直接把file2的代码复制到file1里就能正常工作,但频繁复制代码太麻烦。请问如何让动态导入的结果传递回原文件file1?
原file2.py代码如下:
# file2.py import os import sys import importlib import subprocess import traceback def try_import(import_name, alt_install_name=None, from_pkg=None, alias=None): succ = 4096 # 支持不同的导入策略,导入失败时尝试安装 try: if from_pkg is None: to_import = import_name elif not (from_pkg is None): to_import = from_pkg else: raise AttributeError("Not a valid package") import_entry = importlib.import_module(to_import) if not (from_pkg is None): import_entry = import_entry.__dict__[import_name] print(import_entry) print(to_import) if(not (alias is None)): globals()[alias] = import_entry else: globals()[import_name] = import_entry except AttributeError: print("Not a valid package") succ |= 1 except ImportError: print("can't find the " + to_import + " module.") succ |= 2 if not (alt_install_name is None): to_install = alt_install_name else: to_install = to_import return succ, to_install def try_install(to_install): # 导入失败时询问用户是否安装缺失包 succ = 4096 should_install = input("Would you like to install? (y/n): ") if should_install.lower() in ('y', 'yes'): try: ret = subprocess.check_call([sys.executable, "-m", "pip", 'install', to_install]) except ChildProcessError: print("Failed to install package with return code " + str(ret) + ". Try again manually") succ |= 4 else: print("Install attempted.") restart_script() else: print("You can't run the script until you install the package") succ |= 8 return succ def restart_script(): print("Restarting script. If restart fails try again manually.") print('\a') if not (os.name == 'nt'): os.execl(sys.executable, 'python', traceback.extract_stack()[0].filename, *sys.argv[1:]) else: p = subprocess.call([sys.executable, os.path.realpath(traceback.extract_stack()[0].filename), *sys.argv], shell=True, start_new_session=True) sys.exit(0) def import_and_install(import_name, alt_name=None, from_pkg=None, alias=None): succ, to_install = try_import(import_name, alt_name, from_pkg, alias) if succ == (4096 | 2): succ |= try_install(to_install) return succ
解决方案
问题出在try_import函数中使用的globals()——它指向的是file2.py模块自身的全局命名空间,而非调用者file1.py的。要解决这个问题,需要获取调用者的全局命名空间并将导入的模块绑定到那里。
修改后的file2.py代码
# file2.py import os import sys import importlib import subprocess import traceback import inspect # 新增:用于获取调用者栈帧 def try_import(import_name, alt_install_name=None, from_pkg=None, alias=None): succ = 4096 # 获取调用者的全局命名空间 caller_globals = inspect.currentframe().f_back.f_globals try: if from_pkg is None: to_import = import_name else: to_import = from_pkg import_entry = importlib.import_module(to_import) if from_pkg is not None: import_entry = import_entry.__dict__[import_name] print(import_entry) print(to_import) # 将导入的对象绑定到调用者的全局命名空间 if alias is not None: caller_globals[alias] = import_entry else: caller_globals[import_name] = import_entry except AttributeError: print("无效的包") succ |= 1 except ImportError: print(f"找不到模块 {to_import}") succ |= 2 to_install = alt_install_name if alt_install_name is not None else to_import return succ, to_install def try_install(to_install): succ = 4096 should_install = input("是否安装该包?(y/n): ") if should_install.lower() in ('y', 'yes'): try: ret = subprocess.check_call([sys.executable, "-m", "pip", 'install', to_install]) except ChildProcessError as e: print(f"安装失败,返回码 {e.returncode}。请手动重试") succ |= 4 else: print("安装已尝试,正在重启脚本") restart_script() else: print("必须安装该包才能运行脚本") succ |= 8 return succ def restart_script(): print("正在重启脚本。如果重启失败请手动重试") print('\a') if os.name != 'nt': os.execl(sys.executable, 'python', traceback.extract_stack()[0].filename, *sys.argv[1:]) else: p = subprocess.call([sys.executable, os.path.realpath(traceback.extract_stack()[0].filename), *sys.argv], shell=True, start_new_session=True) sys.exit(0) def import_and_install(import_name, alt_name=None, from_pkg=None, alias=None): succ, to_install = try_import(import_name, alt_name, from_pkg, alias) if succ == (4096 | 2): succ |= try_install(to_install) return succ
修改说明
- 新增
inspect模块导入:用于获取调用者的栈帧信息,从而拿到调用脚本的全局命名空间。 - 替换
globals()为caller_globals:inspect.currentframe().f_back.f_globals指向调用try_import函数的脚本(即file1.py)的全局变量字典,这样导入的模块或别名就会被添加到调用者的全局命名空间中,在file1里直接就能使用。 - 优化部分提示文本:将英文提示改为中文,提升可读性。
验证理想用法
修改后,最初的file1.py代码可以正常工作:
# file1.py from file2 import import_and_install succ = import_and_install("numpy") # 导入numpy succ |= import_and_install("cv2", "opencv-python") # 导入cv2 succ |= import_and_install("openpyxl", alias="xl") # 导入openpyxl并命名为xl # 现在可以直接使用这些模块 print(numpy.__version__) print(cv2.__version__) print(xl.__version__)
内容的提问来源于stack exchange,提问作者Painguy
相关产品推荐
相关产品推荐

