python包匯入細節

駿馬金龍發表於2018-11-15

包匯入格式

匯入模組時除了使用模組名進行匯入,還可以使用目錄名進行匯入。例如,在sys.path路徑下,有一個dir1/dir2/mod.py模組,那麼在任意位置處都可以使用下面這種方式匯入這個模組。

import dir1.dir2.mod
from dir1.dir2.mod import XXX

一個實際一點的示例,設定PYTHONPATH環境變數為d:pypath,然後在此目錄下建立以上目錄和mod.py檔案:

set PYTHONPATH="D:pypath"
mkdir d:pypathdir1dir2
echo print("mod.py") >d:pypathdir1dir2mod.py
echo x=3 >>d:pypathdir1dir2mod.py

# 進入互動式python
>>> import dir1.dir2.mod
mod.py
>>> dir1.dir2.mod.x
3

注1:在python3.3版本及更高版本是可以匯入成功的,但是在python3.3之前的版本將失敗,因為缺少__init__.py檔案,稍後會解釋該檔案
注2:頂級目錄dir1必須位於sys.path列出的路徑搜尋列表下

如果輸出dir1和dir2,將會看到它們的是模組物件,且是名稱空間

>>> import dir1.dir2.mod
mod.py

>>> dir1
<module `dir1` (namespace)>

>>> dir1.dir2
<module `dir1.dir2` (namespace)>

>>> dir1.dir2.mod
<module `dir1.dir2.mod` from `d:\pypath\dir1\dir2\mod.py`>

這種模組+名稱空間的形式就是包(嚴格地說是包的一種形式),也就是說dir1是包,dir2也是包,這種方式是包的匯入形式。包主要用來組織它裡面的模組。

從上面的結果也可以看出,包也是模組,所以能使用模組的地方就能使用包。例如下面的程式碼,可以像匯入模組一樣直接匯入包dir2,包和模組的區別在於它們的組織形式不一樣,模組可能位於包內,僅此而已。

import dir1.dir2
from dir1 import dir2

另外,匯入dir1.dir2.mod時,它宣告的模組變數名為dir1,而不是dir1.dir2.mod,但是匯入的物件卻包含了3個模組:dir1、dir1.dir2以及dir1.dir2.mod。如下:

>>> dir()
[`__annotations__`, `__builtins__`, `__doc__`, `__loader__`, `__name__`, `__package__`, `__spec__`, `dir1`]

>>> for key in sys.modules:
...     if key.startswith("dir1"):
...             print(key,":",sys.modules[key])
...
dir1 : <module `dir1` (namespace)>
dir1.dir2 : <module `dir1.dir2` (namespace)>
dir1.dir2.mod : <module `dir1.dir2.mod` from `d:\pypath\dir1\dir2\mod.py`>

__init__.py檔案

上面的dir1和dir1.dir2目前是空包,或者說是空模組(再一次強調,包就是模組)。但並不意味著它們對應的模組物件是空的,因為模組是物件,只要是物件就會有屬性。例如,dir1包有如下屬性:

>>> dir(dir1)
[`__doc__`, `__loader__`, `__name__`, `__package__`, `__path__`, `__spec__`, `dir2`]

之所以稱為空包,是因為它們現在僅提供了包的組織功能,而且它們是目錄,而不像py檔案一樣,是實實在在的可以編寫模組程式碼的地方。換句話說,包現在是目錄檔案,而不是真正的模組檔案。

為了讓包”真正的”成為模組,需要在每個包所代表的目錄下加入一個__init__.py檔案,它表示讓這個目錄格式的模組(也就是包)像py檔案一樣可以寫模組程式碼,只不過這些模組程式碼是寫入__init__.py中的。當然,模組檔案中允許沒有任何內容,所以__init__.py檔案也可以是空檔案,它僅表示讓包成為真正的模組檔案。

每次匯入包的時候,如果有__init__.py檔案,將會自動執行這個檔案中的程式碼,就像模組檔案一樣,事實上它就是讓目錄代表的包變成模組的,甚至可以說它就是包所對應的模組檔案(見下面示例),所以也可以認為__init__.py是包的初始化檔案。在python3.3之前,這個檔案必須存在,否則就會報錯,因為它不認為目錄是有效的模組。

現在,在dir1和dir2下分別建立空檔案__init__.py

type nul>d:pypathdir1\__init__.py
type nul>d:pypathdir1dir2\__init__.py

現在目錄的層次格式如下:

λ tree /f d:pypath
D:PYPATH
└─dir1
    │  __init__.py
    └─dir2
            mod.py
            __init__.py

再去執行匯入操作,並輸出包dir1和dir2。

>>> import dir1.dir2.mod
mod.py

>>> dir1
<module `dir1` from `d:\pypath\dir1\__init__.py`>

>>> dir1.dir2
<module `dir1.dir2` from `d:\pypath\dir1\dir2\__init__.py`>

>>> dir1.dir2.mod
<module `dir1.dir2.mod` from `d:\pypath\dir1\dir2\mod.py`>

從輸出結果中不難看出,包dir1和dir1.dir2是模組,且它們的模組檔案是各自目錄下的__init__.py

實際上,包分為兩種:名稱空間模組、普通模組。名稱空間包是沒有__init__.py檔案的,普通包是有__init__.py檔案的。無論是哪種,它都是模組。

__init__.py寫什麼內容

既然包是模組,而__init__.py檔案是包的模組檔案,這個檔案中應該寫入什麼程式碼?答案是可以寫入任何程式碼,我們只需把它當作一個模組對待就可以。不過,包既然是用來組織模組的,真正的功能性屬性應該儘量寫入到它所組織的模組檔案中(也就是示例中的mod.py)。

但有一項__all__是應該在__init__.py檔案中定義的,它是一個列表,用來控制from package import *使用*匯入哪些模組檔案。這裡的*並非像想象中那樣會匯入包中的所有模組檔案,而是隻匯出__all__列表中指定的模組檔案。

例如,在dir1.dir2包下有mod1.py、mod2.py、mod3.py和mod4.py,如果在dir2/__init__.py檔案中寫入:

__all__ = ["mod1", "mod2", "mod3"]

則執行:

from dir1.dir2 import *

不會匯入mod4,而是隻匯入mod1-mod3。

如果不設定__all__,則from dir1.dir2 import *不會匯入該包下的任何模組,但會匯入dir1和dir1.dir2。

__path__屬性

嚴格地說,只有當某個模組設定了__path__屬性時,才算是包,否則只算是模組。這是包的絕對嚴格定義。

__path__屬性是一個路徑列表(可迭代物件即可,但通常用列表),和sys.path類似,該列表中定義了該包的初始化模組檔案__init__.py的路徑。

只要匯入的是一個包(無論是名稱空間包還是普通包),首先就會設定該屬性,預設匯入目錄時該屬性會初始化當前目錄,然後去該屬性列出的路徑下搜尋__init__.py檔案對包進行初始化。預設情況下由於__init__.py檔案後執行,在此檔案中可以繼續定義或修改__path__屬性,使得python會去找其它路徑下的__init__.py對模組進行初始化。

以下是預設初始化後的__path__值:

>>> import dir1.dir2
>>> dir1.dir2.__path__
[`d:\pypath\dir1\dir2`]

>>> import dir1.dir3
>>> dir1.dir3
<module `dir1.dir3` (namespace)>
>>> dir1.dir3.__path__
_NamespacePath([`d:\pypath\dir1\dir3`])

一般來說,幾乎不會設定__path__屬性。

匯入示例

import和from匯入時有多種語法可用,這兩個語句的匯入方式和匯入普通模組的方式是一樣的:import匯入時需要使用字首名稱去引用,from匯入時是賦值到當前程式的同名全域性變數中。如果不瞭解,請看前一篇文章:python模組匯入細節

假設現在有如下目錄結構,且d:pypath位於sys.path列表中:

$ tree -f d:pypath
d:pypath
└── dir1
    ├── __init__.py
    └── dir2
        ├── __init__.py
        └── mod.py

只匯入包:

import dir1             # 匯入包dir1
import dir1.dir2        # 匯入包dir1.dir2
from dir1 import dir2   # 匯入包dir1.dir2

匯入某個模組:

import dir1.dir2.mod
from dir1.dir2 import mod

如果dir2/__init__.py中設定了__all__,則下面的匯入語句會匯入已設定的模組:

from dir1.dir2 import *

注意,只支援上面這種from...import *語法,不支援import *

匯入模組中的屬性,比如變數x:

from dir1.dir2.mod import x

相對路徑匯入

注:如果允許,不要使用相對路徑匯入,很容易出錯,特別是對新手而言。使用絕對路徑匯入,並將包放在sys.path的某個路徑下就可以。

假設現在有如下目錄結構:

$ tree -f d:pypath
d:pypath
└── dir1
    ├── __init__.py
    ├── dir4
    │   ├── __init__.py
    │   ├── c2.py
    │   └── c1.py
    ├── dir3
    │   ├── __init__.py
    │   ├── b3.py
    │   ├── b2.py
    │   └── b1.py
    └── dir2
        ├── __init__.py
        ├── a4.py
        ├── a3.py
        ├── a2.py
        └── a1.py

在dir1.dir2.a1模組檔案中想要匯入dir1.dir3.b2模組,可以在a1.py中使用下面兩種方式匯入:

import dir1.dir3.b2
from dir1.dir2. import b2

上面的匯入方式是使用絕對路徑進行匯入的,只要使用絕對路徑,都是從sys.path開始搜尋的。例如,上面是從sys.path下搜尋dir1,再依次搜尋dir1.dir3.b2。

python還支援包的相對路徑的匯入,只要使用...即可,就像作業系統上的相對路徑一樣。使用相對路徑匯入時不會搜尋sys.path。

相對路徑匯入方式只有from...import支援,import語句不支援,且只有使用...的才算是相對路徑,否則就是絕對路徑,就會從sys.path下搜尋

例如,在a1.py中匯入dir1.dir3.b2:

from ..dir3 import b2

注意,必須不能直接python a1.py執行這個檔案,這樣會報錯:

    from ..dir3 import b2
ValueError: attempted relative import beyond top-level package

報錯原因稍後解釋。現在在互動式模式下匯入,或者使用python -m dir1.dir2.a1的方式執行。

>>> import dir1.dir2.a1

以下幾個示例都如此測試。

在a1.py中匯入包dir3:

from .. import dir3

在a1.py中匯入dir1.dir2.a2,也就是同目錄下的a2.py:

from . import a2

匯入模組的屬性,如變數x:

from ..dir3.b2 import x
from .a2 import x

相對路徑匯入陷阱

前面說過一個相對路徑匯入時的錯誤:

    from ..dir3 import b2
ValueError: attempted relative import beyond top-level package

dir3明明在dir1下,在路徑相對上,dir3確實是a1.py的../dir3,但執行python a1.py為什麼會報錯?

from ..dir3 import b2

這是因為檔案系統路徑並不真的代表包的相對路徑,當在dir1/a1.py中使用..dir3,python並不知道包dir1的存在,因為沒有將它匯入,沒有宣告為模組變數,同樣,也不知道dir2的存在,僅僅只是根據語句知道了dir3的存在。但因為使用了相對路徑,不會搜尋sys.path,所以它的相對路徑邊界只在本檔案。所以,下面的匯入也是錯誤的:

from . import a2

實際上,更標準的解釋是,當py檔案作為可執行程式檔案執行時,它所在的模組名為__main__,即__name____main__,但它並非一個包,而是一個模組檔案,對它來說沒有任何相對路徑可言。

解決方法是顯式匯入它們的父包,讓python記錄它的存在,只有這樣才能使用..

python -m dir1.dir2.a2

還有幾個常見的相對路徑匯入錯誤:

from .a3 import x

錯誤:

ModuleNotFoundError: No module named `__main__.a3`; `__main__` is not a package

原因是一樣的,py檔案作為可執行程式檔案執行時,它所在的模組名為__main__,它並非一個包。

最後,建議在條件允許的情況下,使用絕對路徑匯入,而不是相對路徑。

使用別名匯入

通過包的匯入方式也支援別名。例如:

from dir1.dir2.a2 import x as xx
print(xx)

import dir1.dir2.a2 as a2
print(a2.x)

from dir1.dir2 import a2 as a22
print(a22.x)

相關文章