對不少 Python 初學者來說,Python 導入其他模塊的方式讓他們很難理解。什么時候用import xxx
?什么時候用from xxx import yyy
?什么時候用from xxx.yyy import zzz
?什么時候用from xxx import *
?
這篇文章,我們來徹底搞懂這個問題。
系統自帶的模塊
以正則表達式模塊為例,我們經常這樣寫代碼:
1
2
3
4
|
import re
target =
'abc1234xyz'
re.search(
'(\d+)', target)
|
但有時候,你可能會看到某些人這樣寫代碼:
1
2
3
|
from re import search
target =
'abc1234xyz'
search(
'(\d+)', target)
|
那么這兩種導入方式有什么區別呢?
我們分別使用type
函數來看看他們的類型:
1
2
3
4
5
6
|
>>> import re
>>> type(re)
<class 'module'>
>>> from re import search
>>> type(search)
<class 'function'>
|
如下圖所示:
可以看到,直接使用import re
導入的re
它是一個module
類,也就是模塊。我們把它成為正則表達式模塊
。而當我們from re import search
時,這個search
是一個function
類,我們稱呼它為search 函數
。
一個模塊里面可以包含多個函數。
如果在你的代碼里面,你已經確定只使用search
函數,不會再使用正則表達式里面的其他函數了,那么你使用兩種方法都可以,沒什么區別。
但是,如果你要使用正則表達式下面的多個函數,或者是一些常量,那么用第一種方案會更加簡潔清晰。
例如:
1
2
3
4
|
import re
re.search(
'c(.*?)x', flags=re.S)
re.sub(
'[a-zA-Z0-9]', '***', target, flags=re.I)
|
在這個例子中,你分別使用了re.search
,re.sub
,re.S
和re.I
。后兩者是常量,用於忽略換行符和大小寫。
但是,如果你使用from re import search, sub, S, I
來寫代碼,那么代碼就會變成這樣:
1
2
3
4
|
import re
search(
'c(.*?)x', flags=S)
sub(
'[a-zA-Z0-9]', '***', target, flags=I)
|
看起來雖然簡潔了,但是,一旦你的代碼行數多了以后,你很容易忘記S
和I
這兩個變量是什么東西。而且我們自己定義的函數,也很有可能取名為sub
或者search
,從而覆蓋正則表達式模塊下面的這兩個同名函數。這就會導致很多難以覺察的潛在 bug。
再舉一個例子。Python 的 datetime
模塊,我們可以直接import datetime
,此時我們導入的是一個datetime
模塊,如下圖所示:
但是如果你寫為from datetime import datetime
,那么你導入的datetime
是一個type
類:
因為這種方式導入的datetime
,它就是Python 中的一種類型,用於表示包含日期和時間的數據。
這兩種導入方式導入的datetime
,雖然名字一樣,但是他們的意義完全不一樣,請大家觀察下面兩種寫法:
1
2
3
4
|
import datetime
now = datetime.datetime.now()
one_hour_ago = now - datetime.timedelta(hours=
1)
|
1
2
3
|
from datetime import datetime, timedelta
now = datetime.now()
one_hour_ago = now - timedelta(hours=
1)
|
第二種寫法看似簡單,但實則改動起來卻更為麻煩。例如我還需要增加一個變量today
用於記錄今日的日期。
對於第一段代碼,我們只需要增加一行即可:
1
|
today = datetime.date.today()
|
但對於第二行來說,我們需要首先修改導入部分的代碼:
1
|
from datetime import datetime, timedelta, date
|
然后才能改代碼:today = date.today()
這樣一來你就要修改兩個地方,反倒增加了負擔。
第三方庫
在使用某些第三方庫的代碼里面,我們會看到類似這樣的寫法:
1
2
3
|
from lxml.html import fromstring
selector = fromstring(HTML)
|
但是我們還可以寫為:
1
2
3
|
from lxml import html
selector = html.fromstring(HTML)
|
但是,下面這種寫法會導致報錯:
1
2
|
import lxml
selector = lxml.html.fromstring(HTML)
|
那么這里的lxml.html
又是什么東西呢?
這種情況多常見於一些特別大型的第三方庫中,這種庫能處理多種類型的數據。例如lxml
它既能處理xml
的數據,又能處理html
的數據,於是這種庫會划分子模塊,lxml.html
模塊專門負責html
相關的數據。
自己來實現多種導入方法
我們現在自己來寫代碼,實現這多種導入方法。
我們創建一個文件夾DocParser
,在里面分別創建兩個文件main.py
和util.py
,他們的內容如下:
util.py
文件:
1
2
|
def write():
print(
'write 函數被調用!')
|
main.py
文件:
1
2
3
|
import util
util.write()
|
運行效果如下圖所示:
現在我們把main.py
的導入方式修改一下:
1
2
3
|
from util import write
write()
|
依然正常運行,如下圖所示
當兩個文件在同一個文件夾下面,並且該文件夾里面沒有init.py 文件時,兩種導入方式等價。
現在,我們來創建一個文件夾microsoft
,里面再添加一個文件parse.py
:
1
2
|
def read():
print(
'我是 microsoft 文件夾下面的 parse.py 中的 read函數')
|
如下圖所示:
此時我們在 main.py
中對它進行調用:
1
2
3
|
from microsoft import parse
parse.read()
|
運行效果如下圖所示:
我們也可以用另一種方法:
1
2
3
|
from microsoft.parse import read
read()
|
運行效果如下圖所示:
但是,你不能直接導入microsoft
,如下圖所示:
你只能導入一個模塊或者導入一個函數或者類,你不能導入一個文件夾
無論你使用的是import xxx
還是from xxx.yyy.zzz.www import qqq
,你導入進來的東西,要不就是一個模塊(對應到.py 文件的文件名),或者是某個.py 文件中的函數名、類名、變量名。
無論是import xxx
還是from xxx import yyy
,你導入進來的都不能是一個文件夾的名字。
可能有這樣一種情況,就是某個函數名與文件的名字相同,例如:
在 microsoft
文件夾里面有一個microsoft.py
文件,這個文件里面有一個函數叫做microsoft
,那么你的代碼可以寫為:
1
2
|
from microsoft import microsoft`
microsoft.microsoft()
|
但請注意分辨,這里你導入的還是模塊,只不過microsoft.py
文件名與它所在的文件夾名恰好相同而已。
總結
無論是使用import
還是from import
,第一個要求是代碼能夠正常運行,其次,根據代碼維護性,團隊編碼風格來確定選擇哪一種方案。
如果我們只會使用到某個模塊下面的一個函數(或者常量、類)並且名字不會產生混淆,可識別性高,那么from 模塊名 import 函數名
這沒有什么問題。
如果我們會用到一個模塊下面的多個函數,或者是我們將要使用的函數名、常量名、類名可能會讓人產生混淆(例如 re.S、re.I),那么這種情況下,import 模塊名
然后再 模塊名.xxx
來調用會讓代碼更加清晰,更好維護。
但無論什么情況下,都禁止使用from xxx import *
這種寫法,它會給你帶來無窮無盡的噩夢。
未完待續
在明天的文章中,我們來講講還有一種寫法from . import xxx
,以及當文件夾中存在__init__.py
時,導入方式又有什么變化。
轉自:微信公眾號:未聞code