根據編程經驗的不同,我們在運行程序時可能經常或者偶爾碰到下面這些問題,仔細觀察后會發現這些問題無一例外都出現了一個相同的短語,很容易就可以發現,這個短語就是"relative import"。
ValueError: attempted relative import beyond top-level package # 翻譯:試圖在頂級包之外進行相對導入
ImportError: attempted relative import with no known parent package # 翻譯:嘗試相對導入,但沒有已知的父包
ValueError: Attempted relative import in non-package # 翻譯:試圖在非包中進行相對導入
SystemError: Parent module '' not loaded, cannot perform relative import # 翻譯:父模塊'xxx'未加載,不能執行相對導入。
既然關於相對導入的報錯提示,說明我們在代碼中一定用到了相對導入的語法。下面先簡單介紹一下相對導入的概念,隨后詳細介紹相對導入可能的問題和原理,最后給出上面提到的每個報錯的解決方案。
絕對導入
既然要介紹相對導入,那必然繞不開絕對導入。絕對導入的格式為 import A.B 或 from A import B
下面是絕對導入的一些🌰:
import fibo # 隱式相對導入 from fibo import fibo1, fibo2 # 絕對路徑導入 import fibo as fib # 重命名 from fibo import fib as fibonacci
相對導入
相對導入格式為 from .A import B 或 from ..X import Y, . 代表當前包, .. 代表上層包, ... 代表上上層包,依次類推。
相對導入的一些案例如下所示:
from . import echo # 表示從當前文件所在package導入echo這個module from .. import formats # 表示從當前文件所在package的上層package導入formats這個子package或者moudle from ..filters import equalizer # 表示從當前文件所在package的上層package導入的filters這個子package或者子module中導入equalizer
相對導入基於當前模塊的名稱。由於主模塊的名稱始終為"__main__"
,因此用作 Python 應用程序主模塊的模塊必須始終使用絕對導入。主模塊所在文件夾不會被視作package,因此除了主模塊外,與主模塊處在同個文件夾的模塊(也就是同級的模塊)也必須使用絕對導入。
還有個更專業的說法: 相對導入使用模塊的名稱屬性來決定模塊在包層次結構中的位置,如果模塊的名稱不包含任何包信息(例如:模塊名稱為'main'),那么相對導入則被解析為最頂層的位置,不管這個時候這個模塊實際上位於文件系統中的什么位置。
乍一看是不是覺得雲里霧里,感覺每個字都認識,但就是不知道是什么意思,沒關系,看完本文就理解了。加油💪🏻。
包package
文件夾被python解釋器視作package需要滿足兩個條件:
1、文件夾中必須有__init__.py文件,該文件可以為空,但必須存在該文件。
2、不能作為頂層模塊來執行該文件夾中的py文件(即不能作為主函數的入口)。頂層模塊即是我們通常說的"main"方法所在的模塊及其同級模塊,其中"main"也常被稱為主模塊,即主模塊所在文件夾不會被視作package。主模塊的同級package被python解釋器視為頂級包(也就是top-level package)。
如果想要導入頂層包更上層的包或者模塊,需要將包路徑添加到sys.path中
第一點很容易理解,下面詳細介紹一下第二點。
腳本 & 模塊?(script vs module)
python有兩種加載文件的方法:一種是作為頂層的腳本,另一種是當做模塊。如果你直接執行這個程序,那么這個文件就被當做是頂層腳本來執行了,在命令行里面輸入 python myfile.py 就是這個情況。如果你輸入python -m myfile.py或者在其他的文件當中使用import來導入這個文件的時候,它就被當做模塊來導入。在同一時間里,只有一個主模塊,主模塊常被稱為頂層腳本,頂層腳本可以這樣解釋:它是一個能夠讓你的程序從這里開始的python文件。
將模塊作為腳本執行
test_script.py
# test_script.py def fun1(): print("I'm fun1") def fun2(): print("I'm fun2") if __name__ == "__main__": fun1() fun2()
腳本中的__main__="name"下的代碼僅在模塊作為“主”文件執行時才運行:
$ python test_script.py
I'm fun1
I'm fun2
如果module是作為導入的模塊,則不會執行該模塊的__main__代碼:
>>>import fibo >>>
這通常用於為模塊提供方便的用戶界面,或用於測試目的(將模塊作為腳本執行測試套件運行)。
模塊的名稱
當一個文件被加載進來,它就有一個名稱(這個名稱存儲在__name__屬性當中)。如果這個文件被當做一個主模塊來執行,那么它的名字就是__main__。如果它被當做一個模塊加載,那么它的名稱就是文件名稱,加上它所在的包名,以及所有的頂層的包名,這些名稱中間是用點號隔開的。
比如下面的例子
package/ __init__.py subpackage1/ __init__.py moduleX.py moduleA.py
比如你導入moduleX(from package.subpackag1 import moduleX),它的名稱就package.subpackage1.mouleX。如果你導入moduleA的時候(from package import moduleA),它的名稱就是package.moudleA。
(注:這里是使用包導入,即把package以及里面的所有文件看做一個包,導入的時候使用from xxx import yyy的形式來進行,我們調用第三方包的時候就是這種情況),
但是,當你直接從命令行里面運行moduleX的時候,他的名稱則被替換為__main__。如果你直接從命令行運行moduleA,它的名稱也是__main__。當一個模塊被當做一個頂層腳本來執行的時候,它原來的名稱則會被__main__取代。
結論
當一個模塊的名稱中沒有包,也就是只有文件名的時候,說明這個模塊是一個頂層模塊。頂層模塊中不能使用相對導入。相對導入使用模塊的名稱屬性來決定模塊在包層次結構中的位置,相對導入能向上相對多少級,完全取決於模塊名稱中有多少層。
當你運行交互式的解釋器的時候,交互式進程的名稱永遠是__main__,因此你不能在交互式進程當中使用相對導入。相對導入只能在模塊文件當中使用。
參考: python相對包導入報“Attempted relative import in non-package”錯誤
解決方案:
1、ImportError: attempted relative import with no known parent package
導致這個問題的原因: 主模塊或者同級模塊用到了相對導入,且引用了主模塊所在包。因為主模塊所在包不會被python解釋器視為package,在python解釋器看來主模塊所在的包就是一個未知的父包,所以如果不小心以相對導入的方式引用到了,就會報with no known parent package
這個錯誤。
案例一
主模塊的同級模塊在使用相對導入時引入了主模塊所在包的案例
TestModule/
├── __init__.py # 這個文件其實未起作用
├── main.py # import brother1; print(__name__) ├── brother1.py # from . import brother2; print(__name__) └── brother2.py # print(__name__)
運行main.py,運行結果如下
Traceback (most recent call last):
File "/TestModule/main.py", line 1, in <module> import brother1 File "/TestModule/brother1.py", line 1, in <module> from . import brother2 ImportError: attempted relative import with no known parent package
案例二
主模塊在使用相對導入時引入了主模塊所在包的案例
TestModule/
├── __init__.py # 這個文件其實未起作用
├── main.py # from . import brother1; print(__name__) ├── brother1.py # import brother2; print(__name__) └── brother2.py # print(__name__)
運行main.py,運行結果如下
Traceback (most recent call last):
File "/TestModule/main.py", line 1, in <module> from . import brother1 ImportError: attempted relative import with no known parent package
方案一:
解決方案也很簡單,將相對導入給成絕對導入即可,上面這個案例只需要把from .
去掉即可。比如第一個案例
TestModule/
├── __init__.py # 這個文件其實未起作用
├── main.py # import brother1; print(__name__) ├── brother1.py # import brother2; print(__name__) └── brother2.py # print(__name__)
運行main.py
brother2
brother1
__main__
方案二
案例2只能使用改為絕對導入這種方式,但是案例一還有一種解決方式是把main.py文件移動到TestModule文件夾外面,使之與TestModule文件夾平級,這樣TestModule即會被解析器視為一個package,在其他模塊中使用相對導入的方式引用到了也不會報錯。
2、ValueError: attempted relative import beyond top-level package
導致這個問題的原因: 主模塊所在同級包的子模塊在使用相對導入時引用了主模塊所在包。因為主模塊所在包不會被python解釋器視為package,主模塊的同級package被視為頂級包(也就是top-level package),所以主模塊所在包其實是在python解釋器解析到的頂層包之外的,如果不小心以相對導入的方式引用到了,就會報beyond top-level package
這個錯誤。
一個案例:
TestModule/
├── main.py # from Tom import tom; print(__name__) ├── __init__.py ├── Tom │ ├── __init__.py # print(__name__) │ ├── tom.py # from . import tom_brother; from ..Kate import kate; print(__name__) │ └── tom_brother.py # print(__name__) └── Kate ├── __init__.py # print(__name__) └── kate.py # print(__name__)
python main.py
Tom # 這個是Tom包的__init__.py的模塊名,可以看出包里的__init__.py的模塊名就是包名
Tom.tom_brother
Traceback (most recent call last):
File "/TestModule/main.py", line 1, in <module> from Tom import tom File "/TestModule/Tom/tom.py", line 2, in <module> from ..Kate import kate ImportError: attempted relative import beyond top-level package
這個問題同樣有兩個解決方案
方案一:
把main.py移動到TestModule文件夾外面,使之與TestModule平級,這樣TestModule即會被解析器視為一個package,在其他模塊中使用相對導入的方式引用到了也不會報錯。
src/
├── main.py # from TestModule.Tom import tom; print(__name__) └── TestModule/ ├── __init__.py # print(__name__) ├── Tom │ ├── __init__.py # print(__name__) │ ├── tom.py # from . import tom_brother; from ..Kate import kate; print(__name__) │ └── tom_brother.py # print(__name__) └── Kate ├── __init__.py # print(__name__) └── kate.py # print(__name__)
運行main.py
TestModule
TestModule.Tom
TestModule.Tom.tom_brother
TestModule.Kate
TestModule.Kate.kate
TestModule.Tom.tom
__main__
方案二:
tom.py中將TestModule包加入到sys.path變量中,並使用絕對導入的方式導入Kate包,修改后的tom.py內容如下:
from . import tom_brother import os, sys sys.path.append("..") # 等價於 sys.path.append(os.path.dirname(os.path.dirname(__file__))) from Kate import kate # 改成絕對導入的方式導入Kate print(__name__)
運行結果如下
Tom
Tom.tom_brother
Kate
Kate.kate
Tom.tom
__main__
關於為什么已經把TestModule加入了包查找路徑還需要使用絕對導入來導入Kate的的解釋:
從上面的運行結果可以看出,tom_brother.py的模塊名還是Tom.tom_brother,模塊名並沒有因為把TestModule加入了包查找路徑就發生改變,而相對導入是根據模塊名來確定的,如果模塊名中沒有TestModule,那還是不能使用絕對導入的方式來導入Kate,所以必須使用絕對導入的方式來導入Kate包
3、ValueError: Attempted relative import in non-package
4、SystemError: Parent module ' ' not loaded, cannot perform relative import
3和4這兩個報錯的原因是一樣的,都是把使用了相對導入的module當做主模塊而直接運行了,即直接運行的腳本中包含了相對導入。但是我按這種方式操作了一下,發現無論怎么操作,報錯提示始終是上面的第一個報錯,即ImportError: attempted relative import with no known parent package
。
后來發現是python 版本的問題,
python2在直接運行的腳本中使用相對導入時會報ValueError: Attempted relative import in non-package
這個錯誤,
python3.x(沒測3.x具體是哪個版本)到python3.5報錯SystemError: Parent module '' not loaded, cannot perform relative import
;
python3.6及以上的報錯提示是ImportError: attempted relative import with no known parent package
。
解決方案是:
去除主模塊(腳本)中的相對導入,改為絕對導入。
這個問題的復現花我了好大一番功夫,找了好多博客,都說是主模塊中用到了相對導入,但是我這么做就是不報這個錯,而是報第一個錯
ImportError: attempted relative import with no known parent package
,最后還是在一些博客中找到了蛛絲馬跡,並且用docker快速拉取對應區間python版本鏡像驗證,得出結論,平時多學點東西還是很有用處的,比如docker,看似平時用不上,關鍵時刻還是真幫了大忙,否則重新在電腦上安裝一個python版本,估計又得費老大功夫了。
相對導入對於包的優勢
既然會引發這么多問題,那是不是我們以后就完全不用相對導入了呢。當然不!
相對導入相較於絕對導入還是有一些優勢的
- 書寫相較於絕對導入簡單
- 相對導入可以避免硬編碼帶來的包維護問題,例如我們改了某一層包的名稱,那么其它模塊對於其子包的所有絕對導入就不能用了,但是采用相對導入語句的模塊,就會避免這個問題。
順便一提
包查找路徑是sys.path
變量中。sys.path
初始狀態一般由三部分組成:python正在執行的腳本的目錄,PYTHONPATH路徑,包的默認安裝路徑。
自從python2.6,模塊的名稱不在決定使用__name__屬性,而是使用__packege__屬性。這就是為什么我避免使用__name__這么明確的名稱來代表一個模塊的名稱。自從python2.6,一個模塊的名稱是由__package__+'.'+__name__來確定的,如果__packege__是None的話,那么這個名稱就是__name__了
參考:
python相對包導入報“Attempted relative import in non-package”錯誤
源文:
