python的reflect反射方法


核心內容專自: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模塊內。如下圖:

image.png-4.1kB

但在實際環境中,頁面處理函數往往被分類放置在不同目錄的不同模塊中,也就是如下圖:

image.png-18.3kB

原則上,只需要在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不在一個目錄下,存在跨包的問題:

image.png-7.3kB

那么在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()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM