Python导入编码样式


69

我发现了一种新模式。这种模式众所周知吗?对此有何看法?

基本上,我很难遍历源文件来确定可用的模块导入等等,所以现在,而不是

import foo
from bar.baz import quux

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

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

def myFunction():
    import foo
    from bar.baz import quux

    foo.this.that(quux)

这做了一些事情。首先,我很少偶然用其他模块的内容污染我的模块。我可以设置__all__为模块变量,但是随后我必须在模块发展时对其进行更新,这对实际存在于模块中的代码的命名空间污染没有帮助。

其次,我很少在模块顶部添加大量的导入,因为重构后,不再需要一半或更多的导入。最后,我发现此模式更容易阅读,因为每个引用的名称都在函数体内。


Answers:


123

这个问题的(以前)投票最高的答案格式正确,但是在性能方面绝对是错误的。让我示范

性能

热门进口

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

导入功能主体

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领域一致。


6
+1代表实际的字节码差异...有时,如果我要在函数中使用它们(更清晰的字节码和更清晰的代码),有时我会将一些类属性设置为方法的局部属性
lunixbochs 2011年

6
进行进口确实有不小的处罚。本地访问和函数中的循环在这里使这一点变得模糊。如果在“最重要的导入”示例中添加“ r = random”并使用r.random(),将获得与第二个相同的性能。如果添加“ r = random.random”并使用“ r()”,它将更快。
H Krishnan 2012年

1
@HKrishnan,完全正确。我想我还不够清楚。如果模块被多次访问,则导入功能会更快。您建议的速度会更快,但只有一点点,除非多次调用导入的功能。总体而言,我主张在模块级别进行导入。我可以想到的一种情况是,在最佳情况下导入函数是在正常执行程序的过程中不希望调用该函数并且该函数具有唯一的导入。Django视图将是一个很好的例子。
aaronasterling 2012年

3
如果你只是想LOAD_FAST出于性能的考虑,import random在全球层面上,然后设置random_ = random在本地范围和使用random_(或更好,但保存的一些属性查找random_ = random.randomfrom random import random)。在仅使用的每个函数调用上都消耗导入的性能影响LOAD_FAST是一个坏主意。
user2357112支持Monica17年

56

这确实有一些缺点。

测验

如果您想通过运行时修改来测试模块,这可能会增加难度。而不是做

import mymodule
mymodule.othermodule = module_stub

你必须要做

import othermodule
othermodule.foo = foo_stub

这意味着您必须全局修补其他模块,而不是仅更改mymodule中的引用所指向的对象。

依赖追踪

这使得模块所依赖的模块变得不明显。如果您使用许多第三方库或正在重新组织代码,这将特别令人讨厌。

我必须维护一些遗留代码,这些代码在各处都使用内联导入,这使得代码极难重构或重新打包。

性能说明

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

热门进口

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

导入功能主体

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

3
您可能需要澄清这一点-每次都会检查导入,但是模块只会加载一次。
S.Lott

1
感谢您的输入。即使模块缓存,它仍然不会有很大的性能影响,因为你可以从我的测试中看到的。
Ryan

是的,但是现在您已经清楚了。这是非常误导的。删除了我的否定表决
09年

2
这不是一个很好的例子,因为您将导入放置在for循环,而不是仅放置在f()的定义内。但是,是的,总的来说,本地进口确实有成本。
davidavr

1
@瑞恩-1。如何在循环中输入import语句比在外部输入更容易?您的回答绝对不正确。请见我的
aaronasterling 2011年

23

这种方法的几个问题:

  • 打开文件所依赖的模块时,并不是立即显而易见的。
  • 这将混淆要分析的依赖程序,如py2exepy2app等。
  • 在许多功能中使用的模块呢?您要么将获得大量的冗余导入,要么必须在文件的顶部具有一些内部函数。

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

在那里我一些情况下已经发现里面进口的功能是有用的:

  • 处理循环依赖(如果您确实无法避免的话)
  • 平台特定代码

另外:将导入放入每个函数实际上并不比在文件顶部慢得多。首次加载每个模块时,会将其放入sys.modules,随后的每次导入仅花费查找模块的时间,这相当快(不会重新加载)。


+1:而且很慢。每个函数调用都必须重复导入模块检查。
S.Lott


4

我建议您尽量避免from foo import bar进口。我只在包中使用它们,将它们拆分为模块是实现细节,反正不会有很多。

在其他所有导入软件包的地方,只需使用import foo,然后使用全名进行引用即可foo.bar。这样,您始终可以知道某个元素来自何处,而不必维护已导入元素的列表(实际上,它始终会过时并且不再使用已导入的元素)。

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


3

人们已经很好地解释了为什么要避免内联导入,但实际上并没有真正替代的工作流来解决您首先需要它们的原因。

我很难在源文件中上下滑动,以找出可用的模块导入等等。

要检查未使用的导入,我使用pylint。它对Python代码进行静态分析,并且检查的许多内容之一是未使用的导入。例如,以下脚本。

import urllib
import urllib2

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

..将生成以下消息:

example.py:2 [W0611] Unused import urllib2

至于检查可用的导入,我通常依靠TextMate的(非常简单)的完成方式-当您按Esc时,它将与文档中的其他单词一起完成当前单词。如果完成了import urlliburll[Esc]将扩展为urllib,否则将跳转到文件的开头并添加导入。


3

我认为这是在某些情况/场景下的推荐方法。例如,在Google App Engine中,建议延迟加载大模块,因为这样可以最大程度地降低实例化新Python VM /解释器的预热成本。查看Google工程师对此介绍。但是请记住,这并不意味着您应该延迟加载所有模块。



2

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

性能

在几个答案中都提到了它,但我认为他们都没有完整的讨论。

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

  • 找到包。
  • 检查软件包是否已转换为字节码(著名__pycache__目录或.pyx文件),如果不是,则将其转换为字节码。
  • Python加载字节码。
  • 加载的模块放入sys.modules

随后的导入不必执行所有这些操作,因为Python可以简单地从返回模块sys.modules。因此,随后的进口将更快。

可能是模块中的某个功能实际上并不经常使用,但它依赖于import花费很长时间的功能。然后,您实际上可以import在函数内部移动。这样可以更快地导入模块(因为不必立即导入长加载包),但是最终使用该函数时,第一次调用时会很慢(因为必须导入模块)。这可能会影响感知的性能,因为您不会减慢所有用户的速度,而只会减慢使用依赖于慢速加载依赖性的功能的用户。

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

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除了在函数中使用它之外,您还可以简单地使用一个变量并将其分配给全局名称:

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对全局变量进行重复查找很慢,但是在函数内部导入模块或在函数内部的变量中分配全局模块之间几乎没有区别。

通过避免循环内的函数查找,可以将其发挥到极致:

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)

再次加快了速度,但是import和变量之间几乎没有区别。

可选依赖项

有时进行模块级导入实际上可能是一个问题。例如,如果您不想添加其他安装时依赖性,但是该模块对于某些其他功能确实很有帮助。确定依赖项是否应该是可选的,不应该轻易做,因为它将影响用户(如果他们遇到意外情况,ImportError或者错过了“出色的功能”),并且通常情况下,使具有所有功能的软件包的安装更加复杂依赖项pipconda (仅提及两个软件包管理器)即可使用,但是对于可选的依赖项,用户必须稍后手动安装软件包(有一些选项可以自定义需求,但又会给安装带来负担”正确”)。

但是同样可以通过两种方式完成此操作:

try:
    import matplotlib.pyplot as plt
except ImportError:
    pass

def function_that_requires_matplotlib():
    plt.plot()

要么:

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

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

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

循环进口

函数内导入对于避免由于循环导入而引起的ImportErrors很有帮助。在很多情况下,循环进口是“不良”包装结构的标志,但如果绝对无法避免循环进口,则可通过将导致圆形的进口放到里面来解决“圆”(从而产生问题)实际使用它的功能。

不要重复自己

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

  • 现在,您可以在多个地方检查是否已过时任何导入。
  • 万一您拼错了一些导入,您只会在运行特定功能时才能找到,而不会在加载时发现。因为您有更多的import语句,所以错误发生的可能性会增加(不多),并且测试所有功能变得更加重要。

其他想法:

我很少在模块顶部添加大量的导入文件,因为重构后,不再需要一半或更多的导入文件。

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

我很少意外地用其他模块的内容污染我的模块

这就是为什么您通常使用__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__),因为它允许他们执行(可能)一直做的事情。


1

您可能想看看python Wiki中的Import语句开销。简而言之:如果模块已经加载(请参阅参考资料sys.modules),您的代码将运行得更慢。如果您的模块尚未加载,并且foo仅在需要时加载(可以为零),那么整体性能会更好。


2
-1代码不一定会运行得更慢。看我的回答
aaronasterling 2011年

-1

安全实施

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.