我试图找到一个全面的指南,是否最好使用导入模块或从模块导入。我刚刚开始学习Python,我正试图从最佳实践开始。

基本上,我希望任何人都能分享他们的经验,其他开发者有什么偏好,以及避免任何陷阱的最佳方法是什么?


当前回答

@ahfx已经提到了这些导入的一个关键方面,即加载模块过程的内部。如果你的系统需要使用循环导入(例如,你想在一些流行的http框架中使用依赖注入),就会弹出这个窗口。在这种情况下,from {module} import {function}对于加载过程如何进行的要求显得更加激进。让我们举个例子:

#m1.py:
print('--start-m1--')
from m2 import *    # form does not matter; just need to force import of m2
print('--mid-m1--')

def do1(x):
    print(x)

print('--end-m1--')

进口

#m2.py
print('--start-m2--')

# from m1 import *      # A
# from m1 import do1    # B
# import m1             # C
                        # D -- no import of "do1" at all
                        
print('--mid-m2--')

def do2(x):
    m1.do1(x)

print('--end-m2--')

通过运行

#main.py:
from m1 import do1
do1('ok')

在m2.py (A,B,C,D)中的所有导入选项中,from {module} import {function}是唯一会导致加载过程崩溃的选项,导致臭名昭著的(CPython 3.10.6)

ImportError: cannot import name 'do1' from partially initialized module 'm1' 
(most likely due to a circular import)

虽然我不能说为什么会发生这种情况,但似乎从……进口…语句对有问题的模块已经处于初始化过程的“多远”提出了更严格的要求。

其他回答

这是我当前目录的目录结构:

. └─a └─b └─c

The import statement remembers all intermediate names. These names have to be qualified: In[1]: import a.b.c In[2]: a Out[2]: <module 'a' (namespace)> In[3]: a.b Out[3]: <module 'a.b' (namespace)> In[4]: a.b.c Out[4]: <module 'a.b.c' (namespace)> The from ... import ... statement remembers only the imported name. This name must not be qualified: In[1]: from a.b import c In[2]: a NameError: name 'a' is not defined In[2]: a.b NameError: name 'a' is not defined In[3]: a.b.c NameError: name 'a' is not defined In[4]: c Out[4]: <module 'a.b.c' (namespace)>


注意:当然,我在第1步和第2步之间重新启动了Python控制台。

我正在回答一个类似的问题,但在我发布之前,发帖者删除了它。这里有一个例子来说明这些区别。

Python库可以有一个或多个文件(模块)。为例子,

package1
  |-- __init__.py

or

package2
  |-- __init__.py
  |-- module1.py
  |-- module2.py

我们可以在任何基于设计需求的文件中定义python函数或类。

让我们来定义

在mylibrary1下的__init__.py中的Func1 () mylibrary2下的module2.py中的Foo()。

我们可以使用这些方法之一访问func1()

import package1

package1.func1()

or

import package1 as my

my.func1()

or

from package1 import func1

func1()

or

from package1 import *

func1()

我们可以使用以下方法之一来访问foo():

import package2.module2

package2.module2.foo()

or

import package2.module2 as mod2

mod2.foo()

or

from package2 import module2

module2.foo()

or

from package2 import module2 as mod2

mod2.foo()

or

from package2.module2 import *

foo()

有很多答案,但没有一个提到测试(使用unittest或pytest)。

博士tl;

对外部模块使用import foo来简化测试。

艰难的方式

从模块中单独导入类/函数(从foo import bar中)会使红绿重构周期变得冗长乏味。例如,如果我的文件看起来像

# my_module.py

from foo import bar


class Thing:
    def do_thing(self):
        bar('do a thing')

我的测试是

# test_my_module.py

from unittest.mock import patch
import my_module


patch.object(my_module, 'bar')
def test_do_thing(mock_bar):
    my_module.Thing().do_thing()
    mock_bar.assert_called_with('do a thing')

乍一看,这似乎很棒。但是如果我想在不同的文件中实现Thing类会发生什么呢?我的结构将不得不像这样改变……

# my_module.py

from tools import Thing


def do_thing():
    Thing().do_thing()


# tools.py

from foo import bar


class Thing:
    def do_thing(self):
        bar('do a thing')


# test_my_module.py

from unittest.mock import patch
import my_module
import tools  # Had to import implementation file...


patch.object(tools, 'bar')  # Changed patch
def test_do_thing(mock_bar):
    my_module.do_thing()  # Changed test (expected)
    mock_bar.assert_called_with('do a thing')

不幸的是,由于我使用from foo import bar,我需要更新我的补丁来引用工具模块。从本质上讲,由于我的测试对实现了解太多,因此要进行这个重构,需要更改的内容要比预期的多得多。

更好的方法

使用import foo,我的测试可以忽略模块是如何实现的,而只是对整个模块打补丁。

# my_module.py

from tools import Thing


def do_thing():
    Thing().do_thing()


# tools.py

import foo


class Thing:
    def do_thing(self):
        foo.bar('do a thing')  # Specify 'bar' is from 'foo' module


# test_my_module.py

from unittest.mock import patch
import my_module


patch('foo')  # Patch entire foo module
def test_do_thing(mock_foo):
    my_module.do_thing()  # Changed test (expected)
    mock_foo.bar.assert_called_with('do a thing')

测试知道的实现细节越少越好。这样,如果您提出了更好的解决方案(使用类而不是函数,使用额外的文件来分离思想,等等),那么在您的测试中需要更改的内容就会更少,以适应重构。

导入模块——你不需要额外的努力从模块中获取另一个东西。它有缺点,如冗余输入

模块导入-更少的输入和更多的控制一个模块的项目可以访问。要使用模块中的新项,必须更新import语句。

正如Jan Wrobel提到的,不同导入的一个方面是导入的披露方式。

模块mymath

from math import gcd
...

使用mymath:

import mymath
mymath.gcd(30, 42)  # will work though maybe not expected

如果我导入gcd只是为了内部使用,而不向mymath的用户公开它,这可能会很不方便。我经常遇到这种情况,在大多数情况下,我想“保持我的模块干净”。

除了Jan Wrobel提出的通过使用导入数学来掩盖这一点之外,我已经开始通过使用前导下划线来隐藏导入,以避免公开:

# for instance...
from math import gcd as _gcd
# or...
import math as _math

在较大的项目中,这种“最佳实践”允许我准确地控制向后续导入公开的内容和不公开的内容。这使我的模块保持干净,并在一定规模的项目中回报。