9.1 创建模块的循环依赖
假设你有两个文件,a.py
和 b.py
,每个文件都按以下方式导入另一个:
在 a.py
中:
import b
def f():
return b.x
print(f())
在 b.py
中:
import a
x = 1
def g():
print(a.f())
首先尝试导入 a.py
:
import a
# 1
成功了!这可能让你感到惊讶。毕竟,模块是循环导入的,这可能会变成个问题,不是吗?
答案是,模块的循环导入本身在Python中并不是个问题。如果一个模块已经被导入过,Python足够聪明,不会尝试重新导入它。然而,具体问题会根据每个模块尝试访问其他模块中的函数或变量的时间而有所不同。
回到我们的例子,当我们导入 a.py
时,它在导入 b.py
的时候没问题,因为 b.py
不需要在导入时从 a.py
中获得任何东西。在 b.py
中唯一对 a
的引用是调用 a.f()
。但这个调用在 g()
中,而在 a.py
或 b.py
中都没有调用 g()
。所以一切运行得很好。
但是如果我们试图导入 b.py
(而不是先导入 a.py
),会发生什么呢:
import b
Traceback (most recent call last):
File "<stdin>", line 1, in
File "b.py", line 1, in
import a
File "a.py", line 6, in
print(f())
File "a.py", line 4, in f
return b.x
AttributeError: 'module' object has no attribute 'x'
这里的问题是,在导入 b.py
的过程中,它尝试导入 a.py
,而后者又调用 f()
,而 f()
尝试访问 b.x
。但 b.x
尚未被定义。因此抛出 AttributeError
异常。
至少有一个解决此问题的方法相当简单。只需更改 b.py
以便在 g()
中导入 a.py
:
x = 1
def g():
import a # 只有在调用 g() 时才评估
print(a.f())
现在,当我们导入它时,一切正常:
import b
b.g()
# 1 首次打印,因为模块 'a' 在末尾调用 'print(f())'
# 1 第二次打印,这是我们对 'g()' 的调用
9.2 与Python标准库模块名冲突
Python的一个优点是它自带了很多模块。但结果就是,如果你不小心,你的模块名可能会和Python标准库中的模块名冲突(例如,你的代码中可能有一个名为 email.py
的模块,它和标准库中的同名模块冲突)。
这可能导致严重的问题。例如,如果某个模块尝试导入Python标准库中的模块,而你的项目中有同名模块,它可能错误地导入你的模块而不是标准库中的模块。
因此,应小心避免使用与Python标准库模块相同的名称。更改自己的模块名称要比请求更改标准库模块名称并等待批准简单得多。
9.3 异常的可见性
考虑以下文件 main.py
:
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def bad():
e = None
try:
bar(int("1"))
except KeyError as e:
print('key error')
except ValueError as e:
print('value error')
print(e)
bad()
看起来不错,代码应该能正常工作,让我们看看它输出什么:
$ python main.py 1
Traceback (most recent call last):
File "C:\Projects\Python\TinderBolt\main.py", line 19, in <module>
bad()
File "C:\Projects\Python\TinderBolt\main.py", line 17, in bad
print(e)
^
UnboundLocalError: cannot access local variable 'e' where it is not associated with a value
刚刚发生了什么?问题在于,在Python中,异常块内的对象在块外不可访问。(这样做的原因是否则这些对象将保留在内存中,直到垃圾回收器运行并删除对它们的引用)。
一种避免此问题的方法是,在异常块之外保存对异常块对象的引用,以使其保持可访问。以下是使用此技术的前一个示例的版本,从而使代码可用:
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def good():
exception = None
try:
bar(int("1"))
except KeyError as e:
exception = e
print('key error')
except ValueError as e:
exception = e
print('value error')
print(exception)
good()
9.4 错误使用 __del__
方法
当解释器删除对象时,它会检查对象是否有 __del__
方法,如果有,则在删除对象前调用它。当你希望你的对象清理一些外部资源或缓存时,这非常方便。
假设你有一个文件 mod.py
:
import foo
class Bar(object):
...
def __del__(self):
foo.cleanup(self.myhandle)
你尝试从另一个文件 another_mod.py
执行以下操作:
import mod
mybar = mod.Bar()
结果是一个可怕的 AttributeError
。
为什么?因为正如这里所述,当解释器终止时,模块的所有全局变量的值都是 None
。因此,在上面的例子中,当调用 __del__
时,foo
名称已经设置为 None
。
解决这个“星号问题”的方法是使用特殊方法 atexit.register()
。这样,当你的程序终止时(即正常退出时),在解释器终止之前,你的 handle
就会被删除。
考虑到这一点,上面 mod.py
代码的修正可能如下所示:
import foo
import atexit
def cleanup(handle):
foo.cleanup(handle)
class Bar(object):
def __init__(self):
...
atexit.register(cleanup, self.myhandle)
这种实现提供了一种简单可靠的方法来在程序正常终止后进行任何必要的清理。显然,决定如何处理与 self.myhandle
关联的对象是 foo.cleanup
的事,但我想你明白了这个想法。
GO TO FULL VERSION