Python编程之顺序执行与程序的主入口详解
前言Python 程序是从上到下顺序执行的解释器读到一个语句就执行一个。这一点和 很多只在main()里跑逻辑的语言不太一样——Python 里没有强制的入口函数模块 顶层的任何一条语句都会被执行。由此带来一个常见误解以为只有主动运行的代码才会执行。实际上import一个模块时那个模块的顶层语句会被完整执行一遍。所以如果你在 模块顶层写了print、读文件、连数据库别人一import就会触发这些副作用。 这就是if __name__ __main__:这个主入口守卫存在的理由。本文讲清楚三件事顶层代码的执行顺序、import的行为执行一次并缓存、 以及__name__是怎么把被导入的模块和直接运行的脚本区分开的。示例以 Python 3 为准。一、顶层语句自上而下执行一个模块被加载时解释器逐行执行它的顶层语句。函数和类只在这一行被执行时才被创建并绑定名字所以先定义、后使用是硬规则。# 适用于 Python 3.8print(第一步) # 立刻打印def hello(): # 执行到这里hello 才被创建print(第二步)hello() # 现在才能调用print(第三步)输出依次是第一步 / 第二步 / 第三步。如果把hello()写到def hello()之前 就会在运行时抛NameError——因为执行到那一行时名字hello还没被绑定。二、import 执行模块但只执行一次import一个模块会找到文件、执行它的顶层代码、把模块对象缓存进sys.modules。同一个模块在同一个进程里只会被执行一次之后再import直接复用缓存不会重跑。# 适用于 Python 3.8# 文件 mod.pyprint(mod 正在加载)VALUE 42# 适用于 Python 3.8import mod # 输出: mod 正在加载import mod # 什么都不输出命中缓存print(mod.VALUE) # 42这也带来一个副作用运行时改动了模块文件已经在内存里的模块不会自动更新 需要显式重新加载。另外模块级的可变状态比如一个列表是整个进程共享的。三、__name__脚本与模块的分水岭每个模块都有一个__name__被导入时__name__等于模块名如mod。作为脚本直接运行时__name__等于字符串__main__。于是就有了那个到处都能见到的守卫# 适用于 Python 3.8def main():print(业务逻辑在这里)if __name__ __main__:main()python thisfile.py运行__name__ __main__成立main()被调用。import thisfile__name__是模块名那段if不成立main()不会自动跑——调用方想用时自己thisfile.main()。顺带纠正旧写法早期 Python 2 教程里这段示例用的是print ...这种语句形式。Python 2 已于 2020 年 1 月 1 日停止维护在 Python 3 里打印 必须写成函数调用print(...)否则是语法错误。四、把脚本和模块两种角色合到一起一个文件常常既想能被import当工具模块用又想能直接运行当脚本用。 标准做法就是把直接运行时要做的事全部收进一个main()函数再用守卫调用。# 适用于 Python 3.8# 文件 calc.pyimport argparsedef add(a, b):return a bdef parse_args(argvNone):parser argparse.ArgumentParser()parser.add_argument(a, typeint)parser.add_argument(b, typeint)return parser.parse_args(argv)def main(argvNone):args parse_args(argv)print(add(args.a, args.b))if __name__ __main__:main()这样python calc.py 2 3打印 5而import calc; calc.add(2, 3)得到 5 且 不会触发命令行解析。五、直接运行与 python -m 的区别运行方式__name__相对导入典型用途python calc.py__main__通常不可用独立脚本python -m pkg.calc__main__可用在包内包内可执行模块import calccalc不适用当模块使用交互式环境 /python -c__main__不可用快速试验关键差别在相对导入直接python calc.py时这个文件的包上下文是空 的写from . import util会报错用python -m pkg.calc就带着包上下文 相对导入才成立。六、顺序执行带来的副作用循环导入既然import会执行顶层代码那么两个模块互相import时就可能出现循环导入 circular importa 执行到一半去导入 bb 又要导入 a而 a 这时还只完成 了一部分于是 b 拿到的可能是半成品模块触发ImportError或AttributeError。常用的三种解法# 适用于 Python 3.8# 解法一把公共部分抽到第三个模块 common.py双方都只依赖 common# 解法二延迟导入——把 import 放进函数体用到时才执行def need_b():import b # 调用这个函数时才导入return b.something()# 解法三只导入模块、不导入具体名字用属性访问import bdef use():return b.something()延迟导入之所以有效正是利用了顶层代码按需执行这一特性把import从 模块顶层挪进函数体它就不会在模块加载时立刻执行从而打破环。七、sys.modules 缓存速查现象原因第二次 import 无输出命中sys.modules缓存改了模块文件不生效缓存的还是旧模块对象想强制重载用importlib.reload(module)模块级列表被多方修改模块对象在进程内只有一份常见坑点忘了主入口守卫导致导入即触发副作用python # ❌ 顶层直接执行一 import 就跑 # report.py print(生成报表中...) # ✅ 放进 main 并加守卫 def main(): print(生成报表中...) if __name__ __main__: main()顶层代码的顺序写错python # ❌ 调用写在定义之前运行到就 NameError greet() def greet(): print(hi) # ✅ 定义在前调用在后 def greet(): print(hi) greet()以为重复 import 会重新执行python import mod # 执行 import mod # ❌ 不会再次执行命中缓存 # ✅ 需要重新执行用 reload import importlib importlib.reload(mod)直接运行包内模块时用相对导入python # ❌ python pkg/calc.py 时没有包上下文 from . import util # ✅ 用 -m 运行或改成绝对导入 # python -m pkg.calc在顶层读取 __name__ 时想当然python # ❌ 以为 import 进来 __name__ 还是 __main__ # ✅ 被导入时 __name__ 是模块名 print(__name__) # 直接运行: __main__导入时: 模块名把可变状态挂在模块顶层python # ❌ 全进程共享谁都可能改 ITEMS [] def add(x): ITEMS.append(x) # ✅ 需要隔离就用函数参数或类实例main() 里塞太多逻辑python # ❌ main 什么都干没法测试 def main(): # 解析参数、连库、算、打印全混在一起 ... # ✅ main 只做编排细节拆成可测的小函数 def main(argvNone): args parse_args(argv) print(add(args.a, args.b))总结关注点结论执行顺序顶层语句从上到下逐条执行函数 / 类执行到def/class那行才被创建import执行目标模块顶层代码一次结果进sys.modules直接运行__name__ __main__被导入__name__等于模块名入口写法main()if __name__ __main__:记住两条顶层代码会被 import 执行以及__name__区分脚本与模块。 把动作收进main()、再用守卫调用就能让同一个文件既能当脚本又能当模块 还不留副作用。