Python 循环导入与模块导入的 7 个坑📋本期菜单:7 个模块与导入的坑,从「循环导入」到「name守护」[入门] 循环导入 · import * 污染 · 模块级副作用 ·name守护[进阶]init.py 过度初始化 · 相对导入失败[深入] reload 陷阱毛毛姐的项目从单文件长成了多模块。她把代码拆成了fans.py、stats.py、main.py,觉得结构清晰多了。然后python main.py一跑——ImportError: cannot import name 'get_fans' from partially initialized module 'fans'。「我就 import 了一下,它说我没初始化完?Python 的 import 到底怎么工作的?」坑点 1:循环导入 [入门]# ❌ 踩坑代码# fans.pyfromstatsimportcalculate_stats# ← 导入 statsdefget_fans():return["Alice","Bob","Charlie"]defprocess_fans():fans=get_fans()returncalculate_stats(fans)# 用 stats 的函数# stats.pyfromfansimportget_fans# ← 又导入 fans!defcalculate_stats(fans):return{"total":len(fans)}defsummary():fans=get_fans()# 用 fans 的函数returncalculate_stats(fans)# main.pyfromfansimportprocess_fansprint(process_fans())# ImportError: cannot import name 'get_fans' from partially# initialized module 'fans' (most likely due to a circular import)# ✅ 正确写法# 方法一:延迟导入(最简单的修复)# fans.pydefget_fans():return["Alice","Bob","Charlie"]defprocess_fans():fromstatsimportcalculate_stats# ← 延迟到函数内导入fans=get_fans()returncalculate_stats(fans)# stats.pydefcalculate_stats(fans):return{"total":len(fans)}defsummary():fromfansimportget_fans# ← 延迟到函数内导入fans=get_fans()returncalculate_stats(fans)# main.pyfromfansimportprocess_fansprint(process_fans())# {'total': 3} ✅# 方法二:重构依赖关系(更好的设计)# 把共享的数据/函数提取到单独模块# models.pydefget_fans():return["Alice","Bob","Charlie"]# stats.py(只依赖 models,不依赖 fans)frommodelsimportget_fansdefcalculate_stats(fans):return{"total":len(fans)}# fans.py(只依赖 models 和 stats)frommodelsimportget_fansfromstatsimportcalculate_statsdefprocess_fans():returncalculate_stats(get_fans())毛毛姐:fans 导入 stats,stats 又导入 fans,这不是合理的互相依赖吗?我:Python 的 import 是执行时语义——导入 fans 时,Python 开始执行 fans.py,遇到from stats import ...就转去执行 stats.py,stats.py 遇到from fans import get_fans又想回去执行 fans.py——但 fans.py 还没执行完!Python 拿到的是半成品,找不到get_fans。循环导入的根本原因是模块间的环形依赖。Python 的 import 机制是:先创建模块对象并注册到sys.modules,再执行模块顶层代码,最后返回模块对象。如果 A 的顶层代码还没执行完就 import B,而 B 又 import A,B 这时从sys.modules里拿到的 A 就是还没执行完的半成品——get_fans还没定义。解决方案的核心是打破环形依赖:要么延迟导入(把 import 移到函数内部),要么重构架构(提取共享层到第三方模块)。💡一句话带走:A 导 B、B 导 A 就死循环,延迟导入可破坑点 2:init.py 中过度初始化 [进阶]毛毛姐修好了循环导入,但发现import mypackage时程序卡了好几秒。# ❌ 踩坑代码# mypackage/__init__.py# 毛毛姐把所有初始化都塞进了 __init__.pyimportpygame# ← 重型依赖importnumpy# ← 重型依赖frommypackage.configimportload_configfrommypackage.databaseimportconnect_db# 加载配置文件config=load_config()# ← 读文件# 连接数据库db=connect_db(config)# ← 网络连接!# 初始化 pygamepygame.init()# ← 硬件初始化!# 用户只是想 import 一个工具函数# from mypackage.utils import format_name ← 这一行要等上面全部执行完# ✅ 正确写法# mypackage/__init__.py(保持轻量)# 只暴露公共 API,不做重逻辑frommypackage.utilsimportformat_name,format_datefrommypackage.statsimportcalculate_stats __all__=["format_name","format_date","calculate_stats"]__version__="1.0.0"# 重型初始化放到专用模块# mypackage/initializer.pydefinitialize():"""显式调用,不自动执行"""importpygameimportnumpyfrommypackage.configimportload_configfrommypackage.databaseimportconnect_db config=load_config()db=connect_db(config)pygame.init()return{"config":config,"db":db}# 用户按需调用# from mypackage import format_name ← 快!不触发重型初始化# from mypackage.initializer import initialize# resources = initialize() ← 需要时才初始化毛毛姐:init.py 不是用来初始化的吗?我:__init__.py的作用是标记一个目录是 Python 包,它可以用来暴露公共 API。但它不是「初始化文件」——里面的代码在import 包名时立即执行。如果你在里面放数据库连接、pygame 初始化这种重逻辑,用户只是想用一个工具函数,也得等全部跑完。保持init.py 轻量,重逻辑放函数里让用户按需调用。__init__.py在包被 import 时执行,它的作用是:标记目录为 Python 包暴露包的公共 API(from mypackage import useful_function)设置__all__、__version__等元信息它不应该做的事情:连接数据库、网络请求等 I/O 操作初始化重型第三方库执行耗时的计算修改全局状态💡一句话带走:init.py 只做轻量事,重活放函数里坑点 3:import * 污染命名空间 [入门]毛毛姐发现from module import *很方便——不用写模块名前缀。直到有一天,她的sum函数不工作了。# ❌ 踩坑代码# utils.pysum=0# 模块级变量,名字叫 sumcount=0name="utils"defhelper():pass# main.pyfromutilsimport*# ← 导入所有公开名字!fromstatsimport*# ← 又导入一波!# 问题1:覆盖内置函数print(sum([1,2,3]))# TypeError: 'int' object is not callable# sum 被 utils.sum = 0 覆盖了!# 问题2:不知道名字从哪来result=helper()# ← helper 从哪来的?utils 还是 stats?count=100# ← 这个 count 是我定义的还是 import * 来的?# 问题3:难以追踪# 当代码变大,5 个 import * 之后,你根本不知道# 每个名字来自哪个模块# ✅ 正确写法# 显式导入:明确知道每个名字从哪来fromutilsimporthelper,countfromstatsimportcalculate_stats# 或者用模块名前缀(最清晰)importutilsimportstats result=utils.helper()total=stats.calculate_stats([1,2,3])print(sum([1,2,3]))# 6 ← 内置 sum 没被覆盖# 如果模块定义了 __all__,import * 只导入 __all__ 中的名字# utils.py__all__=["helper","count"]# ← 只导出这两个sum=0# ← 不会被 import * 导出(但仍不建议用内置名)# from utils import * # 只导入 helper 和 count毛毛姐:import * 不是很方便吗?省得写模块名。我:方便是方便,但你不知道导入了哪些名字,也不知道它们从哪来。最危险的是覆盖内置函数——sum、type、id、list这些内置名一旦被覆盖,排查起来非常痛苦。显式导入每个名字,或者用模块名前缀,代码可读性和可维护性都好得多。from module import *的行为:如果模块定义了__all__,导入__all__中列出的所有名字如果没有__all__,导入所有不以_开头的模块级名字这会把大量名字注入当前命名空间,可能导致:覆盖内置函数(sum、type、id)覆盖同名变量(多个模块都有count、name)难以追踪名字来源(IDE 也帮不了你)💡一句话带走:import * 是命名空间炸弹,别用坑点 4:相对导入与绝对导入 [进阶]第二天,毛毛姐直接运行一个子模块,报了个看不懂的错。# ❌ 踩坑代码# 项目结构:# myproject/# __init__.py# main.py# utils/# __init__.py# helpers.py# constants.py# utils/helpers.pyfrom.constantsimportMAX_FANS# ← 相对导入defget_max():returnMAX_FANS# 直接运行 helpers.py# $ python utils/helpers.py# ImportError: attempted relative import with no known parent package# 原因:直接运行脚本时,__package__ 为 None# 相对导入(from . import / from .. import)需要包上下文# ✅ 正确写法# 方法一:用 python -m 运行(推荐)# $ python -m myproject.utils.helpers# 这样 Python 知道包上下文,相对导入能工作# 方法二:用绝对导入# utils/helpers.pyfrommyproject.utils.constantsimportMAX_FANS# ← 绝对路径defget_max():returnMAX_FANS# 方法三:脚本入口用绝对导入,包内用相对导入# utils/helpers.py(包内模块,用相对导入)from.constantsimportMAX_FANSdefget_max():returnMAX_FANS# main.py(脚本入口,用绝对导入)frommyproject.utils.helpersimportget_maxif__name__=="__main__":print(get_max())# 运行方式:在项目父目录执行 python -m myproject.main# 方法四:在脚本中手动修复包上下文(不推荐,仅调试用)importsysimportimportlib.utilif__package__isNoneor__package__=='':# 手动设置包路径sys.path.insert(0,'/path/to')# ← 项目父目录(能按名字找到 myproject)__package__='myproject.utils'毛毛姐:相对导入不是更简洁吗?为什么要报错?我:相对导入依赖包上下文——Python 需要知道当前模块属于哪个包。直接运行脚本时,Python 把脚本当成顶层模块,__package__为 None,相对导入就找不到参照系。用python -m myproject.utils.helpers运行,Python 就知道包上下文了。或者干脆用绝对导入,一劳永逸。Python 的导入有两种方式:绝对导入:from myproject.utils.constants import MAX_FANS——从项目根开始,路径明确相对导入:from .constants import MAX_FANS——用.(当前包)和..(父包)定位相对导入的问题在于它依赖__package__属性。当直接运行脚本(python script.py)时,__package__为 None,相对导入就失败了。用python -m package.module运行可以正确设置包上下文。💡一句话带走:相对导入直接跑会报错,用 -m 或绝对导入坑点 5:模块级代码副作用 [入门]几天后,毛毛姐在模块顶层写了数据库连接代码。她的同事只是想 import 一个函数,结果数据库连上了。# ❌ 踩坑代码# database.pyimportsqlite3# 模块顶层代码——import 时就执行!conn=sqlite3.connect("fans.db")# ← 立即连接数据库cursor=conn.cursor()cursor.execute("CREATE TABLE IF NOT EXISTS fans (id INTEGER, name TEXT)")conn.commit()print("数据库已连接")# ← import 时就打印defquery_fans():returncursor.execute("SELECT * FROM fans").fetchall()# 同事只是想用另一个函数# from database import query_fans# ← 但 import 时数据库已经连了,还打印了 "数据库已连接"# ← 在测试环境里连了生产数据库!# ← CI/CD 里跑测试也连了数据库!# 更危险的例子# crawler.pyimportrequests API_KEY="sk-xxxxx"# ← 硬编码密钥# 启动爬虫(在模块顶层!)response=requests.get("https://api.example.com/init",headers={"key":API_KEY})# ← import crawler 就发了一个 HTTP 请求!# ✅ 正确写法# database.pyimportsqlite3 _conn=None_cursor=Nonedefget_connection():"""延迟初始化——调用时才连接"""global_conn,_cursorif_connisNone:_conn=sqlite3.connect("fans.db")_cursor=_conn.cursor()_cursor.execute("CREATE TABLE IF NOT EXISTS fans (id INTEGER, name TEXT)")_conn.commit()return_conn,_cursordefquery_fans():conn,cursor=get_connection()returncursor.execute("SELECT * FROM fans").fetchall()# 现在从 database import query_fans 不会触发任何副作用# 只有调用 query_fans() 时才连接数据库# 需要在启动时执行的代码,放 if __name__ 下if__name__=="__main__":print("初始化数据库...")get_connection()print(query_fans())毛毛姐:我在模块顶层写代码不是很正常吗?我:Python 的 import 会执行模块的所有顶层代码。你在顶层连数据库、发 HTTP 请求、打印日志——别人 import 一下就全触发了。测试环境 import 你的模块,连了生产数据库;CI 跑测试,发了一堆 HTTP 请求。模块顶层只放定义(函数、类、常量),执行逻辑放函数里,用if __name__ == '__main__'守护入口。Python 的 import 机制是执行时语义:import module会先创建模块对象并注册到sys.modules,再执行module.py的所有顶层代码,最后返回模块对象。这意味着模块顶层代码的副作用会在 import 时触发:数据库连接、网络请求 → import 就触发 I/Oprint()→ import 就打印修改全局状态 → import 就改变状态创建文件/目录 → import 就写磁盘最佳实践:模块顶层只放定义(def、class、常量赋值),所有执行逻辑放函数里。💡一句话带走:模块顶层只定义,执行逻辑放函数里坑点 6:importlib.reload 的陷阱 [深入]深夜,毛毛姐在 Jupyter Notebook 里改了一个模块的代码,重新 import 却发现改动没生效。# ❌ 踩坑代码# config.pyMAX_FANS=100defget_limit():returnMAX_FANS# Jupyter Notebook 中importconfigprint(config.MAX_FANS)# 100print(config.get_limit())# 100# 修改 config.py:MAX_FANS = 200# 重新 import(没用!)importconfigprint(config.MAX_FANS)# 100 ← 还是旧值!print(config.get_limit())# 100 ← 还是旧值!# 提前用 from-import 绑定了值快照fromconfigimportMAX_FANS# 用 reload 更新模块对象importimportlib importlib.reload(config)print(config.MAX_FANS)# 200 ← 模块属性更新了!print(MAX_FANS)# 100 ← from-import 的值快照没更新!# 更隐蔽的问题:reload 不递归# config.py 里如果有 import other_module# reload(config) 不会 reload other_module# ✅ 正确写法# 方法一:用模块名前缀访问(实时读属性,最可靠)importimportlibimportconfig importlib.reload(config)print(config.MAX_FANS)# 200 ✅print(config.get_limit())# 200 ✅# config 是模块对象,reload 更新了它的属性,前缀访问读到的是最新值# 方法二:Jupyter Notebook 的自动重载(推荐)%load_ext autoreload%autoreload2# 之后修改模块代码,自动重载,不用手动 reload# 方法三:理解 reload 的三大限制# 1. from module import value 的值快照不会更新# 2. reload 不递归子模块# 3. 类定义会创建新的类对象,旧实例还是旧类毛毛姐:reload 了还不生效?Python 在搞什么?我:importlib.reload(module)会重新执行模块代码,更新模块对象的属性。但如果你之前from config import MAX_FANS,MAX_FANS绑定的是一份值快照,reload 更新的是config模块对象,你手里的MAX_FANS变量还是旧值。用config.MAX_FANS前缀访问实时属性最可靠;或者 reload 后重新 from-import。importlib.reload(module)的行为:重新执行模块的顶层代码更新sys.modules中模块对象的属性(函数、变量、类)但 reload不会:更新已经通过from module import value绑定的值快照递归 reload 模块 import 的子模块更新旧类的实例(类定义变了,旧实例还是旧类)💡一句话带走:reload 更新模块对象,from-import 值快照不自动更新坑点 7:缺少 ifname== ‘main’ 守护 [入门]毛毛姐写了个工具模块,既有函数定义又有测试代码。同事 import 一下,测试代码全跑了。# ❌ 踩坑代码# fan_utils.pydefget_fan_score(fan_id):returnfan_id*10defformat_fan_name(name):returnname.strip().title()# 测试代码(在模块顶层!)print("开始测试...")assertget_fan_score(1)==10assertget_fan_score(2)==20assertformat_fan_name(" alice ")=="Alice"print("所有测试通过!")# 启动服务(也在顶层!)importsocket server=socket.socket()server.bind(("localhost",8080))server.listen(5)print("服务启动在 localhost:8080")# 同事 from fan_utils import get_fan_score# ← 打印了 "开始测试..."# ← 运行了所有 assert# ← 打印了 "所有测试通过!"# ← 绑定了 8080 端口!# ← 服务启动了!同事只是想用一个函数啊!# ✅ 正确写法# fan_utils.pydefget_fan_score(fan_id):returnfan_id*10defformat_fan_name(name):returnname.strip().title()# 测试代码放 if __name__ 守护下if__name__=="__main__":print("开始测试...")assertget_fan_score(1)==10assertget_fan_score(2)==20assertformat_fan_name(" alice ")=="Alice"print("所有测试通过!")# 启动服务也放这里importsocket server=socket.socket()server.bind(("localhost",8080))server.listen(5)print("服务启动在 localhost:8080")# 现在 from fan_utils import get_fan_score# ← 不打印任何东西# ← 不绑定端口# ← 不启动服务# ← 干干净净,只导入函数# 直接运行 python fan_utils.py# ← __name__ == "__main__" 为 True# ← 运行测试 + 启动服务毛毛姐:ifname是什么意思?我:每个 Python 模块都有一个__name__属性。当直接运行脚本时,__name__等于'__main__'。当被 import 时,__name__等于模块名(比如'fan_utils')。if __name__ == '__main__':就是「只有直接运行时才执行」的守护——import 时不执行。__name__是 Python 模块的内置属性:直接运行python module.py:__name__=='__main__'被 importimport module:__name__=='module'(模块名)if __name__ == '__main__':守护块内的代码只在直接运行时执行,被 import 时不执行。这是 Python 的惯用模式(idiom),用于:放测试代码放服务启动代码放 CLI 入口演示用法💡一句话带走:入口代码加 ifname守护,import 时不跑[深入] 根因串联:Python 的 import 是执行时语义回顾今天 7 个坑点,全部可以回溯到一个核心模型——Python 的 import 是执行时语义:导入即执行模块顶层代码。import module ↓ 检查 sys.modules 是否已缓存 ↓ 没有 创建模块对象并存入 sys.modules(先占位) ↓ 执行 module.py 的所有顶层代码 ├── 执行赋值语句(创建变量) ├── 执行 def 语句(创建函数) ├── 执行 class 语句(创建类) ├── 执行 import 语句(递归导入) └── 执行其他顶层代码(副作用!) ↓ 返回模块对象给调用者毛毛姐的踩坑日记 · 本期总结#坑一句话领悟79循环导入A 导 B、B 导 A 就死循环,延迟导入可破80init.py 过重init.py 只做轻量事,重活放函数里81import * 污染import * 是命名空间炸弹,别用82相对导入失败相对导入直接跑会报错,用 -m 或绝对导入83模块级副作用模块顶层只定义,执行逻辑放函数里84reload 陷阱reload 更新模块对象,from-import 值快照不自动更新85name守护入口代码加 ifname守护,import 时不跑三条黄金法则导入即执行——import 不是「加载文件」,是「执行模块顶层代码」。所有副作用都会触发。只导入定义——模块顶层只放def、class、常量。执行逻辑放函数里,入口代码加if __name__守护。显式优于隐式——用显式导入(from module import name),不用import *。用绝对导入,避免相对导入的直接运行问题。毛毛姐:今天学了个 import 三原则。导入即执行、只导入定义、显式优于隐式。我在哪儿?我在循环导入的死循环里转不出来了。我:行了,把你的测试代码塞进if __name__ == '__main__':里吧。下次别再在模块顶层连数据库了。下一期预告:毛毛姐用生成器处理大数据,第二次遍历发现是空的——「Python 的生成器用一次就没了?」🔗 如果你身边也有一个被 Python import 折磨的朋友,把这篇文章转给 ta——「你看,import 一下就把数据库连了,毛毛姐也崩了。」
Python 循环导入与模块导入的 7 个坑📋本期菜单:7 个模块与导入的坑,从「循环导入」到「name守护」[入门] 循环导入 · import * 污染 · 模块级副作用 ·name守护[进阶]init.py 过度初始化 · 相对导入失败[深入] reload 陷阱毛毛姐的项目从单文件长成了多模块。她把代码拆成了fans.py、stats.py、main.py,觉得结构清晰多了。然后python main.py一跑——ImportError: cannot import name 'get_fans' from partially initialized module 'fans'。「我就 import 了一下,它说我没初始化完?Python 的 import 到底怎么工作的?」坑点 1:循环导入 [入门]# ❌ 踩坑代码# fans.pyfromstatsimportcalculate_stats# ← 导入 statsdefget_fans():return["Alice","Bob","Charlie"]defprocess_fans():fans=get_fans()returncalculate_stats(fans)# 用 stats 的函数# stats.pyfromfansimportget_fans# ← 又导入 fans!defcalculate_stats(fans):return{"total":len(fans)}defsummary():fans=get_fans()# 用 fans 的函数returncalculate_stats(fans)# main.pyfromfansimportprocess_fansprint(process_fans())# ImportError: cannot import name 'get_fans' from partially# initialized module 'fans' (most likely due to a circular import)# ✅ 正确写法# 方法一:延迟导入(最简单的修复)# fans.pydefget_fans():return["Alice","Bob","Charlie"]defprocess_fans():fromstatsimportcalculate_stats# ← 延迟到函数内导入fans=get_fans()returncalculate_stats(fans)# stats.pydefcalculate_stats(fans):return{"total":len(fans)}defsummary():fromfansimportget_fans# ← 延迟到函数内导入fans=get_fans()returncalculate_stats(fans)# main.pyfromfansimportprocess_fansprint(process_fans())# {'total': 3} ✅# 方法二:重构依赖关系(更好的设计)# 把共享的数据/函数提取到单独模块# models.pydefget_fans():return["Alice","Bob","Charlie"]# stats.py(只依赖 models,不依赖 fans)frommodelsimportget_fansdefcalculate_stats(fans):return{"total":len(fans)}# fans.py(只依赖 models 和 stats)frommodelsimportget_fansfromstatsimportcalculate_statsdefprocess_fans():returncalculate_stats(get_fans())毛毛姐:fans 导入 stats,stats 又导入 fans,这不是合理的互相依赖吗?我:Python 的 import 是执行时语义——导入 fans 时,Python 开始执行 fans.py,遇到from stats import ...就转去执行 stats.py,stats.py 遇到from fans import get_fans又想回去执行 fans.py——但 fans.py 还没执行完!Python 拿到的是半成品,找不到get_fans。循环导入的根本原因是模块间的环形依赖。Python 的 import 机制是:先创建模块对象并注册到sys.modules,再执行模块顶层代码,最后返回模块对象。如果 A 的顶层代码还没执行完就 import B,而 B 又 import A,B 这时从sys.modules里拿到的 A 就是还没执行完的半成品——get_fans还没定义。解决方案的核心是打破环形依赖:要么延迟导入(把 import 移到函数内部),要么重构架构(提取共享层到第三方模块)。💡一句话带走:A 导 B、B 导 A 就死循环,延迟导入可破坑点 2:init.py 中过度初始化 [进阶]毛毛姐修好了循环导入,但发现import mypackage时程序卡了好几秒。# ❌ 踩坑代码# mypackage/__init__.py# 毛毛姐把所有初始化都塞进了 __init__.pyimportpygame# ← 重型依赖importnumpy# ← 重型依赖frommypackage.configimportload_configfrommypackage.databaseimportconnect_db# 加载配置文件config=load_config()# ← 读文件# 连接数据库db=connect_db(config)# ← 网络连接!# 初始化 pygamepygame.init()# ← 硬件初始化!# 用户只是想 import 一个工具函数# from mypackage.utils import format_name ← 这一行要等上面全部执行完# ✅ 正确写法# mypackage/__init__.py(保持轻量)# 只暴露公共 API,不做重逻辑frommypackage.utilsimportformat_name,format_datefrommypackage.statsimportcalculate_stats __all__=["format_name","format_date","calculate_stats"]__version__="1.0.0"# 重型初始化放到专用模块# mypackage/initializer.pydefinitialize():"""显式调用,不自动执行"""importpygameimportnumpyfrommypackage.configimportload_configfrommypackage.databaseimportconnect_db config=load_config()db=connect_db(config)pygame.init()return{"config":config,"db":db}# 用户按需调用# from mypackage import format_name ← 快!不触发重型初始化# from mypackage.initializer import initialize# resources = initialize() ← 需要时才初始化毛毛姐:init.py 不是用来初始化的吗?我:__init__.py的作用是标记一个目录是 Python 包,它可以用来暴露公共 API。但它不是「初始化文件」——里面的代码在import 包名时立即执行。如果你在里面放数据库连接、pygame 初始化这种重逻辑,用户只是想用一个工具函数,也得等全部跑完。保持init.py 轻量,重逻辑放函数里让用户按需调用。__init__.py在包被 import 时执行,它的作用是:标记目录为 Python 包暴露包的公共 API(from mypackage import useful_function)设置__all__、__version__等元信息它不应该做的事情:连接数据库、网络请求等 I/O 操作初始化重型第三方库执行耗时的计算修改全局状态💡一句话带走:init.py 只做轻量事,重活放函数里坑点 3:import * 污染命名空间 [入门]毛毛姐发现from module import *很方便——不用写模块名前缀。直到有一天,她的sum函数不工作了。# ❌ 踩坑代码# utils.pysum=0# 模块级变量,名字叫 sumcount=0name="utils"defhelper():pass# main.pyfromutilsimport*# ← 导入所有公开名字!fromstatsimport*# ← 又导入一波!# 问题1:覆盖内置函数print(sum([1,2,3]))# TypeError: 'int' object is not callable# sum 被 utils.sum = 0 覆盖了!# 问题2:不知道名字从哪来result=helper()# ← helper 从哪来的?utils 还是 stats?count=100# ← 这个 count 是我定义的还是 import * 来的?# 问题3:难以追踪# 当代码变大,5 个 import * 之后,你根本不知道# 每个名字来自哪个模块# ✅ 正确写法# 显式导入:明确知道每个名字从哪来fromutilsimporthelper,countfromstatsimportcalculate_stats# 或者用模块名前缀(最清晰)importutilsimportstats result=utils.helper()total=stats.calculate_stats([1,2,3])print(sum([1,2,3]))# 6 ← 内置 sum 没被覆盖# 如果模块定义了 __all__,import * 只导入 __all__ 中的名字# utils.py__all__=["helper","count"]# ← 只导出这两个sum=0# ← 不会被 import * 导出(但仍不建议用内置名)# from utils import * # 只导入 helper 和 count毛毛姐:import * 不是很方便吗?省得写模块名。我:方便是方便,但你不知道导入了哪些名字,也不知道它们从哪来。最危险的是覆盖内置函数——sum、type、id、list这些内置名一旦被覆盖,排查起来非常痛苦。显式导入每个名字,或者用模块名前缀,代码可读性和可维护性都好得多。from module import *的行为:如果模块定义了__all__,导入__all__中列出的所有名字如果没有__all__,导入所有不以_开头的模块级名字这会把大量名字注入当前命名空间,可能导致:覆盖内置函数(sum、type、id)覆盖同名变量(多个模块都有count、name)难以追踪名字来源(IDE 也帮不了你)💡一句话带走:import * 是命名空间炸弹,别用坑点 4:相对导入与绝对导入 [进阶]第二天,毛毛姐直接运行一个子模块,报了个看不懂的错。# ❌ 踩坑代码# 项目结构:# myproject/# __init__.py# main.py# utils/# __init__.py# helpers.py# constants.py# utils/helpers.pyfrom.constantsimportMAX_FANS# ← 相对导入defget_max():returnMAX_FANS# 直接运行 helpers.py# $ python utils/helpers.py# ImportError: attempted relative import with no known parent package# 原因:直接运行脚本时,__package__ 为 None# 相对导入(from . import / from .. import)需要包上下文# ✅ 正确写法# 方法一:用 python -m 运行(推荐)# $ python -m myproject.utils.helpers# 这样 Python 知道包上下文,相对导入能工作# 方法二:用绝对导入# utils/helpers.pyfrommyproject.utils.constantsimportMAX_FANS# ← 绝对路径defget_max():returnMAX_FANS# 方法三:脚本入口用绝对导入,包内用相对导入# utils/helpers.py(包内模块,用相对导入)from.constantsimportMAX_FANSdefget_max():returnMAX_FANS# main.py(脚本入口,用绝对导入)frommyproject.utils.helpersimportget_maxif__name__=="__main__":print(get_max())# 运行方式:在项目父目录执行 python -m myproject.main# 方法四:在脚本中手动修复包上下文(不推荐,仅调试用)importsysimportimportlib.utilif__package__isNoneor__package__=='':# 手动设置包路径sys.path.insert(0,'/path/to')# ← 项目父目录(能按名字找到 myproject)__package__='myproject.utils'毛毛姐:相对导入不是更简洁吗?为什么要报错?我:相对导入依赖包上下文——Python 需要知道当前模块属于哪个包。直接运行脚本时,Python 把脚本当成顶层模块,__package__为 None,相对导入就找不到参照系。用python -m myproject.utils.helpers运行,Python 就知道包上下文了。或者干脆用绝对导入,一劳永逸。Python 的导入有两种方式:绝对导入:from myproject.utils.constants import MAX_FANS——从项目根开始,路径明确相对导入:from .constants import MAX_FANS——用.(当前包)和..(父包)定位相对导入的问题在于它依赖__package__属性。当直接运行脚本(python script.py)时,__package__为 None,相对导入就失败了。用python -m package.module运行可以正确设置包上下文。💡一句话带走:相对导入直接跑会报错,用 -m 或绝对导入坑点 5:模块级代码副作用 [入门]几天后,毛毛姐在模块顶层写了数据库连接代码。她的同事只是想 import 一个函数,结果数据库连上了。# ❌ 踩坑代码# database.pyimportsqlite3# 模块顶层代码——import 时就执行!conn=sqlite3.connect("fans.db")# ← 立即连接数据库cursor=conn.cursor()cursor.execute("CREATE TABLE IF NOT EXISTS fans (id INTEGER, name TEXT)")conn.commit()print("数据库已连接")# ← import 时就打印defquery_fans():returncursor.execute("SELECT * FROM fans").fetchall()# 同事只是想用另一个函数# from database import query_fans# ← 但 import 时数据库已经连了,还打印了 "数据库已连接"# ← 在测试环境里连了生产数据库!# ← CI/CD 里跑测试也连了数据库!# 更危险的例子# crawler.pyimportrequests API_KEY="sk-xxxxx"# ← 硬编码密钥# 启动爬虫(在模块顶层!)response=requests.get("https://api.example.com/init",headers={"key":API_KEY})# ← import crawler 就发了一个 HTTP 请求!# ✅ 正确写法# database.pyimportsqlite3 _conn=None_cursor=Nonedefget_connection():"""延迟初始化——调用时才连接"""global_conn,_cursorif_connisNone:_conn=sqlite3.connect("fans.db")_cursor=_conn.cursor()_cursor.execute("CREATE TABLE IF NOT EXISTS fans (id INTEGER, name TEXT)")_conn.commit()return_conn,_cursordefquery_fans():conn,cursor=get_connection()returncursor.execute("SELECT * FROM fans").fetchall()# 现在从 database import query_fans 不会触发任何副作用# 只有调用 query_fans() 时才连接数据库# 需要在启动时执行的代码,放 if __name__ 下if__name__=="__main__":print("初始化数据库...")get_connection()print(query_fans())毛毛姐:我在模块顶层写代码不是很正常吗?我:Python 的 import 会执行模块的所有顶层代码。你在顶层连数据库、发 HTTP 请求、打印日志——别人 import 一下就全触发了。测试环境 import 你的模块,连了生产数据库;CI 跑测试,发了一堆 HTTP 请求。模块顶层只放定义(函数、类、常量),执行逻辑放函数里,用if __name__ == '__main__'守护入口。Python 的 import 机制是执行时语义:import module会先创建模块对象并注册到sys.modules,再执行module.py的所有顶层代码,最后返回模块对象。这意味着模块顶层代码的副作用会在 import 时触发:数据库连接、网络请求 → import 就触发 I/Oprint()→ import 就打印修改全局状态 → import 就改变状态创建文件/目录 → import 就写磁盘最佳实践:模块顶层只放定义(def、class、常量赋值),所有执行逻辑放函数里。💡一句话带走:模块顶层只定义,执行逻辑放函数里坑点 6:importlib.reload 的陷阱 [深入]深夜,毛毛姐在 Jupyter Notebook 里改了一个模块的代码,重新 import 却发现改动没生效。# ❌ 踩坑代码# config.pyMAX_FANS=100defget_limit():returnMAX_FANS# Jupyter Notebook 中importconfigprint(config.MAX_FANS)# 100print(config.get_limit())# 100# 修改 config.py:MAX_FANS = 200# 重新 import(没用!)importconfigprint(config.MAX_FANS)# 100 ← 还是旧值!print(config.get_limit())# 100 ← 还是旧值!# 提前用 from-import 绑定了值快照fromconfigimportMAX_FANS# 用 reload 更新模块对象importimportlib importlib.reload(config)print(config.MAX_FANS)# 200 ← 模块属性更新了!print(MAX_FANS)# 100 ← from-import 的值快照没更新!# 更隐蔽的问题:reload 不递归# config.py 里如果有 import other_module# reload(config) 不会 reload other_module# ✅ 正确写法# 方法一:用模块名前缀访问(实时读属性,最可靠)importimportlibimportconfig importlib.reload(config)print(config.MAX_FANS)# 200 ✅print(config.get_limit())# 200 ✅# config 是模块对象,reload 更新了它的属性,前缀访问读到的是最新值# 方法二:Jupyter Notebook 的自动重载(推荐)%load_ext autoreload%autoreload2# 之后修改模块代码,自动重载,不用手动 reload# 方法三:理解 reload 的三大限制# 1. from module import value 的值快照不会更新# 2. reload 不递归子模块# 3. 类定义会创建新的类对象,旧实例还是旧类毛毛姐:reload 了还不生效?Python 在搞什么?我:importlib.reload(module)会重新执行模块代码,更新模块对象的属性。但如果你之前from config import MAX_FANS,MAX_FANS绑定的是一份值快照,reload 更新的是config模块对象,你手里的MAX_FANS变量还是旧值。用config.MAX_FANS前缀访问实时属性最可靠;或者 reload 后重新 from-import。importlib.reload(module)的行为:重新执行模块的顶层代码更新sys.modules中模块对象的属性(函数、变量、类)但 reload不会:更新已经通过from module import value绑定的值快照递归 reload 模块 import 的子模块更新旧类的实例(类定义变了,旧实例还是旧类)💡一句话带走:reload 更新模块对象,from-import 值快照不自动更新坑点 7:缺少 ifname== ‘main’ 守护 [入门]毛毛姐写了个工具模块,既有函数定义又有测试代码。同事 import 一下,测试代码全跑了。# ❌ 踩坑代码# fan_utils.pydefget_fan_score(fan_id):returnfan_id*10defformat_fan_name(name):returnname.strip().title()# 测试代码(在模块顶层!)print("开始测试...")assertget_fan_score(1)==10assertget_fan_score(2)==20assertformat_fan_name(" alice ")=="Alice"print("所有测试通过!")# 启动服务(也在顶层!)importsocket server=socket.socket()server.bind(("localhost",8080))server.listen(5)print("服务启动在 localhost:8080")# 同事 from fan_utils import get_fan_score# ← 打印了 "开始测试..."# ← 运行了所有 assert# ← 打印了 "所有测试通过!"# ← 绑定了 8080 端口!# ← 服务启动了!同事只是想用一个函数啊!# ✅ 正确写法# fan_utils.pydefget_fan_score(fan_id):returnfan_id*10defformat_fan_name(name):returnname.strip().title()# 测试代码放 if __name__ 守护下if__name__=="__main__":print("开始测试...")assertget_fan_score(1)==10assertget_fan_score(2)==20assertformat_fan_name(" alice ")=="Alice"print("所有测试通过!")# 启动服务也放这里importsocket server=socket.socket()server.bind(("localhost",8080))server.listen(5)print("服务启动在 localhost:8080")# 现在 from fan_utils import get_fan_score# ← 不打印任何东西# ← 不绑定端口# ← 不启动服务# ← 干干净净,只导入函数# 直接运行 python fan_utils.py# ← __name__ == "__main__" 为 True# ← 运行测试 + 启动服务毛毛姐:ifname是什么意思?我:每个 Python 模块都有一个__name__属性。当直接运行脚本时,__name__等于'__main__'。当被 import 时,__name__等于模块名(比如'fan_utils')。if __name__ == '__main__':就是「只有直接运行时才执行」的守护——import 时不执行。__name__是 Python 模块的内置属性:直接运行python module.py:__name__=='__main__'被 importimport module:__name__=='module'(模块名)if __name__ == '__main__':守护块内的代码只在直接运行时执行,被 import 时不执行。这是 Python 的惯用模式(idiom),用于:放测试代码放服务启动代码放 CLI 入口演示用法💡一句话带走:入口代码加 ifname守护,import 时不跑[深入] 根因串联:Python 的 import 是执行时语义回顾今天 7 个坑点,全部可以回溯到一个核心模型——Python 的 import 是执行时语义:导入即执行模块顶层代码。import module ↓ 检查 sys.modules 是否已缓存 ↓ 没有 创建模块对象并存入 sys.modules(先占位) ↓ 执行 module.py 的所有顶层代码 ├── 执行赋值语句(创建变量) ├── 执行 def 语句(创建函数) ├── 执行 class 语句(创建类) ├── 执行 import 语句(递归导入) └── 执行其他顶层代码(副作用!) ↓ 返回模块对象给调用者毛毛姐的踩坑日记 · 本期总结#坑一句话领悟79循环导入A 导 B、B 导 A 就死循环,延迟导入可破80init.py 过重init.py 只做轻量事,重活放函数里81import * 污染import * 是命名空间炸弹,别用82相对导入失败相对导入直接跑会报错,用 -m 或绝对导入83模块级副作用模块顶层只定义,执行逻辑放函数里84reload 陷阱reload 更新模块对象,from-import 值快照不自动更新85name守护入口代码加 ifname守护,import 时不跑三条黄金法则导入即执行——import 不是「加载文件」,是「执行模块顶层代码」。所有副作用都会触发。只导入定义——模块顶层只放def、class、常量。执行逻辑放函数里,入口代码加if __name__守护。显式优于隐式——用显式导入(from module import name),不用import *。用绝对导入,避免相对导入的直接运行问题。毛毛姐:今天学了个 import 三原则。导入即执行、只导入定义、显式优于隐式。我在哪儿?我在循环导入的死循环里转不出来了。我:行了,把你的测试代码塞进if __name__ == '__main__':里吧。下次别再在模块顶层连数据库了。下一期预告:毛毛姐用生成器处理大数据,第二次遍历发现是空的——「Python 的生成器用一次就没了?」🔗 如果你身边也有一个被 Python import 折磨的朋友,把这篇文章转给 ta——「你看,import 一下就把数据库连了,毛毛姐也崩了。」