代码之家  ›  专栏  ›  技术社区  ›  TokenMacGuy

python导入编码样式

  •  59
  • TokenMacGuy  · 技术社区  · 16 年前

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

    基本上,我很难清理上下源文件,以确定哪些模块导入是可用的等等,所以现在,代替

    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__ 模块的变量,但是随着模块的发展,我必须更新它,这无助于实际存在于模块中的代码的命名空间污染。

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

    11 回复  |  直到 7 年前
        1
  •  104
  •   Community CDub    8 年前

    (先前的) top-voted answer 这个问题的格式很好,但在性能方面绝对错误。让我示范一下

    性能

    最高进口

    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世界的大多数其他部分是一致的。

        2
  •  53
  •   Ryan    14 年前

    这确实有一些缺点。

    测试

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

    import mymodule
    mymodule.othermodule = module_stub
    

    你必须这么做

    import othermodule
    othermodule.foo = foo_stub
    

    这意味着您必须全局地修补othermodule,而不是仅仅更改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
  •  19
  •   dF.    16 年前

    这种方法有几个问题:

    • 当打开它所依赖的模块时,并不是很明显。
    • 它会混淆必须分析依赖关系的程序,例如 py2exe , py2app 等。
    • 你在许多函数中使用的模块呢?您要么会得到大量的冗余导入,要么必须在文件的顶部有一些导入和一些内部函数。

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

    有些情况下我 在函数内部找到了有用的导入:

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

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

        4
  •  10
  •   lnafziger    13 年前

    另一个值得注意的是 from module import * 在Python3.0中,函数内部的语法已被删除。

    在“删除的语法”下面有一个简短的介绍:

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

        5
  •  4
  •   nikow    16 年前

    我建议你尽量避免 from foo import bar 进口。我只在包中使用它们,在包中分解成模块是一个实现细节,无论如何也不会有很多模块。

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

    如果 foo 是一个很长的名字,你可以用 import foo as f 然后写 f.bar . 这仍然比维护所有 from 进口。

        6
  •  3
  •   dbr    16 年前

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

    我很难清理上下源文件,以确定哪些模块导入是可用的等等

    检查我使用的未使用的导入 pylint . 它对python代码进行静态(ish)分析,它检查的(许多)事情之一是未使用的导入。例如,下面的脚本..

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

    …将生成以下消息:

    example.py:2 [W0611] Unused import urllib2
    

    至于检查可用的导入,我通常依赖textmate(相当简单)的完成-当您按esc键时,它将与文档中的其他单词一起完成当前单词。如果我做了 import urllib , urll[Esc] 将扩展到 urllib ,如果不是,则跳到文件的开头并添加导入。

        7
  •  2
  •   Community CDub    8 年前

    从性能的角度来看,您可以看到: Should Python import statements always be at the top of a module?

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

        8
  •  2
  •   RSabet    16 年前

    你可能想看看进口 statement overhead 在python wiki中。简而言之:如果模块已经加载(请看 sys.modules )您的代码将运行得更慢。如果您的模块尚未加载,并且将 foo 只会在需要的时候加载,可以是0倍,那么整体性能会更好。

        9
  •  2
  •   Marek Grzenkowicz    13 年前

    我相信在某些情况下,这是一种推荐的方法。例如,在google app engine中,建议延迟加载大模块,因为这将最小化实例化新python vms/解释器的预热成本。看一看 Google Engineer's 描述这一点的陈述。但是请记住 意味着你应该延迟加载所有模块。

        10
  •  0
  •   MSeifert    7 年前

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

    性能

    有好几个答案都提到了这一点,但在我看来,它们都缺乏一个完整的讨论。

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

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

    后续导入不必执行所有这些操作,因为python可以简单地从 系统模块 . 因此,随后的进口将快得多。

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

    但是 系统模块 不是免费的。很快,但不是免费的。所以如果你真的调用一个 进口 您经常会注意到一个包的性能有点下降:

    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 "cheated" a bit in the answer . 他说在函数中进行导入实际上使函数更快。在某种程度上这是真的。这是因为python查找名称的方式:

    • 它首先检查本地范围。
    • 接下来检查周围的范围。
    • 然后检查下一个周围的范围
    • 检查全局范围。

    因此,与其检查本地作用域,然后检查全局作用域,不如检查本地作用域,因为模块的名称在本地作用域中可用。这实际上使它更快!但这是一种叫做 "Loop-invariant code motion" . 这基本上意味着,通过在循环(或重复调用)之前将其存储在变量中,可以减少在循环(或重复调用)中执行的操作的开销。所以不是 进口 在函数中使用它,您也可以简单地使用一个变量并将其分配给全局名称:

    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)
    

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

    可选依赖项

    有时模块级导入实际上可能是个问题。例如,如果您不想添加另一个安装时依赖项,但该模块对某些 额外的 功能。决定依赖项是否应该是可选的不应该很容易,因为它会影响用户(如果他们得到一个意外的 ImportError 或者忽略了“酷特性”),这使得安装包含所有特性的包变得更加复杂,对于正常的依赖关系来说 pip conda (仅提及两个包管理器)可以开箱即用,但对于可选的依赖项,用户必须在以后手动安装包(有一些选项使自定义需求成为可能,但随后“正确”安装它的负担又落到了用户头上)。

    但这同样可以通过两种方式来实现:

    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()
    

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

    如果希望为可选依赖项提供另一种“解决方案”,顶级方法可能会更好一些,但是通常人们使用in函数导入。主要是因为它导致一个更干净的堆栈跟踪和更短。

    循环进口

    函数内导入对于避免循环导入导致的导入非常有用。在许多情况下,循环导入是“坏”包结构的标志,但如果绝对无法避免循环导入,则通过将导致循环的导入放入实际使用它的函数中来解决“循环”(从而解决问题)。

    别再说了

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

    • 现在有多个地方可以检查是否有任何导入已过时。
    • 如果你把一些导入的东西放错了,你只会在运行特定的函数时发现,而不是在加载时发现。因为有更多的import语句,所以出错的可能性会增加(不太多),测试所有函数就变得更为必要了。

    其他想法:

    我很少在模块的顶部出现一系列的导入,其中一半或更多我不再需要,因为我已经重构了它。

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

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

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

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

    如果您想减少名称空间污染,还有一点(非常重要)要提,那就是避免 from module import * 进口。但你也可以避免 from module import a, b, c, d, e, ... 进口,进口 太多 命名并导入模块,然后使用 module.c .

    最后,您可以始终使用别名,以避免使用“public”导入污染命名空间,方法是: import random as _random . 这将使代码更难理解,但它非常清楚什么应该公开,什么不应该公开。 阿尔法 列出最新的(这是建议和合理的方法)。

    总结

    • 性能影响是可见的,但几乎总是微观优化的,所以不要让您决定将导入放在哪里由微观基准来指导。除非依赖关系在第一个 进口 它只用于功能的一小部分。然后它实际上会对大多数用户感知到的模块性能产生可见的影响。

    • 使用常见的工具来定义公共api,我的意思是 阿尔法 变量。保持它的最新状态可能有点烦人,但是检查所有函数是否有过时的导入,或者添加新函数以添加该函数中的所有相关导入时也是如此。从长远来看,您可能需要通过更新 阿尔法 .

    • 你喜欢哪一个并不重要,两个都有用。如果你是一个人工作,你可以考虑利弊,做你认为最好的一个。但是,如果您在一个团队中工作,您可能应该坚持使用已知的模式(这将是 阿尔法 )因为它允许他们做他们(可能)一直做的事情。

        11
  •  -1
  •   1' OR 1 --    9 年前

    安全实现

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