喜歡這些文章嗎?買書吧! ***Jason C. McDonald 的《Dead Simple Python》可從 No Starch Press 取得。
教程最糟糕的部分始終是它們的簡單性,不是嗎?您很少會發現一個包含多個文件的文件,更罕見的是包含多個目錄的文件。
我發現建立 Python 專案是語言教學中最常被忽略的部分之一。更糟的是,許多開發人員都會犯錯,在一堆常見錯誤中跌跌撞撞,直到他們得到至少「有效」的東西。
好訊息是:您不必成為他們中的一員!
在《Dead Simple Python》系列的本期中,我們將探索「import」語句、模組、包,以及如何將所有內容組合在一起而不費力氣。我們甚至會涉及 VCS、PEP 和 Python 之禪。係好安全帶!
在我們深入研究實際的專案結構之前,讓我們先討論一下它如何適合我們的版本控制系統 [VCS]…從您需要 VCS 的事實開始!有幾個原因是...
追蹤您所做的每一個更改,
弄清楚你什麼時候弄壞了東西,
能夠查看舊版的程式碼,
備份您的程式碼,以及
與他人合作。
您有很多選擇。 Git 是最明顯的,尤其是當您不知道還可以使用什麼時。您可以在 GitHub、GitLab、Bitbucket 或 Gitote 等上免費託管 Git 儲存庫。如果您想要 Git 以外的東西,還有許多其他選擇,包括 Mercurial、Bazaar、Subversion(儘管如果您使用最後一個,您可能會被同行視為恐龍。)
我將悄悄假設您在本指南的其餘部分中使用 Git,因為這是我專門使用的。
建立儲存庫並將「本機副本」複製到電腦後,您就可以開始設定專案了。您至少需要建立以下內容:
README.md
:您的專案及其目標的描述。
LICENSE.md
:您的專案的許可證(如果它是開源的)。 (有關選擇一個的更多訊息,請參閱 opensource.org。)
.gitignore
:一個特殊文件,告訴 Git 要忽略哪些文件和目錄。 (如果您使用其他 VCS,則該檔案具有不同的名稱。請尋找。)
包含您的專案名稱的目錄。
沒錯...我們的Python 程式碼檔案實際上屬於一個單獨的子目錄! 這非常重要,因為我們的儲存庫的根目錄將變得非常混亂,其中包含建置檔案、打包腳本、虛擬環境以及各種方式其他實際上不屬於原始碼的內容。
僅為了舉例,我們將虛構的專案稱為「awesomething」。
Python 風格主要由一組稱為 Python 增強提案(縮寫為 PEP)的文件管轄。當然,並非所有 PEP 都被實際採納——這就是它們被稱為「提案」的原因——但有些是被採納的。您可以在Python官方網站上瀏覽主PEP索引。此索引的正式名稱為 PEP 0。
現在,我們主要關注 PEP 8,它最初由 Python 語言建立者 Guido van Rossum 於2001 年。該文件正式概述了所有Python 開發人員應普遍遵循的程式設計風格。把它放在枕頭下!學習它,遵循它,鼓勵其他人也這樣做。
(附註:PEP 8 指出樣式規則總是有例外。它是指南,而不是命令。)
現在,我們主要關注標題為 “包和模組名稱”的部分。。
模組應該有簡短的、全小寫的名稱。如果可以提高可讀性,可以在模組名稱中使用下劃線。 Python 套件也應該有短的、全小寫的名稱,儘管不鼓勵使用底線。
我們稍後會了解模組和包到底是什麼,但現在,請了解模組由檔案名稱命名,而套件由其目錄名稱命名。
換句話說,檔案名稱應全部小寫,如果可以提高可讀性,則使用下劃線。同樣,目錄名稱應全部小寫,如果可以避免,則不使用下劃線。換句話說...
執行此動作:awesomething/data/load_settings.py
不是這個:awesomething/Data/LoadSettings.py
我知道,我知道,這是一種冗長的表達方式,但至少我在你的步驟中加入了一點 PEP。 (你好?這個東西開著嗎?)
這會讓人感覺虎頭蛇尾,但這裡是那些承諾的定義:
任何Python(.py
)檔案都是一個模組,目錄中的一堆模組是一個套件。
嗯……差不多了。要讓目錄成為包,您還必須做另一件事,那就是將名為「init.py」的檔案貼到其中。實際上,您不必將任何內容放入該文件中。它必須在那裡。
您也可以使用__init__.py
做其他很酷的事情,但這超出了本指南的範圍,因此請閱讀文件以了解更多資訊。
如果你確實忘記了套件中的__init__.py
,它會做一些比失敗更奇怪的事情,因為這使它成為一個隱式命名空間包。您可以使用這種特殊類型的套件做一些有趣的事情,但我不會在這裡討論。像往常一樣,您可以透過閱讀文件來了解更多:PEP 420:隱式命名空間包。
所以,如果我們看看我們的專案結構,awesomething
實際上是一個包,它可以包含其他包。因此,我們可以將「awesomething」稱為我們的頂級包,以及其子包下的所有包。一旦我們開始進口東西,這將非常重要。
讓我們看一下我的現實專案“遺漏”的快照,以了解我們如何建置東西...
omission-git
├── LICENSE.md
├── omission
│ ├── app.py
│ ├── common
│ │ ├── classproperty.py
│ │ ├── constants.py
│ │ ├── game_enums.py
│ │ └── __init__.py
│ ├── data
│ │ ├── data_loader.py
│ │ ├── game_round_settings.py
│ │ ├── __init__.py
│ │ ├── scoreboard.py
│ │ └── settings.py
│ ├── game
│ │ ├── content_loader.py
│ │ ├── game_item.py
│ │ ├── game_round.py
│ │ ├── __init__.py
│ │ └── timer.py
│ ├── __init__.py
│ ├── __main__.py
│ ├── resources
│ └── tests
│ ├── __init__.py
│ ├── test_game_item.py
│ ├── test_game_round_settings.py
│ ├── test_scoreboard.py
│ ├── test_settings.py
│ ├── test_test.py
│ └── test_timer.py
├── pylintrc
├── README.md
└── .gitignore
(如果您想知道的話,我使用 UNIX 程式“tree”來製作上面的小圖。)
您會看到我有一個名為“omission”的頂級包,它有四個子包:“common”、“data”、“game”和“tests”。我還有“resources”目錄,但只包含遊戲音訊、圖像等(為簡潔起見,此處省略)。 resources
不是一個包,因為它不包含 __init__.py
。
我的頂層包中還有另一個特殊檔案:__main__.py
。這是當我們直接透過「python -m omission」執行頂級套件時執行的檔案。我們稍後會討論「main.py」中的內容。
如果您以前編寫過任何有意義的 Python 程式碼,那麼您幾乎肯定熟悉「import」語句。例如...
import re
知道當我們導入模組時,我們實際上是在執行它是有幫助的。這意味著模組中的任何“import”語句也正在執行。
例如,re.py
有幾個自己的 import 語句,當我們說 導入重新
。這並不意味著它們可用於我們從*導入“re”的文件,但這確實意味著這些文件必須存在。如果(由於某種不太可能的原因)“enum.py”在您的環境中被刪除,並且您執行了“import re”,它將失敗並出現錯誤...
回溯(最近一次呼叫最後一次):
檔案“weird.py”,第 1 行,位於 <module> 中
進口再
檔案“re.py”,第 122 行,位於 <module> 中
導入枚舉
ModuleNotFoundError:沒有名為「enum」的模組
當然,讀到這裡,你可能會有點困惑。有人問我為什麼找不到外部模組(在本例中為“re”)。其他人想知道為什麼要導入內部模組(此處為“enum”),因為他們沒有直接在程式碼中請求它。答案很簡單:我們導入了 re
,然後導入了 enum
。
當然,上面的場景是虛構的:「import enum」和「import re」在正常情況下永遠不會失敗,因為這兩個模組都是Python核心庫的一部分。這只是一個愚蠢的例子。 ;)
實際上有多種導入方式,但其中大多數應該很少使用(如果有的話)。
對於下面的所有範例,我們假設有一個名為「smart_door.py」的檔案:
# smart_door.py
def close():
print("Ahhhhhhhhhhhh.")
def open():
print("Thank you for making a simple door very happy.")
例如,我們將在 Python 互動式 shell 中執行本節中的其餘程式碼,執行位置與「smart_door.py」相同。
如果我們想執行open()
函數,我們必須先導入模組smart_door
。最簡單的方法是......
import smart_door
smart_door.open()
smart_door.close()
我們實際上會說“smart_door”是“open()”和“close()”的命名空間。 Python 開發人員非常喜歡命名空間,因為它們讓函數和其他內容的來源一目了然。
(順便說一句,不要將 命名空間 與 隱式命名空間包 混淆。它們是兩個不同的東西。)
Python 之禪,也稱為 PEP 20,定義了 Python 語言背後的哲學。最後一行有一個聲明解決了這個問題:
命名空間是一個非常棒的想法——讓我們做更多這樣的事情!
然而,在某種程度上,命名空間可能會變得很痛苦,尤其是對於嵌套包來說。 foo.bar.baz.whatever.doThing()
太醜了。值得慶幸的是,我們確實有辦法避免每次呼叫函數時都必須使用命名空間。
如果我們希望能夠使用 open()
函數,而不必總是在其前面加上模組名稱,我們可以這樣做...
from smart_door import open
open()
但請注意,「close()」和「smart_door.close()」在最後一個場景中都不起作用,因為我們沒有直接匯入該函數。要使用它,我們必須將程式碼更改為這樣...
from smart_door import open, close
open()
close()
在之前可怕的嵌套包噩夢中,我們現在可以說“from foo.bar.baz.whatever import doThing”,然後直接使用“doThing()”。或者,如果我們想要一點命名空間,我們可以說“from foo.bar.baz importwhatever”,然後說“whatever.doThing()”。
“導入”系統非常靈活。
但不久之後,您可能會發現自己說“但是我的模組中有數百個函數,我想全部使用它們!”這是許多開發人員偏離軌道的地方,這樣做...
from smart_door import *
這非常非常糟糕! 簡而言之,它直接導入模組中的所有內容,這是一個問題。想像一下下面的程式碼...
from smart_door import *
from gzip import *
open()
你認為會發生什麼事?答案是,「gzip.open()」將是被呼叫的函數,因為這是在我們的程式碼中導入並定義的「open()」的最後一個版本。 smart_door.open()
已被 shadowed - 我們不能稱之為 open()
,這意味著我們實際上根本無法呼叫它。
當然,由於我們通常不知道,或者至少不記得每個導入的模組中的每個函數、類別和變數,所以我們很容易陷入一堆混亂。
Python 之禪 也解決了這個情況...
顯式優於隱式。
您永遠不必“猜測”函數或變數來自何處。文件中的某個位置應該有程式碼“明確”告訴我們它來自哪裡。前兩個場景證明了這一點。
我還應該提到,早期的 foo.bar.baz.whatever.doThing()
場景是 Python 開發人員不喜歡看到的。也來自 Python 之禪...
扁平比嵌套更好。
一些包的嵌套是可以的,但是當你的專案開始看起來像一套精緻的俄羅斯娃娃時,你就做錯了。將模組組織到包中,但保持相當簡單。
我們之前建立的專案文件結構即將「非常方便」。回想一下我的「遺漏」專案...
omission-git
├── LICENSE.md
├── omission
│ ├── app.py
│ ├── common
│ │ ├── classproperty.py
│ │ ├── constants.py
│ │ ├── game_enums.py
│ │ └── __init__.py
│ ├── data
│ │ ├── data_loader.py
│ │ ├── game_round_settings.py
│ │ ├── __init__.py
│ │ ├── scoreboard.py
│ │ └── settings.py
│ ├── game
│ │ ├── content_loader.py
│ │ ├── game_item.py
│ │ ├── game_round.py
│ │ ├── __init__.py
│ │ └── timer.py
│ ├── __init__.py
│ ├── __main__.py
│ ├── resources
│ └── tests
│ ├── __init__.py
│ ├── test_game_item.py
│ ├── test_game_round_settings.py
│ ├── test_scoreboard.py
│ ├── test_settings.py
│ ├── test_test.py
│ └── test_timer.py
├── pylintrc
├── README.md
└── .gitignore
在我的“game_round_settings”模組中,由“omission/data/game_round_settings.py”定義,我想使用“GameMode”類別。類別在「omission/common/game_enums.py」中定義。我怎樣才能到達它?
因為我將“omission”定義為包,並將模組組織到子包中,所以實際上非常簡單。在“game_round_settings.py”中,我說......
from omission.common.game_enums import GameMode
這稱為絕對導入。它從頂級包“omission”開始,然後進入“common”包,在其中查找“game_enums.py”。
一些開發人員向我提供更像「from common.game_enums import GameMode」的導入語句,並想知道為什麼它不起作用。簡而言之,「data」套件(「game_round_settings.py」所在的位置)不知道其兄弟包。
然而,它確實知道它的父母。正因為如此,Python 有一種叫做「相對導入」的東西,它可以讓我們做同樣的事情,就像這樣...
from ..common.game_enums import GameMode
..
表示“此套件的直接父包”,在本例中為“omission”。因此,導入後退一級,進入“common”,並找到“game_enums.py”。
關於是否使用絕對導入或相對導入有許多爭論。就我個人而言,我更喜歡盡可能使用絕對導入,因為它使程式碼更具可讀性。不過,您可以自己做決定。唯一重要的部分是結果是「顯而易見的」——任何東西的來源都不應該是神秘的。
(繼續閱讀:Real Python - Python 中的絕對導入與相對導入
這裡還隱藏著另一個陷阱!在 omission/data/settings.py
中,我有這一行:
from omission.data.game_round_settings import GameRoundSettings
當然,由於這兩個模組都在同一個包中,我們應該可以直接說“from game_round_settings import GameRoundSettings”,對嗎?
錯誤! 它實際上無法找到“game_round_settings.py”。這是因為我們正在執行頂級包“omission”,這意味著搜尋路徑(Python 查找模組的位置以及順序)的工作方式不同。
但是,我們可以使用相對導入來代替:
from .game_round_settings import GameRoundSettings
在這種情況下,單一“.”表示“這個包”。
如果您熟悉典型的 UNIX 檔案系統,這應該開始有意義。 ..
表示“後一級”,“.表示“目前位置”。當然,Python 更進一步:
...表示“後兩級”,
....` 表示“後三級”,依此類推。
但是,請記住,這些「等級」不僅僅是簡單的目錄。他們是包裹。如果在一個不是包的普通目錄中有兩個不同的包,則不能使用相對導入從一個包跳到另一個包。為此,您必須使用 Python 搜尋路徑,但這超出了本指南的範圍。 (請參閱本文末尾的文件。)
__main__.py
還記得我提到在我們的頂級包中建立一個 __main__.py
嗎?這是一個特殊的文件,當我們直接使用 Python 執行套件時會執行該文件。我的“omission”包可以使用“python -m omission”從我的存儲庫的根目錄執行。
這是該文件的內容:
from omission import app
if __name__ == '__main__':
app.run()
是的,實際上就是這樣!我正在從頂級包“omission”導入我的模組“app”。
請記住,我也可以說「來自…」。改為導入應用程式。或者,如果我只想說“run()”而不是“app.run()”,我可以執行“from omission.app import run”或“from .app import run”。最後,只要程式碼可讀,我如何進行導入並沒有太大的技術差異。
(附註:我們可以爭論為我的主要run()
函數設定一個單獨的app.py
對我來說是否合乎邏輯,但我有我的理由......而且它們超出了本指南的範圍。 )
首先讓大多數人感到困惑的部分是整個「if name == 'main'」語句。 Python 沒有太多樣板 - 必須非常普遍地使用且幾乎不需要修改的程式碼 - 但這是那些罕見的位元之一。
__name__
是每個 Python 模組的特殊字串屬性。如果我將“print(name)”行貼在“omission/data/settings.py”的頂部,當該模組被導入(並因此執行)時,我們會看到“omission.data.settings”被打印出去。
當模組直接透過「python -m some_module」運作時,模組會被指派一個特殊值「name」:「main」。
因此,「if name == 'main':」實際上是在檢查該模組是否以 main 模組執行。如果是,它將在條件下執行程式碼。
您可以透過另一種方式看到這一點。如果我將以下內容加入到“app.py”的底部...
if __name__ == '__main__':
run()
……然後我可以直接透過 python -m omission.app
執行該模組,結果與 python -m omission
相同。現在__main__.py
被完全忽略,omission/app.py
的__name__
是"__main__.py"
。
同時,如果我只是執行“python -m omission”,“app.py”中的特殊程式碼將被忽略,因為它的“name”現在又是“omission.app”。
看看效果如何?
我們來複習。
每個專案都應該使用 VCS,例如 Git。有很多選項可供選擇。
每個 Python 程式碼檔案 (.py
) 都是一個 模組。
將您的模組組織到包中。每個包必須包含一個特殊的「init.py」檔案。
您的專案通常應由一個頂級包組成,通常包含子包。該頂級包通常共享您的專案的名稱,並作為專案存儲庫根目錄中的目錄存在。
永遠不要在導入語句中使用「*」。在考慮可能的例外之前,Python 之禪指出「特殊情況並沒有特殊到足以違反規則」。
使用絕對或相對導入來引用專案中的其他模組。
可執行專案的頂層套件中應該有一個__main__.py
。然後,您可以使用「python -m myproject」直接執行該套件。
當然,我們可以在建立 Python 專案時使用許多更高級的概念和技巧,但我們不會在這裡討論。我強烈建議閱讀文件:
感謝 grym
、deniska
(Freenode IRC #python
)、@cbrintnall 和 @rhymes (Dev) 提出的修改建議。
原文出處:https://dev.to/codemouse92/dead-simple-python-project-structure-and-imports-38c6