核心內容專自:http://www.liujiangblog.com/course/python/48
在自動化測試的時候,需要從excel中讀取關鍵字,此關鍵字對應一個方法,如何使用該關鍵字去調用真正的關鍵字方法呢?
這就用到了反射。
在前面的章節,我們遺留了hasattr()、getattr()、setattr()和delattr()的相關內容,它們在這里。
對編程語言比較熟悉的同學,應該聽說過“反射”這個機制。Python作為一門動態語言,當然不會缺少這一重要功能。下面結合一個web路由的實例來闡述Python反射機制的使用場景和核心本質。
首先,我們要區分兩個概念——“標識名”和看起來相同的“字符串”。兩者字面上看起來一樣,卻是兩種東西,比如下面的func函數和字符串“func”:
def func(): print("func是這個函數的名字!") s = "func" print("%s是個字符串" % s)
前者是函數func的函數名,后者只是一個叫“func”的字符串,兩者是不同的事物。我們可以用func()的方式調用函數func,但我們不能用"func"()的方式調用函數。說白了就是,不能通過字符串來調用名字看起來相同的函數!
實例分析
考慮有這么一個場景:需要根據用戶輸入url的不同,調用不同的函數,實現不同的操作,也就是一個WEB框架的url路由功能。路由功能是web框架里的核心功能之一,例如Django的urls。
首先,有一個commons.py文件,它里面有幾個函數,分別用於展示不同的頁面。這其實就是Web服務的視圖文件,用於處理實際的業務邏輯。
# commons.py
def login(): print("這是一個登陸頁面!") def logout(): print("這是一個退出頁面!") def home(): print("這是網站主頁面!")
其次,有一個visit.py文件,作為程序入口,接收用戶輸入,並根據輸入展示相應的頁面。
# visit.py
import commons def run(): inp = input("請輸入您想訪問頁面的url: ").strip() if inp == "login": commons.login() elif inp == "logout": commons.logout() elif inp == "home": commons.home() else: print("404") if __name__ == '__main__': run()
運行visit.py,輸入home,頁面結果如下:
請輸入您想訪問頁面的url: home 這是網站主頁面!
這就實現了一個簡單的url路由功能,根據不同的url,執行不同的函數,獲得不同的頁面。
然而,讓我們思考一個問題,如果commons文件里有成百上千個函數呢(這很常見)?難道在visit模塊里寫上成百上千個elif?顯然這是不可能的!那么怎么辦?
仔細觀察visit.py中的代碼,會發現用戶輸入的url字符串和相應調用的函數名好像!如果能用這個字符串直接調用函數就好了!但是,前面已經說了字符串是不能用來調用函數的。為了解決這個問題,Python提供了反射機制,幫助我們實現這一想法,其主要就表現在getattr()等幾個內置函數上!
現在將前面的visit.py修改一下,代碼如下:
# visit.py
import commons def run(): inp = input("請輸入您想訪問頁面的url: ").strip() func = getattr(commons,inp) func() if __name__ == '__main__': run()
func = getattr(commons,inp)
語句是關鍵,通過getattr()函數,從commons模塊里,查找到和inp字符串“外形”相同的函數名,並將其返回,然后賦值給func變量。變量func此時就指向那個函數,func()就可以調用該函數。
getattr()函數的使用方法:接收2個參數,前面的是一個類或者模塊,后面的是一個字符串,注意了!是個字符串!
這個過程就相當於把一個字符串變成一個函數名的過程。這是一個動態訪問的過程,一切都不寫死,全部根據用戶輸入來變化。
前面的代碼還有個小瑕疵,那就是如果用戶輸入一個非法的url,比如jpg,由於在commons里沒有同名的函數,肯定會產生運行錯誤,如下:
請輸入您想訪問頁面的url: jpg Traceback (most recent call last): File "F:/Python/pycharm/s13/reflect/visit.py", line 16, in <module> run() File "F:/Python/pycharm/s13/reflect/visit.py", line 11, in run func = getattr(commons,inp) AttributeError: module 'commons' has no attribute 'jpg'
那怎么辦呢?python提供了一個hasattr()的內置函數,用法和getattr()基本類似,它可以判斷commons中是否具有某個成員,返回True或False。現在將代碼修改一下:
# visit.py
import commons def run(): inp = input("請輸入您想訪問頁面的url: ").strip() if hasattr(commons,inp): func = getattr(commons,inp) func() else: print("404") if __name__ == '__main__': run()
這下就沒有問題了!通過hasattr()的判斷,可以防止非法輸入導致的錯誤,並將其統一定位到錯誤頁面。
Python的四個重要內置函數:getattr()、hasattr()、delattr()和setattr()較為全面的實現了基於字符串的反射機制。delattr()和setattr()就不做多解釋,相信從字面意思看,你也該猜到它們的用途和用法了。它們都是對內存中的模塊進行操作,並不會對源文件進行修改。
動態導入模塊
前面的例子需要commons.py和visit.py模塊在同一目錄下,並且所有的頁面處理函數都在commons模塊內。如下圖:
但在實際環境中,頁面處理函數往往被分類放置在不同目錄的不同模塊中,也就是如下圖:
原則上,只需要在visit.py模塊中逐個導入每個視圖模塊即可。但是,如果這些模塊很多呢?難道要在visit里寫上一大堆的import語句逐個導入account、manage、commons模塊嗎?要是有1000個模塊呢?
可以使用Python內置的__import__(字符串參數)
函數解決這個問題。通過它,可以實現類似getattr()的反射功能。__import__()
方法會根據字符串參數,動態地導入同名的模塊。
再修改一下visit.py的代碼。
# visit.py
def run(): inp = input("請輸入您想訪問頁面的url: ").strip() modules, func = inp.split("/") obj = __import__(modules) if hasattr(obj, func): func = getattr(obj, func) func() else: print("404") if __name__ == '__main__': run()
需要注意的是:輸入的時候要同時提供模塊名和函數名字,並用斜杠分隔。
運行一下試試:
請輸入您想訪問頁面的url: commons/home 這是網站主頁面! 請輸入您想訪問頁面的url: account/find 這是一個查找功能頁面!
同樣的,這里也有個小瑕疵!如果我們的目錄結構是這樣的,visit.py和commons.py不在一個目錄下,存在跨包的問題:
那么在visit的調用語句中,必須進行修改,你想當然地可能會這么做:
def run(): inp = input("請輸入您想訪問頁面的url: ").strip() modules, func = inp.split("/") obj = __import__("lib." + modules) #注意字符串的拼接 if hasattr(obj, func): func = getattr(obj, func) func() else: print("404") if __name__ == '__main__':
看起來似乎沒什么問題,和import lib.commons
的傳統方法類似,但實際上運行的時候會有錯誤。
請輸入您想訪問頁面的url: commons/home 404 請輸入您想訪問頁面的url: account/find 404
為什么呢?因為對於lib.xxx.xxx.xxx
這一類的模塊導入路徑,__import__()
默認只會導入最開頭的圓點左邊的目錄,也就是lib
。可以做個測試,在visit同級目錄內新建一個文件,代碼如下:
obj = __import__("lib.commons") print(obj)
執行結果:<module 'lib' (namespace)>
這個問題怎么解決?加上fromlist = True
參數即可!完整的代碼如下:
def run(): inp = input("請輸入您想訪問頁面的url: ").strip() modules, func = inp.split("/") obj = __import__("lib." + modules, fromlist=True) # 注意fromlist參數 if hasattr(obj, func): func = getattr(obj, func) func() else: print("404") if __name__ == '__main__': run()