news 2026/10/30 5:11:41

简单谈谈Python中的模块导入

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
简单谈谈Python中的模块导入

前言


import是学 Python 最早接触的语句之一,但它恰恰是被「会用」和「懂」拉开距离的一条。多数人对它的理解停留在「把那个文件引进来」,于是遇到下面这些情况就卡住:明明改了代码为什么没生效、为什么同样的导入在脚本里能用、放进包里就报错、为什么两个文件互相导入就死锁。


本文不谈包的分发和目录组织,只谈一件窄一点的事:import这条语句本身在做什么。包括名字怎么绑定、模块什么时候执行、缓存放在哪、几种导入写法各有什么副作用。示例只依赖标准库,Python 3.8 及以上可运行。


一、import 绑定的是什么名字、执行几次


这是最容易出错、也最该先说清的一点。看四种写法各自绑定了什么:


# 适用于 Python 3.8+
import os.path
from os import path
from os import path as osp
from os.path import join



写法当前命名空间里新增的名字类型



import os.pathos模块对象(顶层包)

from os import pathpath模块对象

from os import path as osposp模块对象

from os.path import joinjoin函数对象



最关键的一条:import a.b绑定的是a,不是a.b。这一点和第 8 个坑点里说的__import__("a.b")返回顶层模块是同一个道理。你之所以能写os.path.join(...),是因为导入系统在os模块对象上挂了属性path,而不是因为名字os.path被绑进了你的命名空间。


由此可以推出一个实用技巧:import numpy.linalg之后,你只能写numpy.linalg.solve(...),写linalg.solve(...)会报NameError(除非另外导入过)。想让短名字可用,就显式写from numpy import linalg。


模块只执行一次,副作用也只在第一次发生


import的第二件事是执行被导入模块的顶层代码。这句话有两个推论。


推论一:同一进程内,同一个模块的顶层代码只执行一次,之后都从缓存里拿。缓存是sys.modules这个普通字典:


# 适用于 Python 3.8+
import sys

before = "decimal" in sys.modules
import decimal
after = "decimal" in sys.modules

print(before, after) # False True
print(sys.modules["decimal"] is decimal) # True

推论二:放在模块顶层的副作用会一直潜伏。比如某个模块顶层写了print("loading..."),只要有人导入它,屏幕上就多一行;而它在整个进程生命周期里最多出现一次。这解释了为什么很多库的import语句会打印版本号或者写日志——它们把初始化动作放在了顶层。


如果你希望某段代码只在直接运行这个文件时执行,用惯用法保护:


# 适用于 Python 3.8+
def main():
print("这里是主流程")

if __name__ == "__main__":
main()

__name__的值取决于模块怎么被使用:作为脚本直接运行是字符串"__main__",被导入则是模块名。这不是约定,是导入系统写入的属性。


二、python -m与函数内导入


同一个文件,python foo.py和python -m foo跑出来可能不一样。差别在于解释器给模块设的元信息:




启动方式__name____package__相对导入是否可用



python pkg/mod.py"__main__"""(空)不可用

python -m pkg.mod"__main__""pkg"可用

被import pkg.mod"pkg.mod""pkg"可用



也就是说,-m会正确设置包上下文,让包内的相对导入成立;直接给文件路径则不会,因为解释器只知道「一个脚本」,不知道它属于哪个包。这是「包里的模块能用from . import x,但单独跑它就报错」的根本原因。


顺带对比一下sys.path的第一项,两者也不同:



  • python pkg/mod.py:第一项是脚本所在目录pkg。

  • python -m pkg.mod:第一项是当前工作目录。


所以同一条import util,两种启动方式找到的可能是两个不同的文件——一个在pkg/里,一个在当前目录里。这类「我本地好好的」问题,十有八九出在这里。


导入写在哪一行,是有区别的


import不一定非要写在文件顶部。写在函数内部完全合法,而且有大用处。


用途一:避开循环导入。两个模块互相依赖时,把其中一方的导入推迟到函数调用时,可以打破死锁。


# 适用于 Python 3.8+
# 文件名 service.py
def run():
# 在函数内导入:此时两个模块都已完成初始化
from . import repository
return repository.fetch_all()

用途二:避免把可选的重量级依赖变成硬性要求。


# 适用于 Python 3.8+
def to_json(data):
"""把数据转成 JSON;没装第三方加速库就走标准库。"""
try:
import orjson as _json # 假设存在,未安装则回退
except ImportError:
import json as _json
return _json.dumps(data)

标准库json一定存在,所以except分支总能兜住;外面那个名字是假设存在的可选依赖,既然拿不准它的 API,就以它自己的官方文档为准。这种「可选加速器」模式很常见,但要注意:导入失败被吞掉的时候,别把真正的错误也一起吞了。用except ImportError而不是裸的except:,原因见坑点第 6 条。


用途三:缩短启动时间。如果一个子命令只在特定场景用得到,把它的导入放进那个分支里,可以让另外九成调用都不付这份代价。这是把「模块顶层不做重活」这条原则贯彻到底的做法。


需要提醒的是:别把这当成随手可用的技巧。导入位置会显著影响可读性,放在顶部的导入一眼可见,藏在函数里的则需要追代码。用的时候最好配一行注释说明原因。


三、动态导入:把模块名当数据用


有时候模块名不是写死的,而是从配置、命令行参数或环境变量里读出来的。这时用importlib.import_module(name):


# 适用于 Python 3.8+
import importlib

def load_backend(name):
"""按名字加载后端模块,名字非法时抛出 ImportError。"""
if not name.isidentifier():
raise ValueError(f"非法的模块名:{name!r}")
return importlib.import_module(f"backends.{name}")

# 调用示意(前提是存在 backends 这个包,且其中有 memory 模块)
# mod = load_backend("memory")
# 此时 mod 就是 backends.memory 模块本身

三个要点:import_module的参数是字符串;返回值是目标模块本身(不是像__import__那样返回顶层包);它走的还是同一套机制,所以sys.modules缓存照样生效。在拿外部输入拼模块名之前,先校验一下名字是否合法,避免把任意输入直接喂给导入系统。


要判断某个模块能否被找到(而不是立刻导入它),用importlib.util.find_spec(name),找不到时返回None:


# 适用于 Python 3.8+
import importlib.util

for name in ["json", "definitely_not_a_module_xyz"]:
spec = importlib.util.find_spec(name)
print(name, "->", None if spec is None else spec.origin)

注意find_spec只回答「找得到吗」,不回答「导入一定成功吗」——模块自己依赖的东西缺失时,导入仍会失败。


四、from m import *为什么不建议用


from m import *把对方模块里所有不以单下划线开头的顶层名字倒进当前命名空间。三个具体危害:



  1. 覆盖。你本地的list、id、open之类的名字可能被静默替换,之后的行为完全变了,而且没有任何提示。

  2. 来源不明。读代码看到process(),无法判断它来自哪个模块。

  3. 受__all__支配。对方模块如果定义了__all__,实际导入的名字会跟着变,你的代码在一个小版本升级后就可能少了个名字。


真要写,只能写在一个包的__init__.py里配合__all__来控制对外接口,其余场景一律显式列出名字。


常见坑点


1. 以为import a.b绑定了a.b


❌ 写import numpy.linalg之后直接调linalg.solve(...),报NameError。 ✅ 记住绑定的是顶层名字numpy,要么写全numpy.linalg.solve(...),要么显式from numpy import linalg。


2. 用重复 import 来「刷新」模块


❌ 改了源码后在同一次解释器会话里再敲一次import mymod,期望看到新代码。 ✅ 模块只在首次导入时执行;改动要重启进程,交互式里可以用importlib.reload,但要清楚已持有的旧引用不会自动更新。


3. 把顶层导入当成「只在该函数里用」


❌ 写from heavy_lib import thing放在文件顶部却只在一个少用到的函数里调用它,每次启动都付出加载代价。 ✅ 把这种导入挪进函数内部,并加注释说明意图。


4. 用python pkg/mod.py跑包内模块


❌ 包内写from . import helpers,直接跑文件时报「没有已知的父包」。 ✅ 从包的父目录用python -m pkg.mod运行,让包上下文正确。


5. 用裸except:包裹导入


❌try: import x后面跟裸except:,连KeyboardInterrupt、SystemExit一起吞掉,程序按 Ctrl+C 都不停。 ✅ 只捕获ImportError,必要时加ModuleNotFoundError,绝不写裸except:。


6. 用__import__做动态导入


❌__import__("a.b")拿到的是顶层模块a,不是a.b。 ✅ 用importlib.import_module("a.b"),它返回目标模块本身。


7. 用外部输入直接拼模块名


❌ 直接把配置里的字符串塞进importlib.import_module,非法名字会抛出难懂的错误,也给了不该有的自由度。 ✅ 先用str.isidentifier()或白名单校验,再导入。


8. 沿用 Python 2 的导入写法


❌ 依赖隐式相对导入(在包里写import helpers找同目录)、把print当语句用。Python 2.7 已于2020 年 1 月 1 日停止维护。 ✅ 包内显式写from . import helpers;输出用print(...)。


总结




关注点结论



绑定什么名字import a.b绑a;from a import b绑b

执行几次每个进程内每个模块只执行一次

缓存在哪sys.modules,一个普通字典

-m与直接运行-m设好__package__,相对导入才可用

导入的位置顶部是默认;函数内导入可用于破循环、省启动时间

动态导入用importlib.import_module,不要用__import__



把import理解成「绑定名字 + 执行一次 + 进缓存」这三件事,很多看似诡异的导入问题就都有了解释。下次遇到「改了不生效」「本地能用别处报错」「互相导入就崩」,不妨先问自己一句:它在绑定谁的名字、这段代码执行过几次、sys.path的第一项到底是哪个目录。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 5:10:41

从155.520 Mb/s到63路2M:SDH帧结构与复用原理

简介:面向华为HCIA-Transmission认证备考者及传输网络初学者,这份培训课程PDF系统梳理光传送网基础与主流传输体制。内容从光传输理论演进切入,串联PDH、SDH、DWDM、OTN、PTN与RTN等应用场景,重点讲解SDH的技术背景、PDH局限&…

作者头像 李华
网站建设 2026/10/30 5:08:57

奇安信天擎V10.0部署实战:环境准备、安装配置与避坑指南

简介:奇安信天擎终端安全管理系统管理员手册面向企业终端安全运维人员与系统管理员,帮助其系统掌握天擎V10.0的部署、配置与日常管理方法,解决终端防护策略落地与安全运维中的实际问题。资源包共1个文件,为PDF格式的官方管理员手册…

作者头像 李华
网站建设 2026/10/30 5:07:57

iOS与安卓双端通讯录源码拆解:从编译到二次开发实战

简介:这份资源是面向移动端开发学习者的双端通讯录类项目源码合集,涵盖 iOS 与安卓两端 APP 及配套 PHP 后台,适合想研究通讯录、相册、短信、定位等模块实现思路的开发者参考。压缩包共约 2000 个文件、298.59MB,以 548 个 php 后…

作者头像 李华
网站建设 2026/10/30 5:07:41

可直接Play的Unity3D坦克大战工程:Rigidbody操控与状态机AI

简介:面向Unity3D入门与进阶学习者的坦克大战完整工程源码,基于经典教程案例整理完善,可直接在编辑器中运行与拆解,适合想通过小型项目掌握游戏开发主流程的开发者。压缩包共2154个文件,约24.78MB,以info、…

作者头像 李华
网站建设 2026/10/30 5:06:28

Python人工智能实验包复现指南:环境配置、搜索算法与训练循环

简介:这份2023年春季复旦大学人工智能A实验压缩包,面向正在学习人工智能、机器学习课程并需要完成实验与项目作业的高校学生,也可供自学者复现课程案例。包内共52个文件,以43个Python脚本为核心,辅以Markdown说明、PDF…

作者头像 李华
网站建设 2026/10/30 5:06:28

Unity WebGL中文输入完整解决方案

简介:本资源是一个面向Unity WebGL开发者的中文输入功能完整实现方案,专为解决Web端3D/2D应用中中文输入不兼容、输入法无法跟随光标、全屏模式下定位失效等典型痛点而设计。项目已通过实际Dome验证,集成JavaScript与Unity C#双向通信机制&am…

作者头像 李华