Python的秘密生活:导入系统
Source: Dev.to
Python 如何找到你的代码(以及为什么有时会找不到)
Timothy 懒散地靠在椅子上,盯着终端,带着那种调试同一个问题三小时后的疲惫与沮丧。
“我不明白,”他喃喃道,已经第百次运行脚本了。
“我把文件删掉了。我把old_utils.py从项目目录里物理删除了。我甚至重启了终端。但当我运行代码时…”import utils print(utils.get_version()) # Output: "Version 1.0 - DEPRECATED"“它仍然在加载旧版本!这个文件根本不存在了!”
Margaret 走了过来,脸上带着会心的笑容。
“你的 Python 并没有被闹鬼,它只是记性很好。你在和导入系统作斗争——而且因为不懂规则而输掉了。”
缓存:sys.modules
当你输入 import utils 时,Python 首先做什么?
“它会在我的目录里找
utils.py吗?”
Python 很懒——只要能省事就不做。它在搜索文件系统之前,会先检查一个叫 sys.modules 的字典。
import sys
print(type(sys.modules)) # <class 'dict'>
print(len(sys.modules)) # e.g., 347 (varies)
# Show a few entries
for name in list(sys.modules.keys())[:10]:
print(name)
# sys, builtins, _frozen_importlib, _imp, _thread, ...
sys.modules 将模块名映射到模块对象。如果 'utils' 已经是一个键,Python 会立即返回那个缓存的对象——根本不触碰文件系统。
import sys
import json
print('json' in sys.modules) # True
print(sys.modules['json']) # <module 'json' from '.../json/__init__.py'>
print(json.dumps({'test': 'data'})) # {"test": "data"}
print(dir(sys.modules['json'])[:5]) # ['JSONDecodeError', 'JSONDecoder', ...]
因为缓存会在进程生命周期内一直存在,模块源码的更改在解释器重新启动或缓存被刷新之前是不可见的。
问题演示
# Create a simple module file
with open('example.py', 'w') as f:
f.write('''
def greet():
return "Hello, version 1"
''')
import example
print(example.greet()) # Hello, version 1
# Modify the file
with open('example.py', 'w') as f:
f.write('''
def greet():
return "Hello, version 2"
''')
# Re‑import (still cached)
import example
print(example.greet()) # Hello, version 1
print('example' in sys.modules) # True
重新加载模块
危险做法:从 sys.modules 删除 ❌
import sys
del sys.modules['example'] # Remove cached entry
import example # Reloads from disk
print(example.greet()) # Hello, version 2
为什么危险:程序的其他部分可能仍然持有旧模块对象的引用,导致行为不一致。
import sys
if 'example' in sys.modules:
del sys.modules['example']
# Write version 1
with open('example.py', 'w') as f:
f.write('def greet(): return "Version 1"')
import example as ex1
from example import greet as greet1
# Replace file with version 2 and delete cache
del sys.modules['example']
with open('example.py', 'w') as f:
f.write('def greet(): return "Version 2"')
import example as ex2
from example import greet as greet2
print(greet1()) # Version 1 (old reference)
print(greet2()) # Version 2
print(ex1 is ex2) # False
正确做法:importlib.reload() ✅
import importlib
import example
# Edit the file to version 2
with open('example.py', 'w') as f:
f.write('def greet(): return "Version 2"')
importlib.reload(example) # Updates the existing module object
print(example.greet()) # Version 2
print(id(example)) # Same memory address as before
reload() 会就地更新模块,所以已有的引用会看到新代码。不过,在重新加载之前执行的 from example import greet 语句仍会指向旧的函数对象。
映射表:sys.path
如果模块尚未被缓存,Python 会使用 sys.path 在磁盘上搜索它。sys.path 是一个目录列表,定义了导入搜索顺序。
import sys
print(sys.path)
常见的条目包括:
- 用来启动 Python 解释器的脚本所在的目录。
PYTHONPATH环境变量中的条目(如果设置了)。- 标准库目录。
- site‑packages(第三方库)。
Python 会在找到的第一个匹配项就停止搜索。通过操作 sys.path(例如在最前面插入项目的 src 文件夹),可以控制导入哪个版本的模块。
import sys
sys.path.insert(0, '/path/to/my/project')
import mymodule # Now Python looks in '/path/to/my/project' first
要点总结
sys.modules是 Python 的内存缓存,保存已导入的模块;它让导入变快,但也可能导致代码陈旧。- 使用
importlib.reload()可以安全地重新加载模块。 - 当 Python 在磁盘上搜索模块时,要留意
sys.path。 - 调试导入问题时,首先检查模块是否已经被缓存。