Python 導包技巧

Python 導包技巧



起因

起初學 Go 語言的時候,對它的導包規則感到痛苦,——“那麼麻煩幹嘛呢!”。但最近接到一些新功能開發任務,於是 “啪嗒啪嗒” 寫代碼,結果更痛苦了。公司項目目錄規劃打一開始就不合理,以此爲基礎的 “繁榮發展” 導致更多混亂與麻煩。於是馬上翻開《Python cookbook》解惑,有些心得,這裏記之。

場景1

場景1:如何模塊化代碼,同時保證導入方式統一?

公司項目是老項目,因此代碼祖傳,常常一個 .py 文件裏有上千行代碼。恰巧現在重構(雖然並不是嚴格意義上的重構),同事們有意識地將代碼拆分成多個文件。然後就有了這樣的現象:

from onedir.twodir.file1 import func1
from onedir.twodir.three.file2 imort func2
from onedir.file3 import func3

目錄結構如下:

Guan@Orchard:~/python/08/demo$ tree onedir/
onedir/
├── file3.py
├── __init__.py
└── twodir
    ├── file1.py
    ├── __init__.py
    └── three
        ├── file2.py
        └── __init__.py

2 directories, 6 files

很顯然,要我記住哪個函數在哪個模塊中這是很困難、又沒意義的事。況且 onedir 裏邊的代碼只有一個模塊在使用,所以爲什麼不讓調用方式統一一下呢?這裏需要用到 Python 包中的 __init__.py 文件。

現在,修改 onedir 目錄下的 init 文件,修改後變成下面這樣:

# onedir/__init__.py
from .file3 import *
from .twodir.file1 import *
from .twodir.three.file2 import *

利用 init 文件加載子模塊,使得子模塊中的代碼在導入父級包時就可以直接使用。示例如下:

"""原來的導入方式"""
# from onedir.twodir.file1 import func1
# from onedir.twodir.three.file2 import func2
# from onedir.file3 import func3

""""現在的導入方式"""
from onedir import func1, func2, func3

func1()
func2()
func3()

# 輸出:
func1
func2
func3

這裏額外補充一下相對路徑導入模塊的姿勢。

我們都知道,在執行 import xxx 語句時,解釋器會先在當前目錄下找有沒有這個模塊,如果沒有,就去 sys.path 中的路徑裏去找。當兩個 .py 文件在同級目錄時:

Guan@Orchard:~/python/08$ tree demo/
demo/
├── view1.py
└── view2.py

0 directories, 3 files

view2.py 文件中可以直接 import view1,但是不可以 import .view1因爲相對路徑只允許 from … import … 這種導入方式。所以是不是 from . import view1 就可以了呢?也不行!

爲控制篇幅,我主要是在這裏拋結論,想獲悉原理可以耐心去看看這篇文章:Python相對導入機制詳解

結論:

  1. 在執行腳本所在的目錄下,不能使用相對路徑的導入方式。 也就是說,如果我想在 demo 目錄下運行 view2.py 文件,裏邊還有相對導入,那是絕對不可以的。當這個目錄不是執行腳本所在目錄時,就可以用相對導入了。
  2. 如果堅持直接運行 view2.py ,又想使用相對導入,請以 python -m demo.view2 這種方式運行腳本(這種方式下,Python2 要求 demo 目錄中存在 init 文件,但 Python3 沒有此要求)。

場景2

場景2:如何控制符號(函數,變量等)的導入?

我們現在已知的是,當一個函數名(變量、類同理)以下劃線開頭時,不允許 from xxx import * 的方式導入,但可以 from xxx import _xxx 這樣導入。但不知道你們有沒有覺得,下劃線開頭的符號看上去有點醜,我很不愛用,那麼有沒有其他控制方式呢?那當然是有的:__all__

在一個模塊中定義變量 __all__,並指明允許導出的符號名,from ... import * 就不能導入沒有列出的那些符號了。用法如下:

def func1():
    pass

def func2():
    pass

__all__ = ["func1"]  # 在其他模塊中使用時,func1 可以被導入,func2 不行

當然,from xxx import func2 仍可以把 func2() 導進來。

場景3

場景3:如何讓不同目錄下的代碼在統一的命名空間中導入?

目錄結構如下:

Guan@Orchard:~/python/08/demo$ tree demo/
demo/
├── demo1
│   └── spam
│       └── a.py
└── demo2
    └── spam
        └── b.py

4 directories, 2 files

現在,需要使用 a.pyb.py 中的代碼,尋常方式是:

import demo1.spam.a
import demo2.spam.b

儘管它們都在 spam 目錄下,但因爲頂級目錄(demo1 和 demo2)不同,所以需要不同的入口。但其實,是可以統一 a.pyb.py 的入口的。解決方式如下:

import sys
sys.path.extend(["demo1", "demo2"])

import spam.a
import spam.b

事實上,此時的 spam 是一個命名空間包。現在打印 spam ,得到的結果會是:<module ‘spam’ (namespace)>。同時,命名空間包也不會有 __file__ 這個屬性。

...
In [5]: spam
Out[5]:<module 'spam' (namespace)>

In [6]: spam.__file__
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-b25cc88cc23e> in <module>()
----> 1 spam.__file__

AttributeError: module 'spam' has no attribute '__file__'

創建命名空間包的關鍵在於,統一命名空間的頂層目錄中不能有 __init__.py 文件。拿上面的例子來說,就是不能在 spam 目錄下有這個文件。

你可以嘗試導入一個目錄,在兩種情況下,一個是該目錄下存在 init 文件,一個是不存在這個文件。然後打印這個目錄,看看結果會有什麼不同。

感謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章