Python模块(Package Module)import陷阱

两句话总结:

  • 内部的相对引用如果用from ..Branch import module1, from .Branch import module2。。。类似这种形式,然后在最外层python -m Tree.m1,在内层用python m1.py会报错
  • 文件结构如下:
    ------------------------
    Tree\m1.py:
    from .Branch import m3
    m3.printSelf()
    ------------------------
    Tree\Branch\m3.py:
    from . import m4
    def printSelf():
    print('In m3')
    ------------------------
    Tree\Branch\m4.py
    def printSelf():
    print('In m4')
    ------------------------
    去Tree下执行python -m m1,报:
    Traceback (most recent call last):
    File "D:\ProgramFiles\Anaconda3\lib\runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
    File "D:\ProgramFiles\Anaconda3\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
    File "D:\codes\Tree\m1.py", line 1, in <module>
    from .Branch import m3
    ImportError: attempted relative import with no known parent package
    
    去Tree下执行python m1.py,会报:
    Traceback (most recent call last):
      File "m1.py", line 1, in <module>
        from .Branch import m3
    ModuleNotFoundError: No module named '__main__.Branch'; '__main__' is not a package
    
    去Tree外层执行python -m Tree.m1,执行成功
    

     

  • 内部的相对引用如果用from . import module1, from .. import module2, from Branch import module3。。。类似这种形式,直接python m1.py或者python -m m1.py都可以。
文件结构如下:
------------------------
Tree\m1.py:
from Branch import m3
m3.printSelf()
------------------------
Tree\Branch\m3.py:
from . import m4
def printSelf():
print('In m3')
------------------------
Tree\Branch\m4.py
def printSelf():
print('In m4')
------------------------
去Tree下执行python -m m1,执行成功

去Tree下执行python m1.py,执行成功

去Tree外层执行python -m Tree.m1,会报错:
Traceback (most recent call last):
  File "D:\ProgramFiles\Anaconda3\lib\runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "D:\ProgramFiles\Anaconda3\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "D:\codes\Tree\m1.py", line 1, in <module>
    from Branch import m3
ModuleNotFoundError: No module named 'Branch'

-------------------------------------------------------------------------------------------------------------------------------------------------------------------

 一个Python Module(模块),是一个文件,包含了Python对象定义和Python语句(definitions and statements)。文件名就是模块名加上后缀.py,在模块内部,模块名存储在全局变量__name__中,是一个string,可以直接在module中通过__name__引用到module name。

module是为了重复使用一些对象,比如类,函数,而将这些对象定义放在一个.py文件中,或是将一个较大的工程裁缝为多个.py文件而易于维护,每一个.py文件都是一个module。

1,模块的定义和引入(import)

如下一个fibo.py文件

复制代码

1 print ("__name__ :", __name__)
2 def fib(n):
3         a, b = 0, 1
4         result = []
5         print ("in fib():", __name__)
6         while (b<n):
7                 result.append(b)
8                 a, b = b, a+b
9         print(result)

复制代码

这个fibo.py就是一个module,它有一个函数定义fib(),和一个语句(statement),第一行的print语句,我们在当前文件目录运行Python Interpreter就可以去引入这个模块,并执行模块中定义的fib()函数。

>>> import fibo
('__name__ :', 'fibo')         #print语句执行
>>> fibo.fib(10)
('in fib() :', 'fibo')         #fib()函数执行
[1, 1, 2, 3, 5, 8]

可以看到,在import的时候,这个module的语句(statements)执行了,所定义的函数并未执行,在通过module名引用module中的函数定义时,函数才被执行

同样可以在一个script file中引入module,我们在fibo.py所在的目录创建另一个文件calculat.py

1 from fibo import fib                                                                                                                      
2 for n in range(10, 50, 5):
3         fib(n)

然后用Python解释器运行calcute.py得到结果。

这里有两种import 语句,

一种是import module_name1 [as name1], module_name2 [as name2]

一种是from module_name import item1 [as name1], item2 [as name2]

 

2, module的加载

每个module都包含对象定义和一些语句(statements),这些语句应该是意图要来初始化这个module的,这些语句会在这个module第一次被import的时候执行(多次import只会执行一次,不管是以上两种import的语句中那一种),当这个module被作为一个script来运行的时候也会被执行。

每个module都有自己的private symbol table,当使用第一种import语句import一个module的时候,引入者的local symbol table就加入了这个module,其名字如果没有使用as的话就是被引入的模块名本身。使用第二种import语句这会在引入者的local symbol table中加入具体引用的item,其名称若没使用as则就为item的名称。

 

3,module搜索路径

当遇到一个名为xiaoyu的module需要import的时候,Python Interpreter首先搜寻built-in module中有没有叫这个名的,若是没有,则Interpreter会从一系列的目录中去搜寻这个module(也就是这个.py文件),这些目录值存储在sys.path中,而sys.path又是用这些值来初始化的:

  • 当前目录,即input script所在的目录
  • 环境变量PYTHONPATH中存储的值(PYTHONPATH的语法和PATH一样)
  • Python包的安装目录,比如我的服务器上django就安装在 /usr/local/lib/python2.7/dist-packages/中,sys.path含有这个目录

Python有一个标准库,其中定义了一系列的module,这些module中的一部分是直接集成在Interpreter中的,这些built-in module主要提供了很重要的但是Python语言又没有提供的功能,比如跟system call有关的sys module就集成在所有平台的Python Interpreter中,在Interpreter中集成哪些module是可以配置的,并且随平台不同而有差别。

在启动Interpreter,sys.path被初始化后,我们可以对它进行修改

>>> import sys

>>> sys.path.append('/root/home/project/code/python')

 

4, 把module作为script来执行

前面我们已经提到了关于module中语句的执行。这里要补充一点东西,通常一个script file指的是调用Python Interpreter时作为参数传递给Interpreter的文件,当然所有的.py文件都是一个module,这样的一个script或是module,其__name__会被Interpreter自动设置为"__main__"。以下是一个测试:

复制代码

 1 print ("__name__ :", __name__)
 2 def fib(n):
 3         a, b = 0, 1
 4         result = []
 5         print ("in fib() :", __name__)
 6         while (b<n):
 7                 result.append(b)
 8                 a, b = b, a+b
 9         print(result)
10 
11 if __name__ == "__main__":
12         import sys
13         fib(int(sys.argv[1]))   

复制代码

用Python Interpreter直接调用这个script

oot@AY1212240253255e84714:/home/project/code/python# python fibo.py 22

('__name__ :', '__main__')

('in fib() :', '__main__')

[1, 1, 2, 3, 5, 8, 13, 21]

可以看到依然module的语句都会被执行,只是__name__的值一开始就变为了"__main__",给一个模块加上

if __name == "__main__":

常常是为了测试这个模块,因为这个语句块只有当module被作为script直接传给Interpreter的时候才会被执行。

上面例子中的12行import sys可以看出,Python并没有规定import语句必须写在module的最前面,只是习惯性的我们约定都写在最前面。

 

5. 内置dir()函数(built-in dir() function)

 dir()函数可以用来查看一个module所定义的所有names,试验

>>> dir()

['__builtins__', '__doc__', '__name__', '__package__']

>>> import sys, fibo as fibo_local

('__name__ :', 'fibo')

>>> dir()

['__builtins__', '__doc__', '__name__', '__package__', 'fibo_local', 'sys']

>>> dir(fibo)

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

NameError: name 'fibo' is not defined

>>> dir(fibo_local)

['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'fib']

 可以看到在import了fibo和sys后,并且fibo是用别名fibo_local来引入的,在引入者module中就定义了sys和fibo_local,可以看到dir(fibo)是抛了NameError异常的,fibo并没有被定义,定义的是fibo_local,这也可以看出import语句对local symbol table是怎样影响的。

 

6, 模块包(package) 

 包(package)可以理解为是组织起来的module的一个层次结构,也就是package是一个directory,它包含sub-package或者是module,而module是.py文件,要让Python Interpreter把一个目录作为package,则该目录下必须有__init__.py文件,__init__.py可以为空,当然也可以有对象定义和语句,用来做初始化工作,__init__.py还有个作用就是设置__all__变量。

package本身就可以来作为一个module使用,只是它所包含的sub-module或module可以通过package name用package.module的名称形式去引用,这更有利于组织一系列相关的module,避免module间定义的名称的混乱。

package在实际工程中非常常用,__init__.py也常常不会为空,而会有对象定义和初始化代码来让这个包,也就是这个module,包含其该有的item定义。以后我们会对package做更多了解。

 

 

参考:

1,http://docs.python.org/3.3/tutorial/modules.html  modules

2,http://docs.python.org/3.3/reference/simple_stmts.html#the-import-statement  import语句

3,http://docs.python.org/3/reference/executionmodel.html Python execution model

4,http://docs.python.org/3/reference/import.html Python import system

-------------------------------------------------------------------------------------------------------------------------------------------------------------------

再转一篇知乎的文章:

Python用了快两年了吧,其中有些东西一直是稀里糊涂地用,import便是我一直没有明白的东西。曾经有过三次解决它的机会,我都因得过且过、一拖再拖而没能化敌为友。今天下午,它又给了我一次机会,我想我还是从了它的心愿吧。

故事是从这篇台湾同胞的博客(Python 的 Import 陷阱)开始的,然后又跳到了Python社区的PEP 328提案(PEP 328 -- Imports: Multi-Line and Absolute/Relative),再结合过去的经验以及一些测试,我想我大概懂了吧。下面是我的总结,希望内容能够言简意赅、易于理解。

import语句有什么用?import语句用来导入其他python文件(称为模块module),使用该模块里定义的类、方法或者变量,从而达到代码复用的目的。为了方便说明,我们用实例来说明import的用法,读者朋友可以跟着尝试(尝试时建议使用python3,python2和python3在import的表现有差异,之后会提到)。

首先,先建立一个文件夹Tree作为工作目录,并在其内建立两个文件m1.py和m2.py,在m1.py写入代码:

import os
import m2
m2.printSelf()

在m2.py写入代码:

def printSelf():
	print('In m2')

打开命令行,进入到Tree目录下,敲下python m1.py运行,发现没有报错,且打印出In m2,说明这样使用import没有问题。由此我们总结出import语句的第一种用法。

  • import module_name。即import后直接接模块名。在这种情况下,Python会在两个地方寻找这个模块,第一是sys.path(通过运行代码import sys; print(sys.path)查看),os这个模块所在的目录就在列表sys.path中,一般安装的Python库的目录都可以在sys.path中找到(前提是要将Python的安装目录添加到电脑的环境变量),所以对于安装好的库,我们直接import即可。第二个地方就是运行文件(这里是m1.py)所在的目录,因为m2.py和运行文件在同一目录下,所以上述写法没有问题。

用上述方法导入原有的sys.path中的库没有问题。但是,最好不要用上述方法导入同目录下的文件!因为这可能会出错。演示这个错误需要用到import语句的第二种写法,所以先来学一学import的第二种写法。在Tree目录下新建一个目录Branch,在Branch中新建文件m3.py,m3.py的内容如下:

def printSelf():
	print('In m3')

如何在m1中导入m3.py呢,请看更改后的m1.py:

from Branch import m3
m3.printSelf()

总结import语句的第二种用法:

  • from package_name import module_name。一般把模块组成的集合称为包(package)。与第一种写法类似,Python会在sys.path和运行文件目录这两个地方寻找包,然后导入包中名为module_name的模块。

现在我们来说明为什么不要用import的第一种写法来导入同目录下的文件。在Branch目录下新建m4.py文件,m4.py的内容如下:

def printSelf():
	print('In m4')

然后我们在m3.py中直接导入m4,m3.py变为:

import m4
def printSelf():
	print('In m3')

这时候运行m1.py就会报错了,说没法导入m4模块。为什么呢?我们来看一下导入流程:m1使用from Branch import m3导入m3,然后在m3.py中用import m4导入m4。看出问题了吗?m4.py和m1.py不在同一目录,怎么能直接使用import m4导入m4呢。(读者可以试试直接在Tree目录下新建另一个m4.py文件,你会发现再运行m1.py就不会出错了,只不过导入的是第二个m4.py了)

面对上面的错误,使用python2运行m1.py就不会报错,因为在python2中,上面提到的import的两种写法都属于相对导入,而在python3中,却属于绝对导入。话说到了这里,就要牵扯到import中最关键的部分了——相对导入和绝对导入。

我们还是谈论python3的import用法。上面提到的两种写法属于绝对导入,即用于导入sys.path中的包和运行文件所在目录下的包。对于sys.path中的包,这种写法毫无问题;导入自己写的文件,如果是非运行入口文件(上面的m1.py是运行入口文件,可以使用绝对导入),则需要相对导入。

比如对于非运行入口文件m3.py,其导入m4.py需要使用相对导入:

from . import m4
def printSelf():
	print('In m3')

这时候再运行m1.py就ok了。列举一下相对导入的写法:

  • from . import module_name。导入和自己同目录下的模块。
  • from .package_name import module_name。导入和自己同目录的包的模块。
  • from .. import module_name。导入上级目录的模块。
  • from ..package_name import module_name。导入位于上级目录下的包的模块。
  • 当然还可以有更多的.,每多一个点就多往上一层目录。

不知道你有没有留神上面的一句话——“上面的m1.py是运行入口文件,可以使用绝对导入”,这句话是没问题的,也和我平时的做法一致。那么,运行入口文件可不可以使用相对导入呢?比如m1.py内容改成:

from .Branch import m3
m3.printSelf()

答案是可以,但不能用python m1.py命令,而是需要使用在Tree外层用python -m Tree.m1来运行。为什么?关于前者,PEP 328提案中的一段文字好像给出了原因:

Relative imports use a module's _name _ attribute to determine that module's position in the package hierarchy. If the module's name does not contain any package information (e.g. it is set to '__main __') then relative imports are resolved as if the module were a top level module, regardless of where the module is actually located on the file system.

我不太懂,但是又有一点明白。我们应该见过下面一段代码:

if __name__ == '__main__':
	main()

意思是如果运行了当前文件,则__name__变量会置为__main__,然后会执行main函数,如果当前文件是被其他文件作为模块导入的话,则__name__为模块名,不等于__main__,就不会执行main函数。比如对于上述更改后的m1.py,执行python m1.py命令后,会报如下错误:

Traceback (most recent call last): File "m1.py", line 1, in from .Branch import m3 ModuleNotFoundError: No module named '_ main_.Branch'; '__main__' is not a package

据此我猜测执行python m1.py命令后,当前目录所代表的包'.'变成了__main__。

那为什么python -m Tree.m1就可以呢?那位台湾老师给出了解释:

执行指令中的-m是为了让Python预先import你要的package或module给你,然后再执行script。

即不把m1.py当作运行入口文件,而是也把它当作被导入的模块,这就和非运行入口文件有一样的表现了。

那反过来,如果m1.py使用绝对导入(from Branch import m3),能使用python -m m1运行吗?我试了一下,如果当前目录是Tree就可以。如果在其他目录下运行,比如在Tree所在的目录(使用python -m Tree.m1运行),就不可以。这可能还是与绝对导入相关。

(之前看到了一个大型项目,其运行入口文件有一大堆的相对导入,我还傻乎乎地用python直接运行它。之后看到他给的样例运行命令是带了-m参数的。现在才恍然大悟。)

理解import的难点差不多就这样了。下面说一说import的其他简单但实用的用法。

  • import moudle_name as alias。有些module_name比较长,之后写它时较为麻烦,或者module_name会出现名字冲突,可以用as来给它改名,如import numpy as np
  • from module_name import function_name, variable_name, class_name。上面导入的都是整个模块,有时候我们只想使用模块中的某些函数、某些变量、某些类,用这种写法就可以了。使用逗号可以导入模块中的多个元素。
  • 有时候导入的元素很多,可以使用反斜杠来换行,官方推荐使用括号。
from Tkinter import Tk, Frame, Button, Entry, Canvas, Text, \
    LEFT, DISABLED, NORMAL, RIDGE, END	# 反斜杠换行
from Tkinter import (Tk, Frame, Button, Entry, Canvas, Text,
    LEFT, DISABLED, NORMAL, RIDGE, END)	# 括号换行(推荐)

说到这感觉import的核心已经说完了。再跟着上面的博客说一说使用import可能碰到的问题吧。

问题1描述:ValueError: attempted relative import beyond top-level package。直面问题的第一步是去了解熟悉它,最好是能复现它,让它躺在两跨之间任我们去践踏蹂躏。仍然是上面四个文件,稍作修改,四个文件如下:

# m1.py
from Branch import m3
m3.printSelf()
# m2.py
def printSelf():
	print('module2')
# m3.py
from .. import m2 # 复现的关键在这 #
print(__name__)
def printSelf():
	print('In m3')
# m4.py
def printSelf():
	print('In m4')

运行python m1.py,就会出现该问题。问题何在?我猜测,运行m1.py后,m1代表的模块就是顶层模块(参见上面PEP 328的引用),而m3.py中尝试导入的m2模块所在的包(即Tree目录代表的包)比m1的层级更高,所以会报出这样的错误。怎么解决呢?将m1.py的所有导入改为相对导入,然后进入m1.py的上层目录,运行python -m Tree.m1即可。

对于使用import出现的其他问题,碰到了再接着更新。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------

再转一篇台湾朋友的文章 (https://medium.com/pyladies-taiwan/python-%E7%9A%84-import-%E9%99%B7%E9%98%B1-3538e74f57e3):

在脫離 Python 幼幼班準備建立稍大型的專案的時候,學習如何組織化你的 Python 專案是一大要點。Python 提供的 module(模組)與 package(套件)是建立架構的基本元件,但在module之間為了重複使用一些 function(函數)或 class(類別)而必須互相 import(匯入),使用上一個不注意就會掉入混亂的 import 陷阱。

此篇將會從基本 module 和 package 介紹起,提點基本 import 語法及 absolute import 和 relative import 的用法與差異,最後舉出幾個常見因為錯誤 import 觀念造成的錯誤。

請注意,以下只針對 Python3 進行講解與測試。


Module與Package

基本上一個檔案就是一個 module,裡頭可以定義 function,class,和 variable。
把一個 module 想成一個檔案,那一個package就是一個目錄了。Package 可裝有 subpackage 和 module,讓你的專案更條理更組織化,最後一坨打包好還能分給別人使用。

先看看 module。假設有一個 module sample_module.py 裡頭定義了一個 function sample_func

def sample_func():
    print('Hello!')

現在你在同一個目錄裡下有另一個 module sample_module_import.py 想要重複使用這個 function,這時可以直接從 sample_module import 拿取:

from sample_module import sample_funcif __name__ == '__main__':
    sample_func()

跑 python3 sample_module_import.py 會得到:

Hello!

再來是 package。我們把上面兩個檔案包在一個新的目錄 sample_package 底下:

sample_package/
├── __init__.py
├── sample_module.py
└── sample_module_import.py

很重要的是新增那個 __init__.py 檔。它是空的沒關係,但一定要有,有點宣稱自己是一個 package 的味道。

這時候如果是進到 sample_package 裡面跑一樣的指令,那沒差。但既然都打包成 package 了,通常是在整個專案的其他地方需要用到的時候 import 它,這時候裡面的 import 就要稍微做因應。

讓我們修正一下 sample_package/sample_module_import.py 。假設這時我們在跟 sample_package 同一個 folder 底下執行下面兩種指令:

1. python3 sample_package/sample_module_import.py
2. python3 -m sample_package.sample_module_import

以下幾種不同的 import 寫法,會各有什麼效果呢?

# 不標準的 implicit relative import 寫法(Python 3 不支援)
from sample_module import sample_func
1. 成功印出 Hello!
2. ModuleNotFoundError。因為 Python 3 不支援 implicit relative import (前面不加點的寫法),故會將之當作 absolute import,但第三個例子才是正確寫法。# 標準的 explicit relative import 寫法
from .sample_module import sample_func
1. 包含相對路徑的檔案不能直接執行,只能作為 module 被引用,所以失敗
2. 成功印出 Hello!# 標準的 absolute import 寫法
from sample_package.sample_module import sample_func
1. 如果此層目錄位置不在 python path 中,就會失敗
2. 成功印出 Hello!

這邊 absolute import 和 relative import 的詳細說明請稍候。

執行指令中的 -m是為了讓 Python 預先 import 你要的 package 或 module 給你,然後再執行 script。所以這時 sample_module_import 在跑的時候,是以 sample_package 為環境的,這樣那些 import 才會合理。

另外,python path 是 Python 查找 module 時候使用的路徑,例如 standard module 所在的目錄位置。因此在第三種寫法中,Python 會因為在 python path 中找不到 sample_package.sample_module而噴 error。你可以選擇把當前目錄加到 sys.path,也就是 Python path(初始化自環境變數PYTHONPATH),來讓 Python 搜尋得到這個 module ,但這個方法很髒很難維護,最多用來debug,其他時候強烈不建議使用。


基本 import 語法

前面有看過了,這邊統整介紹一下。如果你想使用在其他 module 裡定義的 function、class、variable 等等,就需要在使用它們之前先進行 import。通常都會把需要 import 的 module 們列在整個檔案的最一開始,但不是必須。

語法1:import [module]

# Import 整個 `random` module
import random# 使用 `random` module 底下的 `randint` function
print(random.randint(0, 5))

語法2:from [module] import [name1, name2, ...]

# 從 `random` module 裡 import 其中一個 function `randint`
from random import randint# 不一樣的是,使用 `randint` 的時候就不需要先寫 `random` 了
print(randint(0, 5))

語法3:import [module] as [new_name]

# Import 整個 `random` module,
# 但這個名字可能跟其他地方有衝突,因此改名成 `rd` 
import random as rd# 使用 `rd` 這個名稱取代原本的 `random`
print(rd.randint(0, 5))

語法4(不推薦):from [module] import *

# Import 所有 `random` module 底下的東西
from random import *# 使用 `randint` 的時候也不需要先寫 `random`
print(randint(0, 5))

語法4不推薦原因是容易造成名稱衝突,降低可讀性和可維護性。


Absolute Import v.s. Relative Import

Python 有兩種 import 方法,absolute import 及 relative import。Absolute import 就是完整使用 module 路徑,relative import 則是使用以當前 package為參考的相對路徑。

Relative import 的需求在於,有時候在改變專案架構的時候,裡面的 package 和 module 會拉來拉去,這時候如果這些 package 裡面使用的是relative import 的話,他們的相對關係就不會改變,也就是不需要再一一進入 module 裡更改路徑。但因為 relative import 的路徑取決於當前 package,所以在哪裡執行就會造成不一樣的結果,一不小心又要噴一堆 error;這時absolute import 就會減少許多困擾。

這邊參考PEP328提供的範例。Package 架構如下:

package
├── __init__.py
├── subpackage1
│   ├── __init__.py
│   ├── moduleX.py
│   └── moduleY.py
├── subpackage2
│   ├── __init__.py
│   └── moduleZ.py
└── moduleA.py

現在假設 package/subpackage1/moduleX.py想要從其他 module 裡 import 一些東西,則使用下列語法([A]表 absolute import 範例;[R]表 relative import 範例):

# Import 同一個 package 底下的 sibling module `moduleY`
[A] from package.subpackage1 import moduleY
[R] from . import moduleY
[Error] import .moduleY# 從同一個 package 底下的 sibling module `moduleY` 中,
# import `spam` 這個 function
[A] from package.subpackage1.moduleY import spam
[R] from .moduleY import spam# 從隔壁 package 底下的 module `moduleZ` 中,
# import `eggs` 這個 function
[A] from package.subpackage2.moduleZ import eggs
[R] from ..subpackage2.moduleZ import eggs# Import parent package 底下的 module `moduleA`
[A] from package import moduleA
[R] from .. import moduleA 或 from ... package import moduleA

要點:

  1. Relative import 裡,..代表上一層 ,多幾個.就代表多上幾層。
  2. Relative import 一律採用 from ... import ...語法,即使是從 . import也要寫 from . import some_module 而非 import .some_module。原因是.some_module這個名稱在 expression 裡無法出現。Absolute import 則無限制。

常見 import 陷阱

Trap 1: Circular Import

想像一個 module A在一開始要 import 另一個 module B 裡的東西,但在匯入 module B 的途中也必須得執行它,而很不巧的 module B也需要從 module A import 一些東西。但 module A還正在執行途中,自己都還沒定義好自己的 function 啊!於是你不讓我我不讓你,這種類似 deadlock 的情形正是常見的 circular import(循環匯入)

讓我們看看範例。現在在 sample_package 裡有 A 和 B 兩個 module 想互打招呼,程式碼如下:

A.py

from .B import B_greet_back
def A_say_hello():
    print('A says hello!')
    B_greet_back()def A_greet_back():
    print('A says hello back!')
if __name__ == '__main__':
    A_say_hello()

B.py

from .A import A_greet_back
def B_say_hello():
    print('B says hello!')
    A_greet_back()def B_greet_back():
    print('B says hello back!')
if __name__ == '__main__':
    B_say_hello()

內容都一樣,只是A/B互換。B 很有禮貌想先打招呼。在與 sample_package 同目錄底下執行:

$ python3 -m sample_package.B

會得到:

Traceback (most recent call last):
  File "/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/runpy.py", line 193, in _run_module_as_main
 "__main__", mod_spec)
 File "/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/runpy.py", line 85, in _run_code
 exec(code, run_globals)
 File "/path/to/sample_package/B.py", line 2, in <module>
 from .A import A_greet_back
 File "/path/to/sample_package/A.py", line 1, in <module>
 from .B import B_greet_back
 File "/path/to/sample_package/B.py", line 2, in <module>
 from .A import A_greet_back
ImportError: cannot import name 'A_greet_back'

觀察到了嗎?B 試圖 import A_greet_back,但途中先進到 A 執行,而因為 Python 是從頭開始一行一行執行下來的,於是在定義 A_greet_back之前會先碰到自己的 import statement,於是又進入 B,然後陷入死胡同。

常見解決這種circular import的方法如下:

  1. Import 整個 module 而非單一 attribute

把 B.py 更改成如下:

# from .A import A_greet_back
from . import A
def B_say_hello():
    print('B says hello!')
    # A_greet_back()
    A.A_greet_back()...

就不會發生錯誤:

B says hello!
A says hello back!

理由是,原本執行 from .A import A_greet_back 時被迫要從 load 進來的 Amodule object 中找出 A_greet_back 的定義,但此時這個 module object 還是空的;而更新後的 from . import A 就只會檢查 A module object 存不存在,至於 A_greet_back 存不存在等到需要執行的時候再去找就行了。

2. 延遲 import

把 B.py 更改成如下:

# 前面全刪def B_say_hello():
    from .A import A_greet_back    print('B says hello!')
    A_greet_back()
...

也會成功跑出結果。跟前面類似,Python 在跑到這行時才會 import A module,這時因為 B module 都已經 load 完了,所以不會有 circular import 的問題。但這個方法比較 hacky 一點,大概只能在 hackathon 中使用,否則正式專案裡看到這種難維護的 code 可能會有生命危險。

另一方面,把所有 import statement 擺到整個 module 最後面也是類似效果,但也會被打。

3. 好好釐清架構,避免circular import

是的,治本方法還是好好思考自己寫的 code 為什麼會陷入這種危機,然後重新 refactor 吧。

Trap 2: Relative Import above Top-level Package

還不熟悉 relative import 的人常常會見到這個 error:

ValueError: attempted relative import beyond top-level package

讓我們重現一下這個 error。把 B.py 前頭更改成如下:

# from . import A
from ..sample_package import A
...

現在我們的路徑位置在與 sample_package 同目錄底下。跑:

$ python3 -m sample_package.B

會得到:

Traceback (most recent call last):
  File "/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/runpy.py", line 193, in _run_module_as_main
 "__main__", mod_spec)
 File "/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/runpy.py", line 85, in _run_code
 exec(code, run_globals)
 File "/path/to/sample_package/B.py", line 5, in <module>
 from ..sample_package import A
ValueError: attempted relative import beyond top-level package

所謂的 top-level package 就是你所執行的 package 中最高的那一層,也就是 sample_package。超過這一層的 relative import 是不被允許的,指的就是..sample_package 這行嘗試跳一層上去而超過 sample_package了。

可以試試更改當前目錄到上一層(cd ..),假設叫 parent_folder ,然後執行 python3 -m parent_folder.sample_package.B,就會發現 error 消失了,因為現在的 top-level package 已經變成 parent_folder了。


結語

Import 是各大語言必備功能,看似簡單,使用上來說陷阱卻頗多。如果搞不清楚 Python 中的 import 是怎麼運作的,除了在整體專案架構上難以靈活設計,更可能要陷入可怕的 error 海了。

我寫了一些額外的 sample code 放上 github 了,有不清楚的地方可以直接參考。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值