Python导入编码风格

Python import coding style

我发现了一种新的模式。 这种模式是众所周知的还是对它的看法是什么?

基本上,我很难刷新源文件以找出可用的模块导入等等,所以现在,而不是

1
2
3
4
5
import foo
from bar.baz import quux

def myFunction():
    foo.this.that(quux)

我将所有导入移动到它们实际使用的函数中,如下所示:

1
2
3
4
5
def myFunction():
    import foo
    from bar.baz import quux

    foo.this.that(quux)

这做了一些事情。 首先,我很少意外地用其他模块的内容污染我的模块。 我可以为模块设置__all__变量,但随后我必须在模块发展时更新它,这无助于实际存在于模块中的代码的命名空间污染。

其次,我很少在我的模块顶部进行一连串的进口,其中一半或更多我不再需要,因为我已经重构了它。 最后,我发现这个模式更容易阅读,因为每个引用的名称都在函数体中。


这个问题的(先前)最高投票答案格式很好,但性能绝对错误。让我来证明一下

性能

顶级导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import random

def f():
    L = []
    for i in xrange(1000):
        L.append(random.random())


for i in xrange(1000):
    f()

$ time python import.py

real        0m0.721s
user        0m0.412s
sys         0m0.020s

在函数体中导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def f():
    import random
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(1000):
    f()

$ time python import2.py

real        0m0.661s
user        0m0.404s
sys         0m0.008s

如您所见,在函数中导入模块可能更有效。这样做的原因是简单的。它将引用从全局引用移动到本地引用。这意味着,至少对于CPython,编译器将发出LOAD_FAST指令而不是LOAD_GLOBAL指令。顾名思义,这些更快。另一个回答者通过在循环的每个迭代中导入来人为地夸大了查看sys.modules的性能。

通常,最好在顶部导入,但如果您多次访问模块,性能不是原因。原因是人们可以更容易地跟踪模块所依赖的内容,并且这样做与Python Universe的其余部分一致。


这确实有一些缺点。

测试

如果您想通过运行时修改来测试模块,可能会使其变得更加困难。而不是做

1
2
import mymodule
mymodule.othermodule = module_stub

你必须这样做

1
2
import othermodule
othermodule.foo = foo_stub

这意味着您必须全局修补othermodule,而不是仅仅更改mymodule中的引用指向的内容。

依赖性跟踪

这使得模块所依赖的模块不明显。如果您使用许多第三方库或重新组织代码,这尤其令人恼火。

我不得不维护一些遗留代码,这些代码在整个地方使用了内联导入,这使得代码极难重构或重新打包。

关于表现的说明

由于python缓存模块的方式,没有性能损失。实际上,由于模块位于本地名称空间中,因此在函数中导入模块会有一些性能上的好处。

顶级导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import random

def f():
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(10000):
    f()


$ time python test.py

real   0m1.569s
user   0m1.560s
sys    0m0.010s

在函数体中导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def f():
    import random
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(10000):
    f()

$ time python test2.py

real    0m1.385s
user    0m1.380s
sys     0m0.000s


这种方法存在一些问题:

  • 打开文件所依赖的模块并不是很明显。
  • 它会混淆必须分析依赖关系的程序,例如py2exepy2app等。
  • 您在许多功能中使用的模块怎么样?您最终会得到大量的冗余导入,或者您必须在文件的顶部和一些内部函数中放置一些。

所以...首选的方法是将所有导入放在文件的顶部。我发现如果我的导入很难跟踪,通常意味着我有太多的代码,我最好把它分成两个或更多的文件。

在某些我发现函数内部导入有用的情况:

  • 处理循环依赖(如果你真的无法避免它们)
  • 平台特定代码

另外:在每个函数中放入导入实际上并不比文件顶部慢。第一次加载每个模块时,它被放入sys.modules,每个后续导入只花费查找模块的时间,这相当快(不重新加载)。


另一个需要注意的有用的事情是在Python 3.0中删除了函数内部的from module import *语法。

这里简单提一下"删除的语法":

http://docs.python.org/3.0/whatsnew/3.0.html


我建议你尽量避免from foo import bar导入。我只在包内使用它们,其中拆分模块是一个实现细节,无论如何都不会有很多。

在导入包的所有其他位置,只需使用import foo,然后使用全名foo.bar引用它。这样,您始终可以告诉某个元素来自何处,而不必维护导入元素的列表(实际上,这将永远过时并导入不再使用的元素)。

如果foo是一个非常长的名称,您可以使用import foo as f简化它,然后写f.bar。这比维护所有from导入更加方便和明确。


人们已经很好地解释了为什么要避免内联导入,而不是真正的替代工作流来解决你想要它们的原因。

I have a hard time scrubbing up and down source files to figure out what module imports are available and so forth

要检查未使用的导入,我使用pylint。它执行静态(ish) - Python代码分析,它检查的(很多)事情之一是未使用的导入。例如,以下脚本..

1
2
3
4
import urllib
import urllib2

urllib.urlopen("http://stackoverflow.com")

..将生成以下消息:

1
example.py:2 [W0611] Unused import urllib2

至于检查可用的导入,我通常依赖于TextMate(相当简单)的完成 - 当你按Esc时,它会在文档中与其他人完成当前的单词。如果我已经完成import urlliburll[Esc]将扩展为urllib,如果不是,我跳转到文件的开头并添加导入。


两种变体都有其用途。但是在大多数情况下,最好在函数之外导入,而不是在函数内部导入。

性能

在几个答案中已经提到过,但在我看来,他们都缺乏完整的讨论。

第一次在python解释器中导入模块时,无论它是在顶级还是在函数内部,它都会很慢。它很慢,因为Python(我专注于CPython,它可能与其他Python实现不同)做了多个步骤:

  • 找到包裹。
  • 检查包是否已经转换为字节码(着名的__pycache__目录或.pyx文件),如果没有,则将它们转换为字节码。
  • Python加载字节码。
  • 加载的模块放在sys.modules中。
  • 后续导入不必执行所有这些操作,因为Python只能从sys.modules返回模块。因此后续进口将更快。

    可能是模块中的函数实际上并未经常使用,但它取决于import需要很长时间。然后你可以实际移动函数内的import。这将使您的模块更快地导入(因为它不必立即导入长加载包)但是当最终使用该函数时,它在第一次调用时会很慢(因为那时必须导入模块)。这可能会对感知性能产生影响,因为不会减慢所有用户的速度,而只会减慢那些使用依赖于慢速加载依赖性的函数的速度。

    但是sys.modules中的查找不是免费的。它速度非常快,但并不是免费的。因此,如果您实际上经常调用import s包的函数,您会发现性能略有下降:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import random
    import itertools

    def func_1():
        return random.random()

    def func_2():
        import random
        return random.random()

    def loopy(func, repeats):
        for _ in itertools.repeat(None, repeats):
            func()

    %timeit loopy(func_1, 10000)
    # 1.14 ms ± 20.6 μs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    %timeit loopy(func_2, 10000)
    # 2.21 ms ± 138 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)

    这几乎慢了两倍。

    非常重要的是要意识到aaronasterling在答案中"作弊"了一下。他表示,在函数中进行导入实际上会使函数更快。在某种程度上,这是事实。那是因为Python如何查找名称:

  • 它首先检查本地范围。
  • 它接下来检查周围的范围。
  • 然后检查下一个周围范围
  • ...
  • 检查全局范围。
  • 因此,不是检查本地范围然后检查全局范围,而是检查本地范围,因为模块的名称在本地范围内可用。这实际上使它更快!但这是一种称为"循环不变代码运动"的技术。它基本上意味着通过在循环(或重复调用)之前将其存储在变量中来减少循环(或重复)中完成的事情的开销。因此,除了在函数中import之外,您还可以简单地使用变量并将其分配给全局名称:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import random
    import itertools

    def f1(repeats):
       "Repeated global lookup"
        for _ in itertools.repeat(None, repeats):
            random.random()

    def f2(repeats):
       "Import once then repeated local lookup"
        import random
        for _ in itertools.repeat(None, repeats):
            random.random()

    def f3(repeats):
       "Assign once then repeated local lookup"
        local_random = random
        for _ in itertools.repeat(None, repeats):
            local_random.random()

    %timeit f1(10000)
    # 588 μs ± 3.92 μs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    %timeit f2(10000)
    # 522 μs ± 1.95 μs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    %timeit f3(10000)
    # 527 μs ± 4.51 μs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

    虽然您可以清楚地看到对全局random执行重复查找的速度很慢,但在函数内部导入模块或在函数内部的变量中分配全局模块之间几乎没有区别。

    这可以通过避免循环内的函数查找来达到极限:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    def f4(repeats):
        from random import random
        for _ in itertools.repeat(None, repeats):
            random()

    def f5(repeats):
        r = random.random
        for _ in itertools.repeat(None, repeats):
            r()

    %timeit f4(10000)
    # 364 μs ± 9.34 μs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    %timeit f5(10000)
    # 357 μs ± 2.73 μs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

    再快得多,但导入和变量之间几乎没有差别。

    可选的依赖项

    有时进行模块级导入实际上可能是个问题。例如,如果您不想添加另一个安装时依赖项,但该模块对某些其他功能非常有帮助。决定依赖项是否应该是可选的不应该轻易完成,因为它会影响用户(如果他们得到意外的ImportError或者错过了"酷炫的功能"),它会使安装包含所有功能的包更复杂,对于普通依赖项pipconda(仅提及两个软件包管理器)开箱即用,但对于可选的依赖项,用户必须稍后手动安装软件包(有一些选项可以自定义)要求,但再次"正确"安装它的负担被放在用户身上)。

    但是,这可以通过两种方式完成:

    1
    2
    3
    4
    5
    6
    7
    try:
        import matplotlib.pyplot as plt
    except ImportError:
        pass

    def function_that_requires_matplotlib():
        plt.plot()

    要么:

    1
    2
    3
    def function_that_requires_matplotlib():
        import matplotlib.pyplot as plt
        plt.plot()

    通过提供替代实现或自定义用户看到的异常(或消息),可以更加自定义,但这是主要要点。

    如果想要为可选依赖项提供替代"解决方案",那么顶级方法可能会更好一些,但通常人们使用函数内导入。主要是因为它导致更清晰的堆栈跟踪并且更短。

    循环进口

    函数内导入可以非常有助于避免由于循环导入导致的ImportErrors。在很多情况下,圆形进口是"坏"包装结构的标志,但如果绝对没有办法避免循环导入,则通过将导入圆圈的进口放入"循环"(以及问题)来解决实际使用它的功能。

    不要重复自己

    如果您实际将所有导入放在函数而不是模块范围中,则会引入冗余,因为函数可能需要相同的导入。这有一些缺点:

  • 您现在有多个地方可以检查是否有任何导入已过时。
  • 如果你错误地导入了一些导入,你只能在运行特定功能时找到,而不是在加载时。因为你有更多的import语句错误的可能性增加(不多),它只是变得更加重要,以测试所有功能。
  • 其他想法:

    I rarely end up with a litany of imports at the top of my modules, half or more of which I no longer need because I've refactored it.

    Ok.

    大多数IDE已经有一个未使用导入的检查器,因此可能只需点击几下就可以删除它们。即使您不使用IDE,也可以偶尔使用静态代码检查器脚本并手动修复它。另一个答案提到了pylint,但还有其他答案(例如pyflakes)。

    I rarely accidentally pollute my modules with the contents of other modules

    Ok.

    这就是为什么你通常使用__all__和/或定义你的函数子模块,只导入主模块中的相关类/函数/ ...,例如__init__.py

    此外,如果您认为您过多地污染了模块名称空间,那么您可能应该考虑将模块拆分为子模块,但这只对几十个导入有意义。

    如果你想减少命名空间污染,另外一个(非常重要的)要点是避免from module import *导入。但您可能还想避免导入太多名称的from module import a, b, c, d, e, ...导入,只需导入模块并使用module.c访问这些函数。

    作为最后的手段,您始终可以使用别名来避免使用"import random as _random"通过"公共"导入来污染命名空间。这将使代码更难理解,但它非常清楚应该公开显示什么,什么不应该。这不是我推荐的,你应该保持__all__列表是最新的(这是推荐和明智的方法)。

    摘要

  • 性能影响是可见的,但几乎总是微观优化,所以不要让决定在哪里进行微观基准测试。除非第一个import上的依赖关系真的很慢,并且它仅用于功能的一小部分。然后,对于大多数用户来说,它实际上可以对模块的感知性能产生明显的影响。

  • 使用通常理解的工具来定义公共API,我的意思是__all__变量。保持最新状态可能有点烦人,但是检查过时导入的所有功能或添加新功能以添加该功能中的所有相关导入也是如此。从长远来看,您可能需要通过更新__all__来减少工作量。

  • 你更喜欢哪一个并不重要,两者都有效。如果你是独自工作,你可以推断出利弊,做一个你认为最好的。但是,如果你在一个团队中工作,你可能应该坚持使用已知模式(这将是__all__的顶级导入),因为它允许他们做(他们可能)总是做的事情。

  • 好。


    您可能想要查看python wiki中的Import语句开销。简而言之:如果模块已经加载(查看sys.modules),您的代码将运行得更慢。如果您的模块尚未加载,并且foo只会在需要时加载,这可能是零次,那么整体性能会更好。


    我相信在某些情况/场景中这是一种推荐的方法。例如,在Google App Engine中建议使用延迟加载大模块,因为它可以最大限度地降低实例化新Python VM /解释器的预热成本。看一下Google Engineer的演示文稿,描述这一点。但请记住,这并不意味着您应该延迟加载所有模块。


    从性能的角度来看,您可以看到:Python import语句是否始终位于模块的顶部?

    一般来说,我只使用本地导入来打破依赖循环。


    安全实施

    考虑一个环境,其中所有Python代码都位于只有特权用户才能访问的文件夹中。为了避免以特权用户身份运行整个程序,您决定在执行期间将权限删除给非特权用户。一旦您使用导入另一个模块的函数,您的程序将抛出ImportError,因为由于文件权限,非特权用户无法导入模块。