Python中import的用法陷阱解決盤點小結(jié)
引言
Python用了快兩年了吧,其中有些東西一直是稀里糊涂地用,import便是我一直沒有明白的東西。曾經(jīng)有過三次解決它的機會,我都因得過且過、一拖再拖而沒能化敵為友。今天下午,它又給了我一次機會,我想我還是從了它的心愿吧。
故事是從這篇博客(Python 的 Import 陷阱)開始的,然后又跳到了Python社區(qū)的PEP 328提案(PEP 328 -- Imports: Multi-Line and Absolute/Relative),再結(jié)合過去的經(jīng)驗以及一些測試,我想我大概懂了吧。下面是我的總結(jié),希望內(nèi)容能夠言簡意賅、易于理解。
import語句有什么用?
import語句用來導入其他python文件(稱為模塊module),使用該模塊里定義的類、方法或者變量,從而達到代碼復用的目的。為了方便說明,我們用實例來說明import的用法,讀者朋友可以跟著嘗試(嘗試時建議使用python3,python2和python3在import的表現(xiàn)有差異,之后會提到)。
將要建立文件的結(jié)構(gòu)為:
Tree |____ m1.py |____ m2.py |____ Branch |____m3.py |____m4.py
首先,先建立一個文件夾Tree作為工作目錄,并在其內(nèi)建立兩個文件m1.py和m2.py,在m1.py寫入代碼:
import os import m2 m2.printSelf()
在m2.py寫入代碼:
def printSelf(): print('In m2')
打開命令行,進入到Tree目錄下,敲下python m1.py
運行,發(fā)現(xiàn)沒有報錯,且打印出In m2
,說明這樣使用import沒有問題。由此我們總結(jié)出import語句的第一種用法。
import module_name
。即import后直接接模塊名。在這種情況下,Python會在兩個地方尋找這個模塊- 第一是sys.path(通過運行代碼
import sys; print(sys.path)
查看),os這個模塊所在的目錄就在列表sys.path中,一般安裝的Python庫的目錄都可以在sys.path中找到(前提是要將Python的安裝目錄添加到電腦的環(huán)境變量),所以對于安裝好的庫,我們直接import即可。 - 第二個地方就是運行文件(這里是m1.py)所在的目錄,因為m2.py和運行文件在同一目錄下,所以上述寫法沒有問題。
用上述方法導入原有的sys.path中的庫沒有問題。但是,最好不要用上述方法導入同目錄下的文件!因為這可能會出錯。演示這個錯誤需要用到import語句的第二種寫法,所以先來學一學import的第二種寫法。在Tree目錄下新建一個目錄Branch,在Branch中新建文件m3.py,m3.py的內(nèi)容如下:
def printSelf(): print('In m3')
如何在m1中導入m3.py呢,請看更改后的m1.py:
from Branch import m3 m3.printSelf()
總結(jié)import語句用法
from package_name import module_name
。一般把模塊組成的集合稱為包(package)。與第一種寫法類似,Python會在sys.path和運行文件目錄這兩個地方尋找包,然后導入包中名為module_name的模塊。
現(xiàn)在我們來說明為什么不要用import的第一種寫法來導入同目錄下的文件。在Branch目錄下新建m4.py文件,m4.py的內(nèi)容如下:
def printSelf(): print('In m4')
然后我們在m3.py中直接導入m4,m3.py變?yōu)椋?/p>
import m4 def printSelf(): print('In m3')
這時候運行m1.py就會報錯了,說沒法導入m4模塊。為什么呢?我們來看一下導入流程:m1使用from Branch import m3
導入m3,然后在m3.py中用import m4
導入m4??闯鰡栴}了嗎?m4.py和m1.py不在同一目錄,怎么能直接使用import m4
導入m4呢。(讀者可以試試直接在Tree目錄下新建另一個m4.py文件,你會發(fā)現(xiàn)再運行m1.py就不會出錯了,只不過導入的是第二個m4.py了)
面對上面的錯誤,使用python2運行m1.py就不會報錯,因為在python2中,上面提到的import的兩種寫法都屬于相對導入,而在python3中,卻屬于絕對導入。話說到了這里,就要牽扯到import中最關鍵的部分了——相對導入和絕對導入。
我們還是談論python3的import用法。上面提到的兩種寫法屬于絕對導入,即用于導入sys.path中的包和運行文件所在目錄下的包。對于sys.path中的包,這種寫法毫無問題;導入自己寫的文件,如果是非運行入口文件(上面的m1.py是運行入口文件,可以使用絕對導入),則需要相對導入。
比如對于非運行入口文件m3.py,其導入m4.py需要使用相對導入:
from . import m4 def printSelf(): print('In m3')
這時候再運行m1.py就ok了。列舉一下相對導入的寫法:
from . import module_name
。導入和自己同目錄下的模塊。from .package_name import module_name
。導入和自己同目錄的包的模塊。from .. import module_name
。導入上級目錄的模塊。from ..package_name import module_name
。導入位于上級目錄下的包的模塊。- 當然還可以有更多的
.
,每多一個點就多往上一層目錄。
不知道你有沒有留神上面的一句話——“上面的m1.py是運行入口文件,可以使用絕對導入”,這句話是沒問題的,也和我平時的做法一致。那么,運行入口文件可不可以使用相對導入呢?比如m1.py內(nèi)容改成:
from .Branch import m3 m3.printSelf()
答案是可以,但不能用python m1.py
命令,而是需要進入到Tree所在的目錄,使用python -m Tree.m1
來運行。為什么?關于前者,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.
我不太懂,但是又有一點明白。我們應該見過下面一段代碼:
if __name__ == '__main__': main()
意思是如果運行了當前文件,則__name__變量會置為__main__,然后會執(zhí)行main函數(shù),如果當前文件是被其他文件作為模塊導入的話,則__name__為模塊名,不等于__main__,就不會執(zhí)行main函數(shù)。比如對于上述更改后的m1.py,執(zhí)行python m1.py
命令后,會報如下錯誤:
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
命令后,當前目錄所代表的包'.'變成了__main__。
那為什么python -m Tree.m1
就可以呢?那位臺灣老師給出了解釋:
執(zhí)行指令中的-m是為了讓Python預先import你要的package或module給你,然后再執(zhí)行script。
即不把m1.py當作運行入口文件,而是也把它當作被導入的模塊,這就和非運行入口文件有一樣的表現(xiàn)了。
注意,在Tree目錄下運行python -m m1
是不可以的,會報 ImportError: attempted relative import with no known parent package的錯誤。因為m1.py中的from .Branch import m3
中的.
,解釋器并不知道是哪一個package。使用python -m Tree.m1
,解釋器就知道.
對應的是Tree這個package。
那反過來,如果m1.py使用絕對導入(from Branch import m3
),能使用python -m m1
運行嗎?我試了一下,如果當前目錄是Tree就可以。如果在其他目錄下運行,比如在Tree所在的目錄(使用python -m Tree.m1
運行),就不可以。這可能還是與絕對導入相關。
(之前看到了一個大型項目,其運行入口文件有一大堆的相對導入,我還傻乎乎地用python直接運行它。之后看到他給的樣例運行命令是帶了-m參數(shù)的?,F(xiàn)在才恍然大悟。)
理解import的難點差不多就這樣了。下面說一說import的其他簡單但實用的用法。
import moudle_name as alias
。有些module_name比較長,之后寫它時較為麻煩,或者module_name會出現(xiàn)名字沖突,可以用as來給它改名,如import numpy as np
。from module_name import function_name, variable_name, class_name
。上面導入的都是整個模塊,有時候我們只想使用模塊中的某些函數(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。直面問題的第一步是去了解熟悉它,最好是能復現(xiàn)它,將錯誤暴露在陽光之下。仍然是上面四個文件,稍作修改,四個文件如下:
# m1.py from Branch import m3 m3.printSelf() # m2.py def printSelf(): print('module2') # m3.py from .. import m2 # 復現(xiàn)的關鍵在這 # print(__name__) def printSelf(): print('In m3') # m4.py def printSelf(): print('In m4')
運行python m1.py
,就會出現(xiàn)該問題。問題何在?我猜測,運行m1.py后,m1代表的模塊就是頂層模塊(參見上面PEP 328的引用),而m3.py中嘗試導入的m2模塊所在的包(即Tree目錄代表的包)比m1的層級更高,所以會報出這樣的錯誤。怎么解決呢?將m1.py的所有導入改為相對導入,然后進入m1.py的上層目錄,運行python -m Tree.m1
即可。
對于使用import出現(xiàn)的其他問題,碰到了再接著更新,更多關于Python import用法的資料請關注腳本之家其它相關文章!
相關文章
python通過函數(shù)名調(diào)用函數(shù)的幾種場景
這篇文章主要介紹了python通過函數(shù)名調(diào)用函數(shù)的幾種場景,幫助大家更好的理解和使用python,感興趣的朋友可以了解下2020-09-09Python私有pypi源注冊自定義依賴包Windows詳解
這篇文章主要介紹了Python私有pypi源注冊自定義依賴包Windows,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11Python 實現(xiàn)Serial 與STM32J進行串口通訊
今天小編就為大家分享一篇Python 實現(xiàn)Serial 與STM32J進行串口通訊,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-12-12Django項目中動態(tài)設置靜態(tài)文件路徑的全過程
這篇文章主要給大家介紹了關于Django項目中動態(tài)設置靜態(tài)文件路徑的相關資料,文中通過圖文介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2022-02-02