Python中的局部导入语句

60 投票
7 回答
47998 浏览
提问于 2025-04-15 15:48

我觉得把导入语句放在使用它的代码片段旁边,可以让代码更容易读懂,因为这样可以更清楚地看到它依赖了哪些东西。Python会缓存这个吗?我需要在意这个吗?这样做是个坏主意吗?

def Process():
    import StringIO
    file_handle=StringIO.StringIO('hello world')
    #do more stuff

for i in xrange(10): Process()

再多说一点:这是因为有些方法使用了库里比较复杂的部分,但当我把这个方法重构到另一个文件时,我往往没意识到漏掉了外部依赖,直到出现运行时错误才发现。

7 个回答

13

请查看 PEP 8:

导入的内容总是放在文件的最上面,紧接着模块的注释和文档字符串之后,然后再是模块的全局变量和常量。

请注意,这完全是一个风格上的选择,因为无论你在源文件的哪个地方写 import 语句,Python 都会一样对待它们。不过,我还是建议你遵循这个常见的做法,这样会让其他人更容易阅读你的代码。

14

抛开风格不谈,确实一个导入的模块只会被导入一次(除非你对这个模块调用了 reload)。不过,每次调用 import Foo 时,系统会自动检查这个模块是否已经被加载过(通过查看 sys.modules)。

再考虑一下两个功能相同的函数,其中一个尝试导入模块,而另一个则不导入:

>>> def Foo():
...     import random
...     return random.randint(1,100)
... 
>>> dis.dis(Foo)
  2           0 LOAD_CONST               1 (-1)
              3 LOAD_CONST               0 (None)
              6 IMPORT_NAME              0 (random)
              9 STORE_FAST               0 (random)

  3          12 LOAD_FAST                0 (random)
             15 LOAD_ATTR                1 (randint)
             18 LOAD_CONST               2 (1)
             21 LOAD_CONST               3 (100)
             24 CALL_FUNCTION            2
             27 RETURN_VALUE        
>>> def Bar():
...     return random.randint(1,100)
... 
>>> dis.dis(Bar)
  2           0 LOAD_GLOBAL              0 (random)
              3 LOAD_ATTR                1 (randint)
              6 LOAD_CONST               1 (1)
              9 LOAD_CONST               2 (100)
             12 CALL_FUNCTION            2
             15 RETURN_VALUE        

我不太确定字节码在虚拟机中会被翻译成多少,但如果这是你程序中一个重要的循环,你肯定会更倾向于使用 Bar 的方式,而不是 Foo 的方式。

一个简单粗暴的 timeit 测试确实显示使用 Bar 的速度有了适度的提升:

$ python -m timeit -s "from a import Foo,Bar" -n 200000 "Foo()"
200000 loops, best of 3: 10.3 usec per loop
$ python -m timeit -s "from a import Foo,Bar" -n 200000 "Bar()"
200000 loops, best of 3: 6.45 usec per loop
90

其他回答对import的工作原理有些混淆。

这条语句:

import foo

大致上等同于这条语句:

foo = __import__('foo', globals(), locals(), [], -1)

也就是说,它在当前的作用域中创建了一个与请求的模块同名的变量,并将调用__import__()函数的结果赋值给它,这个函数使用了模块名和一堆默认参数。

__import__()这个函数的作用是把一个字符串(比如'foo')转换成一个模块对象。模块会被缓存到sys.modules中,而__import__()首先会在这个地方查找——如果sys.modules里有'foo'的条目,那么__import__('foo')就会返回那个条目,不管它是什么。它其实并不关心类型。你可以自己试试,运行以下代码:

import sys
sys.modules['boop'] = (1, 2, 3)
import boop
print boop

暂时不考虑风格问题,在函数内部使用导入语句是按你想要的方式工作的。如果这个模块之前从未被导入过,它会被导入并缓存到sys.modules中。然后,它会把这个模块赋值给那个名字的局部变量。它绝对不会修改任何模块级的状态。它可能会修改一些全局状态(比如在sys.modules中添加一个新条目)。

不过,我几乎从不在函数内部使用import。如果导入模块会明显拖慢你的程序——比如它在静态初始化时进行长时间的计算,或者它是一个非常大的模块——而你的程序很少需要用到这个模块,那么只在需要的函数内部导入是完全可以的。(如果这样做不合适,Guido肯定会跳进他的时间机器,修改Python来阻止我们这么做。)但一般来说,我和大多数Python社区的人都把所有的导入语句放在模块的顶部,也就是模块作用域中。

撰写回答