Python中import的用法

Python用了快兩年了吧,其中有些東西一直是稀裏糊塗地用,import便是我一直沒有明白的東西。曾經有過三次解決它的機會,我都因得過且過、一拖再拖而沒能化敵爲友。今天下午,它又給了我一次機會,我想我還是從了它的心願吧。

故事是從這篇臺灣同胞的博客(Python 的 Import 陷阱)開始的,然後又跳到了Python社區的PEP 328提案(PEP 328 -- Imports: Multi-Line and Absolute/Relative),再結合過去的經驗以及一些測試,我想我大概懂了吧。下面是我的總結,希望內容能夠言簡意賅、易於理解。

import語句有什麼用?import語句用來導入其他python文件(稱爲模塊module),使用該模塊裏定義的類、方法或者變量,從而達到代碼複用的目的。爲了方便說明,我們用實例來說明import的用法,讀者朋友可以跟着嘗試(嘗試時建議使用python3,python2和python3在import的表現有差異,之後會提到)。

首先,先建立一個文件夾Tree作爲工作目錄,並在其內建立兩個文件m1.py和m2.py,在m1.py寫入代碼:

import os
import m2
m2.printSelf()

在m2.py寫入代碼:

def printSelf():
	print('In m2')

打開命令行,進入到Tree目錄下,敲下python m1.py運行,發現沒有報錯,且打印出In m2,說明這樣使用import沒有問題。由此我們總結出import語句的第一種用法。

  • import module_name。即import後直接接模塊名。在這種情況下,Python會在兩個地方尋找這個模塊,第一是sys.path(通過運行代碼import sys; print(sys.path)查看),os這個模塊所在的目錄就在列表sys.path中,一般安裝的Python庫的目錄都可以在sys.path中找到(前提是要將Python的安裝目錄添加到電腦的環境變量),所以對於安裝好的庫,我們直接import即可。第二個地方就是運行文件(這裏是m1.py)所在的目錄,因爲m2.py和運行文件在同一目錄下,所以上述寫法沒有問題。

用上述方法導入原有的sys.path中的庫沒有問題。但是,最好不要用上述方法導入同目錄下的文件!因爲這可能會出錯。演示這個錯誤需要用到import語句的第二種寫法,所以先來學一學import的第二種寫法。在Tree目錄下新建一個目錄Branch,在Branch中新建文件m3.py,m3.py的內容如下:

def printSelf():
	print('In m3')

如何在m1中導入m3.py呢,請看更改後的m1.py:

from Branch import m3
m3.printSelf()

總結import語句的第二種用法:

  • from package_name import module_name。一般把模塊組成的集合稱爲包(package)。與第一種寫法類似,Python會在sys.path和運行文件目錄這兩個地方尋找包,然後導入包中名爲module_name的模塊。

現在我們來說明爲什麼不要用import的第一種寫法來導入同目錄下的文件。在Branch目錄下新建m4.py文件,m4.py的內容如下:

def printSelf():
	print('In m4')

然後我們在m3.py中直接導入m4,m3.py變爲:

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文件,你會發現再運行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內容改成:

from .Branch import m3
m3.printSelf()

答案是可以,但不能用python m1.py命令,而是需要使用python -m 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__,然後會執行main函數,如果當前文件是被其他文件作爲模塊導入的話,則__name__爲模塊名,不等於__main__,就不會執行main函數。比如對於上述更改後的m1.py,執行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

據此我猜測執行python m1.py命令後,當前目錄所代表的包'.'變成了__main__。

那爲什麼python -m m1就可以呢?那位臺灣老師給出瞭解釋:

執行指令中的-m是爲了讓Python預先import你要的package或module給你,然後再執行script。

即不把m1.py當作運行入口文件,而是也把它當作被導入的模塊,這就和非運行入口文件有一樣的表現了。

那反過來,如果m1.py使用絕對導入(from Branch import m3),能使用python -m m1運行嗎?我試了一下,如果當前目錄是Tree就可以。如果在其他目錄下運行,比如在Tree所在的目錄(使用python -m Tree.m1運行),就不可以。這可能還是與絕對導入相關。

(之前看到了一個大型項目,其運行入口文件有一大堆的相對導入,我還傻乎乎地用python直接運行它。之後看到他給的樣例運行命令是帶了-m參數的。現在才恍然大悟。)

理解import的難點差不多就這樣了。下面說一說import的其他簡單但實用的用法。

  • import moudle_name as alias。有些module_name比較長,之後寫它時較爲麻煩,或者module_name會出現名字衝突,可以用as來給它改名,如import numpy as np
  • from module_name import function_name, variable_name, class_name。上面導入的都是整個模塊,有時候我們只想使用模塊中的某些函數、某些變量、某些類,用這種寫法就可以了。使用逗號可以導入模塊中的多個元素。
  • 有時候導入的元素很多,可以使用反斜槓來換行,官方推薦使用括號。
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的核心已經說完了。再跟着上面的博客說一說使用import可能碰到的問題吧。

問題1描述:ValueError: attempted relative import beyond top-level package。直面問題的第一步是去了解熟悉它,最好是能復現它,讓它躺在兩跨之間任我們去踐踏蹂躪。仍然是上面四個文件,稍作修改,四個文件如下:

# m1.py
from Branch import m3
m3.printSelf()
# m2.py
def printSelf():
	print('module2')
# m3.py
from .. import m2 # 復現的關鍵在這 #
print(__name__)
def printSelf():
	print('In m3')
# m4.py
def printSelf():
	print('In m4')

運行python m1.py,就會出現該問題。問題何在?我猜測,運行m1.py後,m1代表的模塊就是頂層模塊(參見上面PEP 328的引用),而m3.py中嘗試導入的m2模塊所在的包(即Tree目錄代表的包)比m1的層級更高,所以會報出這樣的錯誤。怎麼解決呢?將m1.py的所有導入改爲相對導入,然後進入m1.py的上層目錄,運行python -m Tree.m1即可。

對於使用import出現的其他問題,碰到了再接着更新。

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