一、概述
但凡稍微復雜一些的python程序,都不可避免地需要import一些內置模塊或外部模塊,當然也有可能import進來一個package,java語言中也經常見到各種import打頭,可見其用法很常見了。今天就來講述一下python中import代碼里面那些不為人知的故事。
二、模塊和包的概念
目前接觸到的python中的import,僅限於import模塊或import包,如果有其他對象可以被import,后續再另行補充。
- 模塊
用來從邏輯組織一段python代碼(變量,函數,類,方法),實現某個或某些特定的邏輯功能,本質上就是一個語法ok的.py結尾的python文件。如編寫了一個test.py文件,其對應的模塊名就是test(去掉后綴)
模塊可以分為以下幾類:
(1) 內置模塊
即標准庫,如常用的import os ,import sys等
(2) 開源模塊
這個就不贅述了,github上很多
(3) 自定義模塊
自己編寫的模塊文件,實際項目中這種情況應該不少。 - 包
用來從邏輯上組織模塊(.py文件)的,本質上是一個目錄,但必須包含名為__init__.py文件。
通過pycharm來new一個package時,可以看到實際上只是創建了一個僅僅包含名為__init__.py文件的目錄(當然你也可以在這個目錄下新建其它的.py文件/模塊)
三、import的本質
3.1 import的過程
明確了可以被import的對象(模塊和包)的概念后,我們由表及里,看一下import這兩種對象的實質:
- import模塊
由於模塊就是一個具體的.py文件,對應於一段python代碼,因此import一個模塊的本質是在當前.py文件下,解釋執行被import的模塊對應的.py文件(對應於一段python代碼)
import model_S (model_S=’model_S.py -----> all code’) 相當於把import的模塊名作為一個變量,而該變量對應於模塊名(.py文件)定義的全部python代碼。 - import包
package是一個目錄,至少應該包含一個名為__init__.py的文件。import 一個package的過程,本質是執行該包下的__init__.py文件。
3.2 import的路徑搜索
上述章節闡述了import一個模塊和一個包的過程,細心的同學會發現這里會引入另外一個問題,我們平時在執行一個python程序時,要么是在當前目錄下直接以python xx.py形式執行,要么是在某個目錄下,通過絕對路徑的形式定義python程序的路徑來執行,即python absPath/xx.py。也就是說我們需要明確告知解釋器我們的程序所在的位置,那么剛剛提到的import的過程,無論是import一個模塊,還是import一個包,本質都是在需要import的.py文件中執行另外一個.py文件,而我們在import的時候,並木有明確指定需要被import的模塊或包所在的路徑(即它們各自所對應的.py文件),這里是不是可以作一個猜想,被import的模塊或包是在當前工作目錄呢(即需要import的.py文件所在目錄)?這里不得不說說import的路徑搜索機制了。
在import過程中,解釋器要為我們需要import進來的模塊或包創建上下文環境,以便正常解釋執行他們,這個上下文環境就是解釋器的搜索路徑,解釋器只會在搜索路徑下去搜索被import的包或模塊。這個搜索路徑,就是當前程序的環境變量,即sys.path的輸出,這就是import的路徑搜索機制。sys.path的輸出是一個包含了所有環境變量在內的列表。其中第一個元素就是當前目錄,因此import當前工作目錄下的模塊或包是完全ok的。
1 import sys 2 print(sys.path) 3 4 輸出: 5 ['D:\\python\\S13\\Day5', 'D:\\python','C:\\Program Files (x86)\\python3.6.1\\python36.zip', 'C:\\Program Files (x86)\\python3.6.1\\DLLs', 'C:\\Program Files (x86)\\python3.6.1\\lib', 'C:\\Program Files (x86)\\python3.6.1', 'C:\\Program Files (x86)\\python3.6.1\\lib\\site-packages']
import的路徑搜索機制,意味着我們要成功import一個模塊或包時,它們必須在sys.path輸出的路徑之一中存在,否則import會失敗。還是實際驗證一下吧。
我們先來看下當前工作目錄和sys.path的輸出:
1 import sys,os 2 print(os.getcwd()) 3 print('----------------') 4 print(sys.path) 5 6 輸出: 7 D:\python\S13\Day5 #當前工作目錄 8 ---------------- 9 ['D:\\python\\S13\\Day5', 'D:\\python','C:\\Program Files (x86)\\python3.6.1\\python36.zip', 'C:\\Program Files (x86)\\python3.6.1\\DLLs', 'C:\\Program Files (x86)\\python3.6.1\\lib', 'C:\\Program Files (x86)\\python3.6.1', 'C:\\Program Files (x86)\\python3.6.1\\lib\\site-packages']
然后我們在D:\python\S13\Day4目錄下有一個hello.py程序:
1 import sys,os 2 print(os.getcwd()) 3 print('----------------') 4 print(sys.path) 5 6 import hello 7 say_hello() 8 9 程序輸出: 10 D:\python\S13\Day5 11 ---------------- 12 ['D:\\python\\S13\\Day5', 'D:\\python', 'C:\\Program Files (x86)\\python3.6.1\\python36.zip', 'C:\\Program Files (x86)\\python3.6.1\\DLLs', 'C:\\Program Files (x86)\\python3.6.1\\lib', 'C:\\Program Files (x86)\\python3.6.1', 'C:\\Program Files (x86)\\python3.6.1\\lib\\site-packages'] 13 Traceback (most recent call last): 14 File "D:/python/S13/Day5/test.py", line 9, in <module> 15 import hello 16 ModuleNotFoundError: No module named 'hello' #報錯沒有hello模塊
由於搜索路徑sys.path下找不到hello.py文件,因此import后調用它的函數會直接報錯。
OK,既然要成功import一個模塊或者包意味着它們必須存在於sys,path的路徑輸出中,那么我們怎么才能成功import一個非同級目錄下的模塊或者包呢?實際這部分內容在早期的不同目錄間的模塊調用中已經講述過,鏈接http://www.cnblogs.com/linupython/p/7736816.html。既然搜索路徑依賴於sys.path的輸出,因此我們把需要import進來的模塊或包所在的目錄,添加到sys.path環境變量中即可為其創建上下文環境來import了。具體可以考慮用list的insert方法或append方法,當然直接使用insert方法插入到列表頭部理論上解釋器在搜索時會更快找到。
還是通過代碼來解決剛剛不能import的問題吧:
1 import sys,os 2 print(os.getcwd()) 3 print('----------------') 4 print(sys.path) 5 6 path1 = os.path.dirname(os.path.dirname(__file__)) 7 sys.path.insert(0, path1) #把當前文件的父目錄的父目錄插入到環境變量中 8 print('================') 9 print(sys.path) 10 from Day4 import hello 11 hello.say_hello() 12 13 輸出: 14 D:\python\S13\Day5 15 ---------------- 16 ['D:\\python\\S13\\Day5', 'D:\\python', 'C:\\Program Files (x86)\\python3.6.1\\python36.zip', 'C:\\Program Files (x86)\\python3.6.1\\DLLs', 'C:\\Program Files (x86)\\python3.6.1\\lib', 'C:\\Program Files (x86)\\python3.6.1', 'C:\\Program Files (x86)\\python3.6.1\\lib\\site-packages'] 17 ['D:/python/S13', 'D:\\python\\S13\\Day5', 'D:\\python','C:\\Program Files (x86)\\python3.6.1\\python36.zip', 'C:\\Program Files (x86)\\python3.6.1\\DLLs', 'C:\\Program Files (x86)\\python3.6.1\\lib', 'C:\\Program Files (x86)\\python3.6.1', 'C:\\Program Files (x86)\\python3.6.1\\lib\\site-packages'] 18 Hello Python!
1. 需要import的py文件所在目錄(如果是軟鏈接,那么是實際所在目錄)或當前目錄;
2. 環境變量 PYTHONPATH中列出的目錄(類似環境變量 PATH,由用戶定義,默認為空,Linux下可通過echo $PYTHONPATH來獲取);
3. site 模塊被 import 時添加的路徑(site 會在運行時被自動 import)。
以上論述引用自 https://loggerhead.me/posts/python-de-import-ji-zhi.html#fn:import-site,有部分改編,感謝大神的分享了!
因此還是建議使用sys.path.append方法來增加搜索路徑。
四、import的用法
import的用法有好幾種,下面來逐一闡述:
- 直接import
可導入單模塊或單個包,需要導入多個模塊或包時,多個模塊或包之間通過逗號分隔即可。這種方式相當於全局導入,即導入執行目標模塊或目標包的全部代碼,引入了目標模塊或包的全部可用方法,但需要通過module名稱來引用模塊的方法,成員變量,函數,類等。具體形式是:
import A
調用:”A.B”
import A相當於在當前文件中賦值變量A=’A.py的全部代碼’
來個最簡單的相同目錄下的模塊調用栗子:
程序結構:
代碼:1 ### hello.py 2 # !/usr/bin/env python 3 # -*- coding: utf-8 -*- 4 def say_hi(): 5 language = "java" 6 print("Hello, world! Let's play %s" % language) 7 8 9 ### import_test.py 10 # !/usr/bin/env python 11 # -*- coding: utf-8 -*- 12 def say_hi(): 13 language = "python" 14 print("Hello, world! Let's play %s" % language) 15 16 17 import hello 18 hello.say_hi() 19 say_hi() 20 21 22 ### 程序輸出 23 Hello, world! Let's play java 24 Hello, world! Let's play python
- 使用from import方式
from后面跟上模塊名或包名,import后面跟上模塊里面的類,函數,方法等,可以import *,也可以import某個或某些具體的對象,import多個時依然通過逗號分隔。調用目標模塊或包的方法時直接調用即可,無需像import那樣還要加上模塊名或包名作為前綴:
from A import B
調用: "B”
不建議使用from A import *這種用法,因此from A import B只是引入了目標模塊或目標包的某個或某幾個方法,並非import第一種情況下的全部引入。另外需要注意的是,由於調用目標模塊或包的方法時與調用本地文件中定義的方法沒有區別,因此如果存在重復命名的方法,from A import B會污染本地namespace,覆蓋本地當前文件中定義的同名方法。
還是使用上面import栗子中的相同代碼文件把:1 ### hello.py 2 # !/usr/bin/env python 3 # -*- coding: utf-8 -*- 4 def say_hi(): 5 language = "java" 6 print("Hello, world! Let's play %s" % language) 7 8 9 ### import_test.py 10 # !/usr/bin/env python 11 # -*- coding: utf-8 -*- 12 def say_hi(): 13 language = "python" 14 print("Hello, world! Let's play %s" % language) 15 16 17 from hello import say_hi #注意導入方式有變化 18 say_hi() #引用函數的方式也變了 19 20 say_hi() 21 22 ### 程序輸出 23 Hello, world! Let's play java 24 Hello, world! Let's play java
還是示例一下,實踐為王:
1 ### hello.py 2 # !/usr/bin/env python 3 # -*- coding: utf-8 -*- 4 __author__ = 'Maxwell' 5 6 def say_hi(): 7 language = "java" 8 print("Hello, world! Let's play %s" % language) 9 10 11 ### import_test.py 12 # !/usr/bin/env python 13 # -*- coding: utf-8 -*- 14 __author__ = 'Maxwell' 15 16 17 def say_hi(): 18 language = "python" 19 print("Hello, world! Let's play %s" % language) 20 21 from hello import say_hi as say_hello 22 say_hello() 23 say_hi() 24 25 程序輸出: 26 Hello, world! Let's play java 27 Hello, world! Let's play python
實際上from … import的使用方式非常靈活,具體有以下幾種:
from 包.[..包] import 模塊
或
from 包.模塊 import 方法
或
from 模塊 import 方法
如果import后面跟的是模塊,那么調用時仍然需要使用“模塊.方法”來進行,其他情況下直接使用“方法”進行調用即可。
導入包的程序示例:
程序結構圖:|
程序代碼:1 ### pack1/hello.py 2 3 # !/usr/bin/env python 4 # -*- coding: utf-8 -*- 5 __author__ = 'Maxwell' 6 7 def say_hi(): 8 language = "Golang" 9 print("Hello, world! Let's play %s" % language) 10 # pack1包中的init.py為空 11 12 13 ### import_test.py 14 # !/usr/bin/env python 15 # -*- coding: utf-8 -*- 16 __author__ = 'Maxwell' 17 18 19 def say_hi(): 20 language = "python" 21 print("Hello, world! Let's play %s" % language) 22 23 ### 以下兩種import和調用方式均可 24 25 # from pack1 import hello 26 # hello.say_hi() 27 28 from pack1.hello import say_hi 29 say_hi() 30 31 程序輸出: 32 Hello, world! Let's play Golang
- 不同目錄間模塊調用時import和from … import …雙拼應用
上面的程序示例都是在同級目錄前提下(需要import的文件和被import的模塊在同級目錄下)展開的,而不同目錄間調用模塊在實際項目中用的更多。早些時候在寫不同目錄間模塊調用時,功課明顯做的不夠,沒有深入學習import的機制,這里借此機會把不同目錄間模塊調用的方法再重復一下,提升一下理解的程度。
大致思路是:
(1). 通過os模塊逐步獲取到被import模塊與當前文件(模塊)的公共目錄
(2). 把第一步獲取到的目錄追加到sys.path中,為import創建上下文環境
(3). 通過from path import module方式把目標模塊import進來,注意這里from后面接的是sys.path中某個元素的子目錄
(4). 通過module.function()形式調用目標模塊的函數模塊
到這里我們基本可以認為這種思路是from import和import的復合雙拼式應用,說明import機制在實際應用中的形式非常靈活。
還是來一個實際栗子把:
程序結構:
代碼:1 ### log.py 2 # !/usr/bin/env python 3 # -*- coding: utf-8 -*- 4 __author__ = 'Maxwell' 5 6 import logging 7 8 def logger(x): 9 logging.warning('[INFO] %s' % x) 10 11 ### config.py 12 # !/usr/bin/env python 13 # -*- coding: utf-8 -*- 14 __author__ = 'Maxwell' 15 16 settings = {'db':'mysql','port':3306, 'master':'true'} 17 18 19 ### main.py 20 # !/usr/bin/env python 21 # -*- coding: utf-8 -*- 22 __author__ = 'Maxwell' 23 24 25 import sys,os 26 27 28 BASEDIR = os.path.dirname(os.path.dirname(__file__)) 29 print(BASEDIR) 30 sys.path.append(BASEDIR) 31 32 33 from logs import log 34 35 from conf import config 36 if config.settings['db'] == 'mysql': 37 log.logger('Check mysql config') 38 if config.settings['port'] == 3306 and config.settings['master'] == 'true': 39 log.logger('Check config for mysql master OK!') 40 41 42 ### 運行結果 43 WARNING:root:[INFO] Check mysql config 44 WARNING:root:[INFO] Check config for mysql master OK! 45
五、import機制總結及優化
關於import機制的要點總結如下:
(1) import的過程
實際上包括兩個部分,第一部分類似於shell 的 "source" 命令, 具體說即相當於將被import的module(即python文件)在當前環境下執行一遍;第二部分使被import的成員(變量名, 函數名, 類....)可見,為保證不發生命名沖突, 需要以 module.name 的方式訪問導入的成員(不存在命名沖突問題)。import的機制是將目標模塊中的對象完整的引入當前模塊,但並不引入新的變量名。
(2) "from *** import "的過程
以這種方式導入module時, python會在當前module 的命名空間中新建相應的命名。即, "from t2 import var1" 相當於:
import t2
var1= t2.var1
在此過程中有一個隱含的賦值的過程。from import的機制則是通過引入新的變量名的形式,將目標模塊的對象的引用拷貝到新的變量名下的方式引入當前模塊。
(3) import和from import的作用機制完全不同,import的范圍更廣(先全部引入然后再挑選),from … import的對象更確切更有針對性(相當於局部引入,需要哪個就引入哪個)
(4) 重復import或from import多次都只會作用一次
上述內容轉自https://blog.csdn.net/lianliange85/article/details/17223429 和 https://www.jianshu.com/p/c82429550dca,有少部分改編。
import的優化目前只有一點:
import的范圍更廣(先全部引入然后再挑選),from … import的對象更確切更有針對性(相當於局部引入,需要哪個就引入哪個),因此對於需要重復調用模塊中方法的情況,通過from import方式可以優化代碼執行速度(避免過多引入),但需要注意命名覆蓋污染問題。
附錄:import機制其他參考資料
http://blog.sina.com.cn/s/blog_c612638e0101q8kc.html
https://loggerhead.me/posts/python-de-import-ji-zhi.html
https://www.cnblogs.com/Cirgo/p/8417490.html