Python中import的用法陷阱解決盤點(diǎn)小結(jié)
引言
Python用了快兩年了吧,其中有些東西一直是稀里糊涂地用,import便是我一直沒有明白的東西。曾經(jīng)有過三次解決它的機(jī)會(huì),我都因得過且過、一拖再拖而沒能化敵為友。今天下午,它又給了我一次機(jī)會(huì),我想我還是從了它的心愿吧。
故事是從這篇博客(Python 的 Import 陷阱)開始的,然后又跳到了Python社區(qū)的PEP 328提案(PEP 328 -- Imports: Multi-Line and Absolute/Relative),再結(jié)合過去的經(jīng)驗(yàn)以及一些測試,我想我大概懂了吧。下面是我的總結(jié),希望內(nèi)容能夠言簡意賅、易于理解。
import語句有什么用?
import語句用來導(dǎo)入其他python文件(稱為模塊module),使用該模塊里定義的類、方法或者變量,從而達(dá)到代碼復(fù)用的目的。為了方便說明,我們用實(shí)例來說明import的用法,讀者朋友可以跟著嘗試(嘗試時(shí)建議使用python3,python2和python3在import的表現(xiàn)有差異,之后會(huì)提到)。
將要建立文件的結(jié)構(gòu)為:
Tree |____ m1.py |____ m2.py |____ Branch |____m3.py |____m4.py
首先,先建立一個(gè)文件夾Tree作為工作目錄,并在其內(nèi)建立兩個(gè)文件m1.py和m2.py,在m1.py寫入代碼:
import os import m2 m2.printSelf()
在m2.py寫入代碼:
def printSelf(): print('In m2')
打開命令行,進(jìn)入到Tree目錄下,敲下python m1.py
運(yùn)行,發(fā)現(xiàn)沒有報(bào)錯(cuò),且打印出In m2
,說明這樣使用import沒有問題。由此我們總結(jié)出import語句的第一種用法。
import module_name
。即import后直接接模塊名。在這種情況下,Python會(huì)在兩個(gè)地方尋找這個(gè)模塊- 第一是sys.path(通過運(yùn)行代碼
import sys; print(sys.path)
查看),os這個(gè)模塊所在的目錄就在列表sys.path中,一般安裝的Python庫的目錄都可以在sys.path中找到(前提是要將Python的安裝目錄添加到電腦的環(huán)境變量),所以對于安裝好的庫,我們直接import即可。 - 第二個(gè)地方就是運(yùn)行文件(這里是m1.py)所在的目錄,因?yàn)閙2.py和運(yùn)行文件在同一目錄下,所以上述寫法沒有問題。
用上述方法導(dǎo)入原有的sys.path中的庫沒有問題。但是,最好不要用上述方法導(dǎo)入同目錄下的文件!因?yàn)檫@可能會(huì)出錯(cuò)。演示這個(gè)錯(cuò)誤需要用到import語句的第二種寫法,所以先來學(xué)一學(xué)import的第二種寫法。在Tree目錄下新建一個(gè)目錄Branch,在Branch中新建文件m3.py,m3.py的內(nèi)容如下:
def printSelf(): print('In m3')
如何在m1中導(dǎo)入m3.py呢,請看更改后的m1.py:
from Branch import m3 m3.printSelf()
總結(jié)import語句用法
from package_name import module_name
。一般把模塊組成的集合稱為包(package)。與第一種寫法類似,Python會(huì)在sys.path和運(yùn)行文件目錄這兩個(gè)地方尋找包,然后導(dǎo)入包中名為module_name的模塊。
現(xiàn)在我們來說明為什么不要用import的第一種寫法來導(dǎo)入同目錄下的文件。在Branch目錄下新建m4.py文件,m4.py的內(nèi)容如下:
def printSelf(): print('In m4')
然后我們在m3.py中直接導(dǎo)入m4,m3.py變?yōu)椋?/p>
import m4 def printSelf(): print('In m3')
這時(shí)候運(yùn)行m1.py就會(huì)報(bào)錯(cuò)了,說沒法導(dǎo)入m4模塊。為什么呢?我們來看一下導(dǎo)入流程:m1使用from Branch import m3
導(dǎo)入m3,然后在m3.py中用import m4
導(dǎo)入m4。看出問題了嗎?m4.py和m1.py不在同一目錄,怎么能直接使用import m4
導(dǎo)入m4呢。(讀者可以試試直接在Tree目錄下新建另一個(gè)m4.py文件,你會(huì)發(fā)現(xiàn)再運(yùn)行m1.py就不會(huì)出錯(cuò)了,只不過導(dǎo)入的是第二個(gè)m4.py了)
面對上面的錯(cuò)誤,使用python2運(yùn)行m1.py就不會(huì)報(bào)錯(cuò),因?yàn)樵趐ython2中,上面提到的import的兩種寫法都屬于相對導(dǎo)入,而在python3中,卻屬于絕對導(dǎo)入。話說到了這里,就要牽扯到import中最關(guān)鍵的部分了——相對導(dǎo)入和絕對導(dǎo)入。
我們還是談?wù)損ython3的import用法。上面提到的兩種寫法屬于絕對導(dǎo)入,即用于導(dǎo)入sys.path中的包和運(yùn)行文件所在目錄下的包。對于sys.path中的包,這種寫法毫無問題;導(dǎo)入自己寫的文件,如果是非運(yùn)行入口文件(上面的m1.py是運(yùn)行入口文件,可以使用絕對導(dǎo)入),則需要相對導(dǎo)入。
比如對于非運(yùn)行入口文件m3.py,其導(dǎo)入m4.py需要使用相對導(dǎo)入:
from . import m4 def printSelf(): print('In m3')
這時(shí)候再運(yùn)行m1.py就ok了。列舉一下相對導(dǎo)入的寫法:
from . import module_name
。導(dǎo)入和自己同目錄下的模塊。from .package_name import module_name
。導(dǎo)入和自己同目錄的包的模塊。from .. import module_name
。導(dǎo)入上級目錄的模塊。from ..package_name import module_name
。導(dǎo)入位于上級目錄下的包的模塊。- 當(dāng)然還可以有更多的
.
,每多一個(gè)點(diǎn)就多往上一層目錄。
不知道你有沒有留神上面的一句話——“上面的m1.py是運(yùn)行入口文件,可以使用絕對導(dǎo)入”,這句話是沒問題的,也和我平時(shí)的做法一致。那么,運(yùn)行入口文件可不可以使用相對導(dǎo)入呢?比如m1.py內(nèi)容改成:
from .Branch import m3 m3.printSelf()
答案是可以,但不能用python m1.py
命令,而是需要進(jìn)入到Tree所在的目錄,使用python -m Tree.m1
來運(yùn)行。為什么?關(guān)于前者,PEP 328提案中的一段文字好像給出了原因:
Relative imports use a module's _name_ attribute to determine that module's position in the package hierarchy. If the module's name does not contain any package information (e.g. it is set to '__main__') then relative imports are resolved as if the module were a top level module, regardless of where the module is actually located on the file system.
我不太懂,但是又有一點(diǎn)明白。我們應(yīng)該見過下面一段代碼:
if __name__ == '__main__': main()
意思是如果運(yùn)行了當(dāng)前文件,則__name__變量會(huì)置為__main__,然后會(huì)執(zhí)行main函數(shù),如果當(dāng)前文件是被其他文件作為模塊導(dǎo)入的話,則__name__為模塊名,不等于__main__,就不會(huì)執(zhí)行main函數(shù)。比如對于上述更改后的m1.py,執(zhí)行python m1.py
命令后,會(huì)報(bào)如下錯(cuò)誤:
Traceback (most recent call last): File "m1.py", line 1, in from .Branch import m3 ModuleNotFoundError: No module named '_main_.Branch'; '__main__' is not a package
據(jù)此我猜測執(zhí)行python m1.py
命令后,當(dāng)前目錄所代表的包'.'變成了__main__。
那為什么python -m Tree.m1
就可以呢?那位臺灣老師給出了解釋:
執(zhí)行指令中的-m是為了讓Python預(yù)先import你要的package或module給你,然后再執(zhí)行script。
即不把m1.py當(dāng)作運(yùn)行入口文件,而是也把它當(dāng)作被導(dǎo)入的模塊,這就和非運(yùn)行入口文件有一樣的表現(xiàn)了。
注意,在Tree目錄下運(yùn)行python -m m1
是不可以的,會(huì)報(bào) ImportError: attempted relative import with no known parent package的錯(cuò)誤。因?yàn)閙1.py中的from .Branch import m3
中的.
,解釋器并不知道是哪一個(gè)package。使用python -m Tree.m1
,解釋器就知道.
對應(yīng)的是Tree這個(gè)package。
那反過來,如果m1.py使用絕對導(dǎo)入(from Branch import m3
),能使用python -m m1
運(yùn)行嗎?我試了一下,如果當(dāng)前目錄是Tree就可以。如果在其他目錄下運(yùn)行,比如在Tree所在的目錄(使用python -m Tree.m1
運(yùn)行),就不可以。這可能還是與絕對導(dǎo)入相關(guān)。
(之前看到了一個(gè)大型項(xiàng)目,其運(yùn)行入口文件有一大堆的相對導(dǎo)入,我還傻乎乎地用python直接運(yùn)行它。之后看到他給的樣例運(yùn)行命令是帶了-m參數(shù)的?,F(xiàn)在才恍然大悟。)
理解import的難點(diǎn)差不多就這樣了。下面說一說import的其他簡單但實(shí)用的用法。
import moudle_name as alias
。有些module_name比較長,之后寫它時(shí)較為麻煩,或者module_name會(huì)出現(xiàn)名字沖突,可以用as來給它改名,如import numpy as np
。from module_name import function_name, variable_name, class_name
。上面導(dǎo)入的都是整個(gè)模塊,有時(shí)候我們只想使用模塊中的某些函數(shù)、某些變量、某些類,用這種寫法就可以了。使用逗號可以導(dǎo)入模塊中的多個(gè)元素。- 有時(shí)候?qū)氲脑睾芏?,可以使用反斜杠來換行,官方推薦使用括號。
from Tkinter import Tk, Frame, Button, Entry, Canvas, Text, \ LEFT, DISABLED, NORMAL, RIDGE, END # 反斜杠換行 from Tkinter import (Tk, Frame, Button, Entry, Canvas, Text, LEFT, DISABLED, NORMAL, RIDGE, END) # 括號換行(推薦)
說到這感覺import的核心已經(jīng)說完了。再跟著上面的博客說一說使用import可能碰到的問題吧。
import可能碰到的問題
問題1描述:ValueError: attempted relative import beyond top-level package。直面問題的第一步是去了解熟悉它,最好是能復(fù)現(xiàn)它,將錯(cuò)誤暴露在陽光之下。仍然是上面四個(gè)文件,稍作修改,四個(gè)文件如下:
# m1.py from Branch import m3 m3.printSelf() # m2.py def printSelf(): print('module2') # m3.py from .. import m2 # 復(fù)現(xiàn)的關(guān)鍵在這 # print(__name__) def printSelf(): print('In m3') # m4.py def printSelf(): print('In m4')
運(yùn)行python m1.py
,就會(huì)出現(xiàn)該問題。問題何在?我猜測,運(yùn)行m1.py后,m1代表的模塊就是頂層模塊(參見上面PEP 328的引用),而m3.py中嘗試導(dǎo)入的m2模塊所在的包(即Tree目錄代表的包)比m1的層級更高,所以會(huì)報(bào)出這樣的錯(cuò)誤。怎么解決呢?將m1.py的所有導(dǎo)入改為相對導(dǎo)入,然后進(jìn)入m1.py的上層目錄,運(yùn)行python -m Tree.m1
即可。
對于使用import出現(xiàn)的其他問題,碰到了再接著更新,更多關(guān)于Python import用法的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python通過函數(shù)名調(diào)用函數(shù)的幾種場景
這篇文章主要介紹了python通過函數(shù)名調(diào)用函數(shù)的幾種場景,幫助大家更好的理解和使用python,感興趣的朋友可以了解下2020-09-09Python 3.x基礎(chǔ)實(shí)戰(zhàn)檢查磁盤可用空間
這篇文章主要為大家介紹了Python 3.x基礎(chǔ)實(shí)戰(zhàn)之檢查磁盤可用空間實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05Python私有pypi源注冊自定義依賴包Windows詳解
這篇文章主要介紹了Python私有pypi源注冊自定義依賴包Windows,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11Python 實(shí)現(xiàn)Serial 與STM32J進(jìn)行串口通訊
今天小編就為大家分享一篇Python 實(shí)現(xiàn)Serial 與STM32J進(jìn)行串口通訊,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-12-12Django項(xiàng)目中動(dòng)態(tài)設(shè)置靜態(tài)文件路徑的全過程
這篇文章主要給大家介紹了關(guān)于Django項(xiàng)目中動(dòng)態(tài)設(shè)置靜態(tài)文件路徑的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-02-02