2020年11月18日 星期三

Python 的 Import 陷阱

在脫離 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_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,但第三個例子才是正確寫法。

這邊 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

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

# 從 `random` module 裡 import 其中一個 function `randint`
from random import randint

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

# Import 整個 `random` module,
# 但這個名字可能跟其他地方有衝突,因此改名成 `rd`
import random as rd

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

# Import 所有 `random` module 底下的東西
from random import *

語法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

要點:

  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 裡有 AB 兩個 module 想互打招呼,程式碼如下:

A.py

from .B import B_greet_back

B.py

from .A import A_greet_back

內容都一樣,只是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

就不會發生錯誤:

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 更改成如下:

# 前面全刪

也會成功跑出結果。跟前面類似,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 了,有不清楚的地方可以直接參考。

 

 https://medium.com/pyladies-taiwan/python-%E7%9A%84-import-%E9%99%B7%E9%98%B1-3538e74f57e3

沒有留言: