天天看點

python 調用子產品 子產品内部調用其他子產品的import問題:python 調用子產品 子產品内部調用其他子產品的import問題:

python 調用子產品 子產品内部調用其他子產品的import問題:

參考連結

最近遇到一個python import的問題,經過是這樣的:

我先實作好一個功能子產品,這個功能子產品有多級目錄和很多 .py 檔案,然後把該功能子產品放到其他目錄下作為子子產品,運作代碼時,就報錯ModuleNotFoundError

子產品導入原理

一個module(子產品)就是一個.py檔案,一個package(包)就是一個包含.py檔案的檔案夾(對于python2,該檔案夾下還需要__init__.py)。我這裡隻考慮python3的情況。

在python腳本被執行,python導入其他包或子產品時,python會根據sys.path清單裡的路徑尋找這些包或子產品。如果沒找到的話,程式就會報錯ModuleNotFoundError。

既然要根據sys.path清單裡的路徑找到這些需要導入包或子產品,就需要知道這個清單裡都是些什麼東西。

import sys
print(sys.path)
           

sys.path清單中的每個元素為一個搜尋子產品的路徑,程式中要導入包或子產品就需要在這些路徑中進行查找,主要分為三種情況:

  1. 目前執行腳本(主動執行,而不是被其他子產品調用)所在路徑 (所在的目錄路徑)。
  2. python内置的标準庫路徑,PYTHONPATH。
  3. 安裝的第三方子產品路徑。

在運作程式時,先在第一個路徑下查找所需子產品,沒找到就到第二個路徑下找,以此類推,按順序在所有路徑都查找後依然沒找到所需子產品,則抛出錯誤。清單的第一項是調用python解釋器的腳本所在的目錄,是以預設先在腳本所在路徑下尋找子產品。

是以從這裡可以知道的是,如果我們在腳本所在路徑下定義和python标準庫同名的子產品,那麼程式就會調用我們自定義的該子產品而不是标準庫中的子產品。

  • ModuleNotFoundError

    知道了調用子產品的流程,現在來分析一下文章最開始提到的那個錯誤。

假設功能子產品的目錄樹為:

package_0

├── module_0.py

├── module_1.py

├── package_1

│ ├── init.py

│ ├── module_2.py

│ ├── module_3.py

│ └── package_2

│ ├── init.py

│ ├── module_21.py

│ └── module_22.py

└── package_3

├── init.py

└── module_4.py

要建構一個package,則對應檔案夾下需要包含__init.py檔案(python2版本)。

執行指令為 python module_0.py,即通過 module_0.py 來調用python解釋器,則該腳本檔案所在的路徑(’/home/…/package_0’)會被添加到 sys.path 中,可以通過該路徑找到其他子產品的,比如下面這些語句:

# module_0.py
import module_1
from package_1 import module_2
from package_1.package_2 import module_21
           

而在 module_2.py 中加入下面這句:

# module_2.py
import module_3
           

分為下面兩種情況:

  1. 執行 python module_2.py 時,不會出現錯誤。
  2. 執行 python module_0.py 時,出現錯誤:ModuleNotFoundError: No module named ‘module_3’。

    第一種情況把路徑(’/home/…/package_0/package_1’)添加到 sys.path 中,可以通過package_1 找到 module_3。

第二種情況把路徑(’/home/…/package_0’)添加到 sys.path 中,該路徑下就不能在 module_2.py 中通過這種方式找到module_3,(程式會去 package_0 下面找 module_3.py檔案,是以找不到)因為module_2.py 在路徑/home/…/package_0/package_1下。

解決辦法:

絕對路徑導入

在上面第二種情況中想調用module_3的話,可以使用絕對路徑導入的方式:

# module_2.py
from package_1 import module_3
           

即在路徑/home/…/package_0/package_1下先找到package_1,再找到module_3。

同理,想在module_21.py中調用module_22,可以使用如下方式:

# module_21.py
from package_1.package_2 import module_22
           

絕對導入根據從項目根檔案夾開始的完整路徑導入各個子產品。

使用絕對路徑的方式就可以解決這個問題,但是如果package_0這個檔案夾要放到其他項目中,則這個檔案夾下的所有相關導入都要修改,即在絕對導入的基礎上再加一層。

而且如果檔案夾層級太多,調用一個子產品就需要寫很長一串,顯得很備援。想要簡單一些的話,可以考慮相對路徑導入。

相對路徑導入

相對導入的形式取決于目前位置以及要導入的子產品、包或對象的位置。相對導入看起來就比絕對導入簡潔一些。

相對導入使用點符号來指定位置。

單點表示引用的子產品或包與目前子產品在同一目錄中(同一個包中)。

兩點表示在目前子產品所在位置的父目錄中。

還是執行指令為 python module_0.py,想在 module_2.py 中導入其他子產品,可以使用如下方法:

# module_2.py
from . import module_3 # 第一行表示調用和module_2 在同一路徑的module_3 子產品。
from .package_2 import module_21 # 第二行表示調用和module_2 在同一路徑的package_2 包下的module_21 子產品。
           

還有兩種用法:

from .. import module_name:導入本子產品上一級目錄的子產品。

from ..package_name import module_name。導入本子產品上一級目錄下的包中的子產品。

第一個:

在 module_21 中導入 module_2:from … import module_2

在 module_2 中導入 module_4:from …package_3 import module_4

理論上這兩句都沒錯,但是第二句會報如下錯誤:

ValueError: attempted relative import beyond top-level package

這個報錯的意思是:試圖在頂級包(top-level package)之外進行相對導入。也就是說相對導入隻适用于頂級包之内的子產品。

如果将 module_0.py 當作執行子產品,則和該子產品同級的 package_1 和 package_3 就是頂級包(top-level package),而 module_2 在package_1中,module_0、module_1和module_4都在 package_1之外,是以調用這三個子產品時,就會報這個錯誤。

第二個:

還有個注意點就是使用了相對導入的子產品檔案不能作為頂層執行檔案,即不能通過 python 指令執行,比如執行python module_0.py,在 module_0 中添加如下語句:

# module_0.py
from .package_1 import module_2
           

報錯如下:

ModuleNotFoundError: No module named '__main__.package_1'; '__main__' is not a package

python 的相對導入會通過子產品的 name 屬性來判斷該子產品的位置,當子產品作為頂層檔案被執行時,其 name 這個值為 main,不包含任何包的名字,而當這個子產品被别的子產品調用時,其 name 的值為這個子產品和其所在包的名字,比如 module_2 的 name 值為 package_1.module_2。

。。。其實這個内部原理我也沒弄清楚,可以檢視這個stackoverflow 問題,最後結論就是使用了相對導入的子產品檔案不能被直接運作,隻能通過其他子產品調用。

使用相對導入沒有絕對導入那麼直覺,而且如果目錄結構發生改變,則也要修改對應子產品的導入語句。是以我最後使用的是下面這種方法。

添加路徑到sys.path

前面說過程式隻會在sys.path 清單的路徑中搜尋子產品,那麼就可以想到另一個解決方法,即将想調用包或子產品的路徑添加到sys.path 中。

還是執行 python module_0.py,已經知道在 module_2.py 中直接導入module_3 子產品會報錯,除了使用絕對導入和相對導入,還可以将module_2.py 所在目錄添加到sys.path 中。

# module_2.py
sys.path.append(os.path.dirname(__file__))
import module_3
from package_2 import module_21
           

sys.path.append(os.path.dirname(file)) 表示的含義如下:

使用 sys.path.append 将某路徑添加到sys.path 中。

file 獲得該子產品檔案的絕對路徑

os.path.dirname(file) 獲得子產品檔案所在的目錄

是以這條語句就是把子產品檔案所在的目錄添加到sys.path 中。通過這種方法可以比較靈活地把其他路徑添加到sys.path 中,而沒有什麼限制

比如導入module_4.py 所在路徑:

# module_2.py
sys.path.append(os.path.join(os.path.dirname(__file__), '../package_3'))
import module_4
           

其中的 os.path.join(os.path.dirname(file), ‘…/package_3’) 的值為:/home/zxd/Documents/package_0/package_1/…/package_3,兩點表示上一級目錄。然後我們就可以直接導入module_4 了。

當通過這種方法導入工程檔案中的很多子產品路徑在sys.path 中時,如果工程檔案中存在重名子產品,可能會報錯:ImportError: cannot import name。這個要小心一點。