歡迎光臨
每天分享高質量文章

Python imports指南

來源:Python程式員

ID:pythonbuluo

宣告:如果你每天寫Python,你會發現這篇文章中沒有新東西。 這是專為那些像運維人員等偶爾使用Python的人以及那些忘記/誤用python import的人寫的。 儘管如此,代碼是用Python 3.6型別註釋編寫的,以滿足有經驗的Python讀者。 像往常一樣,如果你發現任何錯誤,請告訴我!

模塊

我們從一個常見的python代碼開始

if __name__ == '__main__':
   invoke_the_real_code()

很多人,我也不例外,把它當成固定格式,而不去深入理解它。 我們已經知道一點,當從CLI呼叫你的代碼而不是匯入它時,這個代碼片段會有所不同。 現在讓我們試著去理解我們為什麼需要用它。

為了說明,假設我們正在編寫一款披薩店軟體。 原始碼在Github上。 這是pizza.py檔案。


# pizza.py file

import math

class Pizza:
   name: str = ''
   size: int = 0
   price: float = 0

   def __init__(self, name: str, size: int, price: float) -> None:
       self.name = name
       self.size = size
       self.price = price

   def area(self) -> float:
       return math.pi * math.pow(self.size / 2, 2)

   def awesomeness(self) -> int:
       if self.name == 'Carbonara':
           return 9000

       return self.size // int(self.price) * 100

print('pizza.py module name is %s' % __name__)
if __name__ == '__main__':
   print('Carbonara is the most awesome pizza.')

我已經添加了打印__name__變數的代碼,以便瞭解__name__是如何變化的。

$ python3 pizza.py
pizza.py module name is __main__
Carbonara is the most awesome pizza.

的確,全域性變數__name__在從CLI呼叫的時候設置成了“__main__”。

可是如果從另外一個檔案中取用它會怎麼樣呢?以下是menu.py的原始碼:

# menu.py file

from typing import List
from pizza import Pizza

MENU: List[Pizza] = [
   Pizza('Margherita', 30, 10.0),
   Pizza('Carbonara', 45, 14.99),
   Pizza('Marinara', 35, 16.99),
]

if __name__ == '__main__':
   print(MENU)

運行menu.py

$ python3 menu.py
pizza.py module name is pizza
[0

x7fbbc1045470>, 0x7fbbc10454e0>, 0x7fbbc1045b38>]

接著看看下麵兩點:

  1. pizza.py代碼中的第一條打印陳述句在import的時候執行了。

  2. pizza.py代碼中的全域性變數__name__設置成了沒有.py後綴的檔案名。

所以,事實是,__name__是儲存當前Python模塊名稱的全域性變數。

  • 模塊名稱由解釋器在__name__變數中設置

  • 當從CLI呼叫模塊時,其名稱被設置為__main__

那麼到底什麼是模塊呢? 這非常簡單 – 模塊是一個包含Python代碼的檔案,可以使用解釋器(python程式)執行或從其他模塊匯入。

  • Python模塊只是一個包含Python代碼的檔案

就像執行時一樣,當模塊被匯入時,它的頂級陳述句也會被執行,但是要知道,即使從不同的檔案中匯入它幾次,它也只會被執行一次。

  • 當你匯入模塊時,它會被執行

因為模塊只是純檔案,所以有一個簡單的方法來匯入它們。 只取檔案名,去掉.py擴展名並將其放入import陳述句中。

  • 要匯入模塊,請使用不帶.py擴展名的檔案名

有趣的是,__name__被設置為檔案名,無論你如何匯入它 – 例如import pizza as broccoli,__name__仍然是pizza。 所以

  • 匯入時,即使使用import module as othername將模塊名稱重命名,模塊名稱仍然設置為不帶.py擴展名的檔案名

但是如果匯入的模塊不在同一個目錄下,我們怎麼匯入呢? 答案是放在模塊搜索路徑中,我們最終會在討論包時研究它。

  • 包是模塊集合的名稱空間

命名空間部分很重要,因為它本身並不提供任何功能 – 它只是給你一個組合你所有模塊的方式。

兩種情況下,你需要把模塊放入一個包中。 首先是隔離一個模塊的定義。 在我們的pizza模塊中,我們有一個可能與其他Pizza包相衝突的Pizza類(我們在pypi上有一些pizza包)

第二種情況是,如果你想分發你的代碼,因為

  • 包是Python中最小的代碼分發單元

你在PyPI上看到的所有東西都是通過pip安裝的,所以為了分享你的東西,你必須把它做成一個包。

好吧,假設我們確信並想將我們的2個模塊轉換成一個很好的包。 要做到這一點,我們需要創建一個包含一個空的__init__.py檔案的目錄,並將我們的檔案移入該目錄:

pizzapy/
├── __init__.py
├── menu.py
└── pizza.py

就是這樣 – 現在你有一個比薩餅包!

  • 要創建一個包,創建一個包含__init__.py檔案的目錄

請記住,程式包是模塊的名稱空間,因此您不會匯入包本身,而是從包中匯入模塊。

>>> import pizzapy.menu
pizza.py module name is pizza
>>> pizzapy.menu.MENU
[0x7fa065291160

>, 0x7fa065291198>, 0x7fa065291a20>]

如果以這種方式進行匯入,則可能看起來過於冗長,因為您需要使用完全限定的名稱。 我猜這是有意為之,因為Python宗旨之一是“明確比隱含更好”。

無論如何,你總是可以使用from package import module的格式來縮短名稱:

>>> from pizzapy import menu
pizza.py module name is pizza
>>> menu.MENU
[0x7fa065291160

>, 0x7fa065291198>, 0x7fa065291a20>]

包初始化

還記得我們如何把一個__init__.py檔案放在一個目錄中,這個目錄就神奇地變成了一個包嗎?這是一個很好的慣例配置示例,我們不需要描述任何配置或註冊任何東西。約定包含__init__.py的任何目錄都是Python包。

除了標識一個包,__init__.py還有一個目的 – 包初始化。這就是為什麼它被稱為init!初始化是在包匯入時觸發的,換句話說,匯入包時呼叫__init__.py

  • 當你匯入一個包時,包內的__init__.py模塊被執行

在__init__模塊中,你可以做任何你想做的事情,但最常用的是用於一些包初始化或設置專用的__all__變數。後者控制*(通配符)匯入 – from package import *。

而且因為Python很棒,我們可以在__init__模塊中做很多事情,甚至是很奇怪的事情。假設我們不喜歡顯式匯入,並且希望將所有模塊符號上升到包級別,這樣我們就不必記住實際的模塊名稱。

為此,我們可以在__init__.py中像這樣匯入menu和pizza模塊中的所有東西

# pizzapy/__init__.py

from pizzapy.pizza import *
from pizzapy.menu import *

看看運行結果:

>>> import pizzapy
pizza.py module name is pizzapy.pizza
pizza.py module name is pizza
>>> pizzapy.MENU
[0x7f1bf03b8828

>, 0x7f1bf03b8860>, 0x7f1bf03b8908>]

沒有更多的pizzapy.menu.Menu或menu.MENU :-)這種方式有點像Go中的軟體包,但請註意,你正試圖濫用Python,不鼓勵這樣做,因為在你要代碼檢查時,會讓你抓狂的。 別怪我哦,我只是為了舉例說明!

您可以像這樣更簡潔地重寫匯入

# pizzapy/__init__.py

from .pizza import *
from .menu import *

這隻是另一種做同樣事情的語法,就是所謂的相對匯入。 我們來仔細看看。

絕對和相對匯入

上面的2個代碼段是做所謂的相對匯入的唯一方法,因為自Python 3開始,所有匯入都預設為絕對匯入(如在PEP328中),這意味著匯入將嘗試首先匯入標準模塊,然後才匯入本地包。 在創建自己的sys.py模塊時,需要避免使用標準模塊的名稱,因為import sys可以改寫標準庫sys模塊。

  • 自Python 3開始,所有匯入都預設為絕對匯入 – 它將首先查找系統包

但是如果你的軟體包有一個名為sys的模塊,並且你想把它匯入到同一個包內的另一個模塊中,你必須做相對的匯入。 要做到這一點,你必須再次明確的這樣寫package.module import somesymbol或from .module import somesymbol。 模塊名稱之前的那個有趣的點理解為“當前包”。

  • 要進行相對匯入,請在模塊名前加上程式包名稱或點

可執行程式包

在Python中,您可以使用python3 -m 構造呼叫模塊。

$ python3 -m pizza
pizza.py module name is __main__
Carbonara is the most awesome pizza.

然而也可以這樣呼叫:

$ python3 -m pizzapy
/usr/bin/python3: No module named pizzapy.__main__; 'pizzapy' is a package and cannot be directly executed

如你所看到的,這需要一個__main__模塊,因此要先實現它:

# pizzapy/__main__.py

from pizzapy.menu import MENU

print('Awesomeness of pizzas:')
for pizza in MENU:
   print(pizza.name, pizza.awesomeness())

現在可以正常使用了:

$ python3 -m pizzapy
pizza.py module name is pizza
Awesomeness of pizzas:
Margherita 300
Carbonara 9000
Marinara 200

  • 添加__main__.py使包可執行(使用python3 -m package呼叫它)

匯入兄弟包

而我想要涵蓋的最後一件事是匯入兄弟包。 假設我們有一個兄弟包pizzashop:

.
├── pizzapy
│   ├── __init__.py
│   ├── __main__.py
│   ├── menu.py
│   └── pizza.py
└── pizzashop
   ├── __init__.py
   └── shop.py

# pizzashop/shop.py

import pizzapy.menu
print(pizzapy.menu.MENU)


現在,位於頂層目錄下,如果我們試圖像這樣呼叫shop.py


$ python3 pizzashop/shop.py
Traceback (most recent call last):
 File "pizzashop/shop.py", line 1, in <module>
   import pizzapy.menu
ModuleNotFoundError: No module named 'pizzapy'


我們得到了找不到pizzapy模塊的錯誤。 但是,如果我們把它作為包的一部分來呼叫它

$ python3 -m pizzashop.shop
pizza.py module name is pizza
[0

x7f372b59ccc0>, 0x7f372b59ccf8>, 0x7f372b59cda0>]

它能正常工作了。 這到底是怎麼回事?

對此的解釋原因在於Python模塊的搜索路徑,在模塊文件中有很詳細的描述。

模塊搜索路徑是解釋器用於查找模塊的目錄(在運行時可用sys.path得到)的串列。 它通過Python標準模塊(/usr/lib64/python3.6)的路徑進行初始化,site-packages是pip放置全域性安裝的所有內容的地方,也是一個依賴如何運行模塊的目錄。 如果將模塊像這樣python3 pizzashop/shop.py作為一個檔案運行,則將包含目錄(pizzashop)的路徑添加到sys.path中。 另外,使用-m選項運行時,當前目錄(如在pwd中)被添加到模塊搜索路徑。 我們可以通過在pizzashop/shop.py中打印sys.path來檢查它:

$ pwd
/home/avd/dev/python-imports

$ tree
.
├── pizzapy
│   ├── __init__.py
│   ├── __main__.py
│   ├── menu.py
│   └── pizza.py
└── pizzashop
   ├── __init__.py
   └── shop.py

$ python3 pizzashop/shop.py
['/home/avd/dev/python-imports/pizzashop',
'/usr/lib64/python36.zip',
'/usr/lib64/python3.6',
'/usr/lib64/python3.6/lib-dynload',
'/usr/local/lib64/python3.6/site-packages',
'/usr/local/lib/python3.6/site-packages',
'/usr/lib64/python3.6/site-packages',
'/usr/lib/python3.6/site-packages']
Traceback (most recent call last):
 File "pizzashop/shop.py", line 5, in
   import pizzapy.menu
ModuleNotFoundError: No module named 'pizzapy'

$ python3 -m pizzashop.shop
['',
'/usr/lib64/python36.zip',
'/usr/lib64/python3.6',
'/usr/lib64/python3.6/lib-dynload',
'/usr/local/lib64/python3.6/site-packages',
'/usr/local/lib/python3.6/site-packages',
'/usr/lib64/python3.6/site-packages',
'/usr/lib/python3.6/site-packages']
pizza.py module name is pizza
[0

x7f2f75747f28>, 0x7f2f75747f60>, 0x7f2f75747fd0>]

正如你在第一種情況中可以看到的,我們在路徑中有pizzashop dir,所以我們找不到兄弟包pizzapy,而在第二種情況下,當前dir(表示為””)在sys.path中並且包含兩個包。

  • Python的模塊搜索路徑在運行時可作為sys.path

  • 如果將模塊作為腳本檔案運行,則將包含該模塊的目錄添加到sys.path中,否則,會將當前目錄添加到sys.path中

當人們將一堆測試或示例腳本放在主包相鄰的目錄或包中時,常常會出現匯入同級包的問題。 這裡有幾個StackOverflow問題:

https://stackoverflow.com/q/6323860

https://stackoverflow.com/q/6670275

好的解決方案是把測試或例子放在包里,然後使用相對的匯入來避免這個問題。 差點的解決方案是在運行時修改sys.path,增加所需包的父目錄(耶,動態!)。 人們實際上這樣做,雖然這是一個糟糕的方式。

結束語

我希望閱讀這篇文章之後,你將會對Python的匯入有更好的理解,並且可以最終順利地將你工具箱中的巨大腳本分解成多個部分。最後,Python中的所有東西都非常簡單,即使它不能完整地滿足你的需求,你總可以在運行時隨時修改任何內容。

目前想寫的就這些,謝謝你的關註。 接下來如何,下次分解!

英文原文:https://alex.dzyoba.com/blog/python-import/  
 譯者:javylee  

《Python人工智慧和全棧開發》2018年07月23日即將在北京開課,120天衝擊Python年薪30萬,改變速約~~~~

*宣告:推送內容及圖片來源於網絡,部分內容會有所改動,版權歸原作者所有,如來源信息有誤或侵犯權益,請聯繫我們刪除或授權事宜。

– END –


更多Python好文請點擊【閱讀原文】哦

↓↓↓

赞(0)

分享創造快樂