1. 为什么每个Python脚本必写的这段代码很多人其实没真正吃透先说个我真实的经历。某次帮同事排查一段数据处理脚本他在测试时手动执行一点问题没有但被别人写的主程序import之后莫名其妙跑了两遍全量逻辑。我一看就明白了——他把核心逻辑直接裸放在模块顶层没包进if __name__ __main__里import时自然被逐行执行了一遍。这种错误在每个Python技术群里几乎每周都能看到一次可见这个看起来很简单的语法很多人只是会抄并没有真正理解它的执行机制。另一类常见困惑是为什么有些脚本里写了这个判断有些没有模板生成的代码里明明都有加了对不对删了行不行我在带新人时发现很多人把这个问题当成固定写法背下来了事但实际上它是理解Python模块机制、脚本组织方式、甚至是工程化项目结构的一把钥匙。这篇博文的定位不是再给你念一遍官方文档而是把这句话从内到外拆开Python解释器启动时到底干了什么、__name__这个变量为什么在不同场景下有不同的值、它在库开发、命令行工具、测试、多进程等真实场景下怎么用以及我踩过的那些和它相关的坑。不管你是刚学Python的新手还是写了两三年的老手看完多少能有些原来如此的收获。2. Python解释器的启动路径脚本是运行出来的不是编译出来的2.1 直接运行与import导入是完全不同的两条路要先说清楚一个核心点Python里没有传统意义上的程序入口。C语言里有个main()函数Java里有个main方法但Python解释器是逐行从上往下执行代码的。那么if __name__ __main__到底拦在哪里这得先看你执行文件的两种方式。第一种是直接运行也就是在命令行里敲python my_script.py。这种情况下Python解释器启动后首先要确定入口模块是谁。它会把my_script.py这个文件当作主模块来加载并且在这个模块的全局命名空间里把内置变量__name__的值设置成字符串__main__。然后开始逐行执行文件里的每一行代码。第二种是被导入。当另一个脚本里写了import my_script或者from my_script import something时my_script.py就不是启动入口而是作为一个普通模块被加载。这种情况下Python解释器会把它的__name__设置成模块的名字也就是my_script更准确地说是它在包结构中的完整导入路径比如package_foo.my_script。接下来它同样会从上往下执行所有顶层代码但这时候if __name__ __main__里的判断就为假了缩进块里的内容自然就不会执行。很多人在这里会产生一个疑问import一个模块也应该执行它的代码吗对Python的设计哲学就是这样——模块的顶层代码在首次导入时会执行这是它初始化自身逻辑的方式比如定义函数、类、设置全局变量等。函数体和类体只是定义而已不会立即执行内部逻辑。但如果顶层有直接调用的语句import时确实就会跑一遍。也正因如此代码裸奔在顶层才会成为隐患。我举个最简单的例子来说明假设helper.py内容如下print(helper模块被加载了) def add(a, b): return a b print(计算结果, add(3, 5))当你import helper时控制台会依次输出两行文字包括那个计算结果8。这在某些场景下是帮倒忙的——你只是想要一个加法函数结果却被脆生生地打印了一堆东西。把第二段print放进if __name__ __main__保护起来才是正确做法。2.2__name__到底是什么它是魔法变量但不是魔法__name__以双下划线开头结尾属于Python的内置系统变量。这类变量是解释器在特殊时机自动设置进去的不用你手动定义。它的作用简单来说就是标记当前代码块的身份。类比我平时给人解释的方式每个文件都像一个演员__name__就是演员胸前的名牌。当他作为主角走上舞台被当作主脚本直接运行名牌写的是主演当他作为配角跑龙套被其他脚本导入名牌写的是配角名。Python解释器就是通过看这个名牌来决定怎么对待这个文件的。注意一个小细节__name__和其他内置函数如__file__、__package__、__doc__一样既可以直接在模块层访问也可以在函数内访问。但如果你想在某个模块里使用它来判断当前这个模块是不是主入口必须判断的是当前模块全局作用域里的__name__不是别的模块的。这也是为什么在导入进来时模块内部代码判断的是自己的__name__而不是别人的。还有一点值得一提在交互式解释器REPL里敲代码__name__的值是__main__因为那个交互环境本身就是主入口。所以在Jupyter Notebook的某个cell里直接写print(__name__)你会看到也是__main__。它遵循的是同样逻辑——当前解释器实例的顶级作用域就是__main__。3. 运行时到底发生了什么从逐行执行到命名空间隔离3.1 Python执行文件的两个阶段编译与运行要真正理解上面的行为差异得把Python加载一个文件的内部过程拆成两步。第一步是编译。在执行之前Python先把源代码解析成抽象语法树AST再编译成字节码bytecode。这一步保证了语法正确性但不执行代码。我们平时常见的__pycache__/xxx.cpython-311.pyc文件就是解释器把编译结果缓存起来下次直接从字节码运行省去重新解析编译的耗时。第二步才是执行。解释器从上到下、逐行执行编译后的字节码遇到def创建函数对象但不执行内部语句遇到class创建类对象并执行类体语句遇到普通的print、赋值、循环、函数调用该执行就执行。而if __name__ __main__本质上也只是一条普通的判断语句只不过执行它的解释器上下文是主模块还是被导入模块决定了结果真假。这就解释了一个高频疑惑为什么函数写在if __name__ __main__后面也能正常被导入因为导入模块时解释器会先把def add(...)这行执行一遍把函数对象注册到模块命名空间里此时判断语句虽然为假但函数已经定义好了。等外部代码调用helper.add(1, 2)时直接去模块命名空间取这个函数对象完全不受影响。我见过很多人在函数定义之前调用函数然后报NameError这跟__name__判断没关系纯粹是执行顺序决定一切没理解透。if __name__ __main__的存在保证的是导入时的副作用可控不是保证函数定义顺序。3.2 sys.modules、导入缓存与重复执行问题再往里挖一层。Python里所有被导入的模块都会以模块名为key、以模块对象为value被记录在sys.modules这个全局字典里。第一次import helper时解释器去磁盘找文件编译执行放入缓存。第二次再import helper解释器直接在缓存里找到不会再次执行模块代码。这就是模块天然的单例行为。这个机制和if __name__ __main__之间有个很有意思的组合坑如果同一个文件既被当主脚本运行又被其他模块导入那它在系统里就是两个身份。假设main.py和util.py都执行import config此时config只加载一次但如果util.py被两个不同入口分别导入一次由主进程一次由某个子进程就得看是不是同一进程内了。这也是分布式计算或者多进程开发时容易对main保护产生误解的来源——注意多进程模式下每个子进程是全新启动的解释器if __name__ __main__在每个进程里的判断逻辑是一样的。举个典型场景一个神经网络训练脚本顶层写了加载数据、定义模型、启动训练的代码并且外层用if __name__ __main__保护。开发者在程序里用多进程来并行处理数据子进程通过multiprocessing.Process(targetworker)启动。新启动的子进程会重新 import 主脚本文件——因为multiprocessing在Unix上默认用fork方式Windows上则要通过序列化重新执行模块导入。如果训练逻辑没被保护子进程一启动就把加载数据、训练模型又跑了一遍轻则浪费资源重则直接崩溃。这也是为什么成熟的项目里凡是有进程创建逻辑一定会在主入口处严格使用if __name__ __main__。4. 一句话的两个分支if __name__ __main__在不同角色下的实战应用4.1 角色一给脚本加上命令行开关先从一个最朴素的用途说起——把你的Python文件设计成既可以当库被导入也可以当脚本直接执行。这也是这个语法最核心的应用场景。最常见的是写命令行工具。比如我写一个数据清洗的模块cleaner.py主要逻辑是clean_csv(path)但它不应该在导入时自动执行任何清洗操作。常见做法是import sys def clean_csv(path: str) - None: 清洗CSV文件中的空值和重复行 ... if __name__ __main__: # 当且仅当直接执行 python cleaner.py 时才进入 if len(sys.argv) 2: print(用法: python cleaner.py csv路径) sys.exit(1) clean_csv(sys.argv[1])这样别人在你的模块基础上二次开发时只from cleaner import clean_csv不会触发任何清洗动作但你在自己终端里又能顺手直接跑这个文件测试两不耽误。再往工程化一点说你自己维护的某个项目根目录下可能有main.py作为唯一启动入口。它的开头长这样import argparse from app import create_app if __name__ __main__: parser argparse.ArgumentParser(description业务服务启动入口) parser.add_argument(--host, default0.0.0.0) parser.add_argument(--port, typeint, default8000) args parser.parse_args() app create_app() app.run(args.host, args.port, debugFalse)这里的价值是什么是把启动逻辑和业务定义分开。create_app是应用工厂任何模块都可以构建它但通过命令行启动项目的 副作用解析参数、绑端口只属于主入口。测试代码里from app import create_app绝不会因为导入而绑上端口这就是隔离的意义。4.2 角色二让一个模块天然成为可测试的测试桩这个用途很多人没意识到。如果你在一个脚本文件里写了大量测试辅助代码希望直接运行该文件时能快速自测但又不希望它污染导入方那么在这个文件底部写if __name__ __main__: # 简单自测 result clean_csv(sample_input.csv) print(自测通过清理后剩余行数:, len(result))这在开发调试期极其好用。我维护过的一些小工具类脚本基本全是这种结构——真正的业务逻辑放上面if __name__ __main__里面放几行手动冒烟测试。写的时候顺手执行一下确认改了不破坏功能等到所有功能稳定了再把这几行删掉或者完整迁移到pytest用例里去。但这里要提醒一句别把正式测试都写成这样。因为它没有断言体系、没有测试报告、也运行不了pytest的彩色输出和钩子只适合快速看个响。更合理的方式是——if __name__ __main__只保留最小化的调试入口正式的单元测试还是要去tests目录下用pytest组织。这个原则很多人走偏了有人把大量断言、模拟数据的代码全堆在主入口块里最后代码库乱成一锅粥。4.3 角色三在服务化、微服务架构下的最小化启动模式再说一个和工作相关的场景。我参与过的某个项目里有一段负责定时调度任务的代码早期版本是这么写的# scheduler.py import time def run_all_jobs(): 执行所有注册的定时任务 ... while True: run_all_jobs() time.sleep(60)这段代码单独运行一点问题没有。问题出在后来代码重构另一个模块需要import scheduler来复用run_all_jobs函数。于是每次只要有别的模块连带导入它后台就立刻多出一个死循环进程。最终改法很简单把死循环挪进if __name__ __main__里代码如下# scheduler.py import time def run_all_jobs(): 执行所有注册的定时任务 ... if __name__ __main__: while True: run_all_jobs() time.sleep(60)这是一类很典型的服务边界问题。类似于把作为进程运行的入口和作为功能部件的实现分开对微服务、批量任务、后台常驻进程的开发非常关键。一个模块在架构中的角色应该是明确的要么是进程入口负责启动和生命周期要么是功能模块负责具体业务逻辑两者混在一起必然导致import链路不可控。5. 代码执行顺序与常见误用为什么有些逻辑放在这个判断里不灵5.1 别在主入口块里放全局需要的初始化这是初学者最常见的一个错位。我举个例子一个人想设置日志格式他写了# utils.py import logging if __name__ __main__: logging.basicConfig(levellogging.INFO, format%(asctime)s - %(message)s) def get_logger(name): logger logging.getLogger(name) ... # 使用logger然后他在main.py里from utils import get_logger打算调用get_logger(test)发现日志格式根本没变。为什么因为utils.py被导入时if __name__ __main__判断为假basicConfig根本没有执行。日志配置属于模块加载后就要生效的全局副作用它应该放在模块顶层或者放在应用的统一初始化模块里而不是放在主入口保护块里。这个教训我之前反复给团队强调if __name__ __main__适合放的是本文件直接运行时才需要的启动流程不适合放任何情况下导入本模块都需要的初始化动作。判断逻辑很简单——如果别的模块import你之后这个动作还必须发生那就别放进这个判断里如果这个动作只在你直接运行时才需要那就放进去。5.2 拆包、参数解析放在判断外变量作用域带来的隐蔽问题再举一个稍微进阶但真实发生的坑。有人写命令行工具这样组织import sys def main(script_path): ... if __name__ __main__: path sys.argv[1] if len(sys.argv) 1 else default.csv main(path) # 后来想在main外面用path发现path not defined这种问题的根子在于if __name__ __main__块内定义的变量是模块全局作用域里的理论上块外面也能访问到。但如果你后续在模块顶层再次引用它时该块没执行过比如作为库被导入那变量确实不存在。看起来是NameError本质是这个变量只在一种加载路径下被定义代码其他地方却默认它一定存在。这给我们一个操作规范凡是模块内多处要使用的配置变量应该在模块顶层定义默认值、在if __name__ __main__里仅覆盖/调整它。比如config_path config_default.yaml if __name__ __main__: if len(sys.argv) 1: config_path sys.argv[1] app build_app(config_path) app.run()这样既给了库导入时的默认值又保留主入口覆盖参数的灵活性。如果模块顶层直接引用sys.argv[1]就会在导入时因为参数列表为空而报IndexError。5.3 别把所有代码都塞进主入口块有种反模式是把几百行业务代码全写进if __name__ __main__:的缩进块里。逻辑上没问题但代码可读性和可测试性很差。主入口块只应该是几行启动指令真正的工作函数都要定义在模块作用域。这其实是Python社区比较公认的风格主入口块小而干脆。我自己Review代码时有一条隐性标准直接看if __name__ __main__:块的行数如果超过20行就会让人警醒是不是把不该放的东西放进来了。正常的入口块应该简洁到一屏能看完比如创建解析器、读取配置、调用某个函数、启动服务。6. 深入一个真实案例场景当__name__与包结构、字节码优化相遇6.1 包内部模块的__name__长什么样刚才说__name__可能是模块名比如my_script但如果你有一个包my_package里面包含子模块utils.py那么utils.py这个模块的__name__值在import时是my_package.utils不是utils。这就带来了一个深层问题如果你在包里用字符串判断——比如if __name__ utils:会永远为假如果你用if __name__ __main__:那它只在这个文件作为入口运行时为真。这两种判断的语义是完全不同的。多说一嘴在调试代码时直接print(__name__)查看当前模块身份是定位为什么逻辑没执行最高效的手段之一。如果你发现一个文件被导入时某个条件块没有运行先在文件顶部打印__name__看看实际值是什么很多时候马上就能判断出是入口身份还是模块身份。6.2 Python -O 参数下__name__相关代码是否受影响另一个容易被忽视的点是用python -O运行脚本时解释器会优化掉assert语句。很多人担心if __name__ __main__会不会因为优化而失效从而带来危险。答案是不会。-O只影响assert和基于__debug__的条件表达式不会改动普通的if判断。__name__是运行时动态变量的值判断与字节码优化无关。这个点我在一次面试中被问过——如果python -O 运行会有哪些代码不执行 如果只知道assert消失不够真正稳妥的回答是 assert 消失且if __debug__:块不执行但if __name__ __main__:完全不受影响。这种细节平时用不到但它能检验一个人对解释执行模型理解得是否通透。6.3 一个共享代码但独立入口的设计案例去年我在一个模拟项目里负责把一套算法库同时应用到两个服务上。算法库内部要启动一个预热函数只在某个服务启动时执行但算法函数本身两个服务都要用。抽象出来大概是# algorithm_engine.py def warm_up(): 预热模型或加载缓存 ... def predict(feature): 核心推理逻辑 ...两个服务各自有入口service_a.py 和 service_b.py按照需求只有service_a需要预热。最简单的做法是service_a.py里面加一行from algorithm_engine import warm_up warm_up()但这样导入关系就乱了——service_b只要导入algorithm_engine即使没显式调用只要warm_up在模块顶层被执行也会受影响。最后的设计是# algorithm_engine.py def warm_up(): ... def predict(feature): ... if __name__ __main__: # 只支持直接运行时手动预热 warm_up() print(algorithm_engine 预热完成)而service_a在启动逻辑里显式调用warm_up()service_b完全不调用。这样三个文件各司其职代码库的入口关系清楚也不存在import造成副作用的隐患。这种库本身自带一个可执行入口但外部调用与否完全取决于业务方的用法就是对这个语法最好的诠释。7. 面试题里的变形与深挖这些衍生场景你都考虑过吗7.1 多文件之间的相互入口怎么处理有一次我面试候选人时问到一个开放性问题如果有两个文件 a.py 和 b.py分别都写了自己的if __name__ __main__然后a import b、b import a会发生什么答案是它会陷入循环导入但能不能成功取决于谁先导入谁以及导入时定义的顺序。if __name__ __main__在这里并不能拯救你因为循环导入的问题是模块尚未完全初始化就又被反向导入和入口判断无关。想真正解决循环依赖得靠重构公共代码到第三个模块、延迟导入等方案。这个例子想说明的是__name__判断解决的是执行时机问题不是依赖结构问题两者别混为一谈。7.2 在类内部判断__name__是否可行有人会问我在类的静态方法里写if __name__ __main__可以吗当然可以但要注意这个判断发生在方法被调用的时刻而不是模块被加载的时刻。如果这个方法在模块导入后被外部调用且当时这个文件没有作为主入口运行那__name__就不是__main__条件照样不成立。绝大多数情况下这个判断应该放在模块顶层因为只有顶层代码的加载时机和导入时机是一致的。放到类方法、函数内部语义就变了容易产生为什么我在函数里调用不生效的困惑。补充一个冷知识python -m方式运行比如python -m my_package.script会把该模块的__name__设置成__main__。这和直接python my_package/script.py运行是等价的入口效果但模块搜索路径、包结构解析上会有细微差别-m会以当前目录加入模块搜索路径。所以判断条件完全覆盖这种启动方式不需要额外写分支。7.3 IDE与工具链对主入口的不同处理还有一个实际开发中一定会遇到的场景IDE里点击绿色三角形运行脚本时背后执行的命令等价于python file.py但如果你配置了pytest运行pytest导入模块时走的是import路径。这就导致某段代码在IDE里直接运行正常但pytest把模块导入进来时if __name__ __main__块不会执行自然就测不到入口块里的逻辑。这不算bug而是标准的模块隔离行为。理解这一点你就明白为什么自动化测试一定要直接调用函数而不是通过运行入口块来测。8. 与模块机制相关的常见坑import链上的隐形执行和全局状态污染8.1 导入即执行导致的全局变量污染if __name__ __main__虽然隔离了主入口块但模块顶层仍然会执行。这带来一个问题如果你在顶层定义一个可变全局变量并且入口块里修改了它导入方依然会看到修改后的值吗举一个我正在维护的旧代码里的例子# config_manager.py global_config {env: dev} def load_config_from_file(): ... if __name__ __main__: global_config[env] prod load_config_from_file()一个程序里同时存在两个入口脚本都导入了config_manager。第一个入口直接运行了config_manager.py作为主脚本它把global_config改成prod。第二个入口是另一个脚本import config_manager后拿到的是同一个global_config对象——是的因为在同一进程里sys.modules缓存了它两个入口共享同一个对象。这个问题的本质是全局可变状态在模块间共享而__name__判断只是触发了第一次修改的时机。要防止这种情况应该把配置加载收敛成显式函数调用而不是依赖入口块的坚持执行。这是架构设计层面的坑比单纯少写一个判断要隐蔽得多。8.2 为什么测试文件放一起时入口保护差点酿成麻烦还有个和我实战相关的细节。某次给一批数据采集器写自动化测试conftest.py 里有一段公共的fixture依赖一个data_loader.py。而data_loader.py本身是设计成命令行工具直接运行的文件末尾长这样if __name__ __main__: loader DataLoader(sys.argv[1]) loader.run()问题是pytest执行时会importdata_loader这个块不会执行没问题。但如果你在当前目录下直接执行python data_loader.py时文件会正常进入主入口块。看起来毫无冲突。真正麻烦的是有的团队习惯在__main__.py里调用其他模块的入口块比如这样# __main__.py from data_loader import SomeClass ... if __name__ __main__: SomeClass().run()当外部用python -m my_package启动整个包时文件__main__.py的__name__是__main__但同一个包的data_loader.py被import时的__name__是my_package.data_loader。所以包入口和模块入口是两个完全不同的主入口身份你写在各文件内部的条件块各管各的互不干扰。这是好事但也意味着只要启动链路从包入口走模块内部的主入口块永远不会执行你的初始化逻辑如果只放在那里就等于没放。这一点在写包结构时要时刻记住。8.3 动态执行exec/eval时__name__的隔离Python还允许用exec或eval动态执行代码字符串比如在插件系统、模板引擎里很常见。当你对一个代码字符串调用exec时如果不传独立的命名空间它会默认继承当前作用域的局部变量。此时如果字符串代码里也有if __name__ __main__:它判断的实际上是执行现场所在模块的__name__而不是字符串自带的语义。这意味着如果你在某个模块里用exec执行了一个本该作为脚本运行的字符串主入口判断可能意外为真或者为假非常容易混乱。我处理过的一个场景是某系统的配置脚本引擎它允许用户在配置中写Python代码片段引擎内部用exec执行。用户写了if __name__ __main__:引擎执行时因为当前模块就叫config_engine这个判断为假用户的启动逻辑永远不跑。解决办法是在exec调用时传入一个自定义的命名空间把__name__明确设置为__main__。细节如下namespace {__name__: __main__} exec(user_code, namespace)这段代码就相当于告诉动态执行环境这份代码现在是主角。这个技巧在插件系统内非常好用也是深入理解__name__机制后能自然延伸出来的应用。9. 超越单文件if __name__ __main__在工程组织上的设计哲学9.1 从能不能用到怎么设计入口可读性与可维护性写代码写到一定阶段你不再关心语法本身而是关心怎么组织代码让人一眼看懂。if __name__ __main__之所以成为Python社区约定俗成的标准写法本质上它提供一个非常清晰的信号——这个缩进块里就是进程的出生点。我Review项目代码时总是先找主入口块。它告诉我这个服务的生命周期从哪里开始加载配置在哪、初始化资源在哪、启动服务在哪。如果一个模块没有主入口块那它很可能是一个纯功能库如果有它就是一个可执行单元。这种一眼定位带来的维护效率提升远比语法本身的收益大得多。所以我建议每个自己维护的项目都要有意识地设计主入口分层最外层if __name__ __main__调一个main()函数或run()函数里面只有3~5行启动指令第二层main()函数内部处理参数解析、配置加载、组装对象第三层真正的业务逻辑都在类、函数里与入口完全解耦。这样做有一个附带好处测试可以直接针对main()写集成测试通过传入mock参数也可以针对业务逻辑写单元测试二者互不干扰。反观一上来就把几百行逻辑塞进入口块的项目测试根本无从下手。9.2 一个标准项目骨架的长什么样拿我自己经常用的项目骨架举例目录结构大致是project_root/ ├── my_app/ │ ├── __init__.py │ ├── __main__.py # 包级入口支持 python -m my_app │ ├── config.py # 配置模块不含入口块 │ ├── core.py # 核心逻辑不含入口块 │ └── utils.py # 工具函数 ├── tests/ │ └── test_core.py └── pyproject.toml__main__.py内容极其简单from .core import run if __name__ __main__: run()core.py里定义run()def run(): # 解析参数、初始化、启动 ...这样一来python -m my_app和python my_app/__main__.py都能运行pytest测试时导入my_app.core也不会触发任何副作用。这就是现代Python工程的常规布局。老话讲模块即文件入口即模块当入口被控制在一个明确的小文件里整个项目的启动链路就是透明、可控的。9.3 从单机脚本到库开发什么时候该保留主入口最后说一个封装库的场景。假设你写了一个库打算发布给其他人pip install使用。你当然可以在模块里保留if __name__ __main__作为开发者自测入口但要注意别人安装后通常是调用你的公开API而不是执行文件本身。如果库文件里主入口块包含了不该被外部使用的硬编码路径或敏感操作那这等于埋了一颗雷。我在审查代码时也不止一次看到有人把含硬编码密钥的初始化逻辑放在主入口块里看似很隐蔽实际任何拿到源码的人运行一下文件就能看到。所以库文件里的主入口块要么只放自测代码要么干脆删掉敏感操作绝不该出现。10. 我把这段代码和工程化理解挂钩之后的变化以及给同样在学Python的人一点建议说实话我刚学Python的时候也只是把它当成固定模板甚至有一段时间习惯性地在每一个文件末尾都加上这段判断包括只有一个函数的工具模块。后来项目规模变大了代码被很多人import我才慢慢体会到这不是一个要不要加的问题而是一个这个文件的角色是什么的问题。它是入口就用它来定义启动路径它是库就尽量保持纯净不该有的副作用一律不留。我现在的习惯是三步走第一每个准备独立运行的文件明确写出主入口块但只放启动指令第二每个纯库文件顶层代码精简化入口判断可有可无但一旦有就附带自测示例第三包结构的项目入口统一收敛到__main__.py所有模块内部都避免依赖作为主脚本运行这个状态。如果你正在学习Python或者被这段代码困扰过我建议你自己做一次实验建一个文件第一行print(__name__)然后分别用python file.py和import两种方式看输出。再把这个观察放到一个有包结构的例子里跑一遍你就能彻底理解整个机制。不用背结论直接去看运行时真实发生了什么比什么教程都管用。代码里的主角意识说到底是一种边界意识知道什么时候该上场什么时候该安静地待在后台。理解了这一点你对Python模块系统的理解就算真正上了一层台阶。