Python 反射機制

来源:http://www.cnblogs.com/qianyuliang/archive/2017/04/28/6781994.html
-Advertisement-
Play Games

對編程語言比較熟悉的朋友,應該知道“反射”這個機制。Python作為一門動態語言,當然不會缺少這一重要功能。然而,在網路上卻很少見到有詳細或者深刻的剖析論文。下麵結合一個web路由的實例來闡述python的反射機制的使用場景和核心本質。 一、前言 在上面的代碼中,我們必須區分兩個概念,f1和“f1" ...


對編程語言比較熟悉的朋友,應該知道“反射”這個機制。Python作為一門動態語言,當然不會缺少這一重要功能。然而,在網路上卻很少見到有詳細或者深刻的剖析論文。下麵結合一個web路由的實例來闡述python的反射機制的使用場景和核心本質。

一、前言

1 def f1():
2   print("f1是這個函數的名字!")
3   
4 s = "f1"
5 print("%s是個字元串" % s)

  在上面的代碼中,我們必須區分兩個概念,f1和“f1"。前者是函數f1的函數名,後者只是一個叫”f1“的字元串,兩者是不同的事物。我們可以用f1()的方式調用函數f1,但我們不能用"f1"()的方式調用函數。說白了就是,不能通過字元串來調用名字看起來相同的函數!

二、web實例

  考慮有這麼一個場景,根據用戶輸入的url的不同,調用不同的函數,實現不同的操作,也就是一個url路由器的功能,這在web框架里是核心部件之一。下麵有一個精簡版的示例:

  首先,有一個commons模塊,它裡面有幾個函數,分別用於展示不同的頁面,代碼如下:

 1 #coding:utf8
 2 
 3 def login():
 4   print("這是一個登陸頁面!")
 5  
 6 def logout():
 7   print("這是一個退出頁面!")
 8  
 9 def home():
10   print("這是網站主頁面!")

 

  其次,有一個visit模塊,作為程式入口,接受用戶輸入,展示相應的頁面,代碼如下:(這段代碼是比較初級的寫法)

 1 #coding:utf8
 2 
 3 import commons
 4  
 5 def run():
 6   inp = input("請輸入您想訪問頁面的url: ").strip()
 7   if inp == "login":
 8     commons.login()
 9   elif inp == "logout":
10     commons.logout()
11   elif inp == "home":
12     commons.home()
13   else:
14     print("404")
15  
16 if __name__ == '__main__':
17   run()                                          

  我們運行visit.py,輸入:home,頁面結果如下:

請輸入您想訪問頁面的url: home
這是網站主頁面!

  

  這就實現了一個簡單的WEB路由功能,根據不同的url,執行不同的函數,獲得不同的頁面。

  然而,讓我們考慮一個問題,如果commons模塊里有成百上千個函數呢(這非常正常)?。難道你在visit模塊里寫上成百上千個elif?顯然這是不可能的!那麼怎麼破?

三、反射機制

  仔細觀察visit中的代碼,我們會發現用戶輸入的url字元串和相應調用的函數名好像!如果能用這個字元串直接調用函數就好了!但是,前面我們已經說了字元串是不能用來調用函數的。為瞭解決這個問題,python為我們提供一個強大的內置函數:getattr!我們將前面的visit修改一下,代碼如下:

 1 #coding:utf8
 2 
 3 import commons
 4  
 5 def run():
 6   inp = input("請輸入您想訪問頁面的url: ").strip()
 7   func = getattr(commons,inp)
 8   func()
 9   
10 if __name__ == '__main__':
11   run()

  首先說明一下getattr函數的使用方法:它接收2個參數,前面的是一個對象或者模塊,後面的是一個字元串,註意了!是個字元串!

  例子中,用戶輸入儲存在inp中,這個inp就是個字元串,getattr函數讓程式去commons這個模塊里,尋找一個叫inp的成員(是叫,不是等於),這個過程就相當於我們把一個字元串變成一個函數名的過程。然後,把獲得的結果賦值給func這個變數,實際上func就指向了commons里的某個函數。最後通過調用func函數,實現對commons里函數的調用。這完全就是一個動態訪問的過程,一切都不寫死,全部根據用戶輸入來變化。

  執行上面的代碼,結果和最開始的是一樣的。

  這就是python的反射,它的核心本質其實就是利用字元串的形式去對象(模塊)中操作(查找/獲取/刪除/添加)成員,一種基於字元串的事件驅動!

  這段話,不一定准確,但大概就是這麼個意思。

四、進一步完善

  上面的代碼還有個小瑕疵,那就是如果用戶輸入一個非法的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的內置函數,用於判斷commons中是否具有某個成員。我們將代碼修改一下:

 1 #coding:utf8
 2 
 3 import commons
 4   
 5 def run():
 6   inp = input("請輸入您想訪問頁面的url: ").strip()
 7   if hasattr(commons,inp):
 8     func = getattr(commons,inp)
 9     func()
10   else:
11     print("404")
12   
13 if __name__ == '__main__':
14   run()

  通過hasattr的判斷,可以防止非法輸入錯誤,並將其統一定位到錯誤頁面。

  其實,研究過python內置函數的朋友,應該註意到還有delattrsetattr兩個內置函數。從字面上已經很好理解他們的作用了。

  python的四個重要內置函數:getattrhasattrdelattrsetattr較為全面的實現了基於字元串的反射機制。他們都是對記憶體內的模塊進行操作,並不會對源文件進行修改。

五、動態導入模塊

  上面的例子是在某個特定的目錄結構下才能正常實現的,也就是commons和visit模塊在同一目錄下,並且所有的頁面處理函數都在commons模塊內。如下圖:

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

  難道我們要在visit模塊里寫上一大堆的import 語句逐個導入account、manage、commons模塊嗎?要是有1000個這種模塊呢?

  剛纔我們分析完了基於字元串的反射,實現了動態的函數調用功能,我們不禁會想那麼能不能動態導入模塊呢?這完全是可以的!

  python提供了一個特殊的方法__import__(字元串參數)通過它,我們就可以實現類似的反射功能。__import__()方法會根據參數,動態的導入同名的模塊。

我們再修改一下上面的visit模塊的代碼。

 1 #coding:Utf8
 2 
 3 def run():
 4   inp = input("請輸入您想訪問頁面的url: ").strip()
 5   modules, func = inp.split("/")
 6   obj = __import__(modules)
 7   if hasattr(obj, func):
 8     func = getattr(obj, func)
 9     func()
10   else:
11     print("404")
12   
13 if __name__ == '__main__':
14   run()

 

運行一下:

1 請輸入您想訪問頁面的url: commons/home
2 這是網站主頁面!
3 請輸入您想訪問頁面的url: account/find
4 這是一個查找功能頁面!

 

  我們來分析一下上面的代碼:

  首先,我們並沒有定義任何一行import語句;

  其次,用戶的輸入inp被要求為類似“commons/home”這種格式,其實也就是模擬web框架里的url地址,斜杠左邊指向模塊名,右邊指向模塊中的成員名。

  然後,modules,func = inp.split("/")處理了用戶輸入,使我們獲得的2個字元串,並分別保存在modules和func變數里。

  接下來,最關鍵的是obj = __import__(modules)這一行,它讓程式去導入了modules這個變數保存的字元串同名的模塊,並將它賦值給obj變數。

  最後的調用中,getattr去modules模塊中調用func成員的含義和以前是一樣的。

  總結:通過__import__函數,我們實現了基於字元串的動態的模塊導入。

  同樣的,這裡也有個小瑕疵!

  如果我們的目錄結構是這樣的:

  那麼在visit的模塊調用語句中,必須進行修改,我們想當然地會這麼做:

 1 #coding:Utf8
 2 
 3 def run():
 4   inp = input("請輸入您想訪問頁面的url: ").strip()
 5   modules, func = inp.split("/")
 6   obj = __import__("lib." + modules)   #註意字元串的拼接
 7   if hasattr(obj, func):
 8     func = getattr(obj, func)
 9     func()
10   else:
11     print("404")
12   
13 if __name__ == '__main__':
14   run()

 

  改了這麼一個地方:obj = __import__("lib." + modules)看起來似乎沒什麼問題,和import lib.commons的傳統方法類似,但實際上運行的時候會有錯誤。

請輸入您想訪問頁面的url: commons/home
404
請輸入您想訪問頁面的url: account/find
404

  為什麼呢?因為對於lib.xxx.xxx.xxx這一類的模塊導入路徑,__import__預設只會導入最開頭的圓點左邊的目錄,也就是“lib”。我們可以做個測試,在visit同級目錄內新建一個文件,代碼如下:

1 1 obj = __import__("lib.commons")
2 2 print(obj)

  執行結果: 

<module 'lib' (namespace)>

  這個問題怎麼解決呢?加上fromlist = True參數即可!

 1 #coding:utf8
 2 
 3 def run():
 4   inp = input("請輸入您想訪問頁面的url: ").strip()
 5   modules, func = inp.split("/")
 6   obj = __import__("lib." + modules, fromlist=True) # 註意fromlist參數
 7   if hasattr(obj, func):
 8     func = getattr(obj, func)
 9     func()
10   else:
11     print("404")
12   
13 if __name__ == '__main__':
14   run()

 

  至此,動態導入模塊的問題基本都解決了,只剩下最後一個,那就是萬一用戶輸入錯誤的模塊名呢?比如用戶輸入了somemodules/find,由於實際上不存在somemodules這個模塊,必然會報錯!那有沒有類似上面hasattr內置函數這麼個功能呢?答案是沒有!碰到這種,你只能通過異常處理來解決。

六、最後的思考

  可能有人會問python不是有兩個內置函數execeval嗎?他們同樣能夠執行字元串。比如:

exec("print('haha')")
結果:
haha

  

  那麼直接使用它們不行嗎?非要那麼費勁地使用getattr__import__幹嘛?

  其實,在上面的例子中,圍繞的核心主題是如何利用字元串驅動不同的事件,比如導入模塊、調用函數等等,這些都是python的反射機制,是一種編程方法、設計模式的體現,凝聚了高內聚、松耦合的編程思想,不能簡單的用執行字元串來代替。當然,exec和eval也有它的舞臺,在web框架里也經常被使用。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 預計分數:100+50+50 實際分數:5+50+100 =.= 多重背包 (backpack.cpp/c/pas) (1s/256M) 題目描述 提供一個背包,它最多能負載重量為W的物品。 現在給出N種物品:對於第i類物品,一共有Ci件物品;對於每一件物品,重量為Wi,價值為Vi。 找出一種裝載方 ...
  • 知乎指南 知乎讓我增長了見識, 看到了很多真實故事, 喝了百樣的雞湯, 所以我決定以後不看知乎了 知乎給我的錯覺:階段性 第一周 我打開了新的大門, 很多有意思的段子, 看上去很有內涵, 不過都很老套 有很多故事, 大家似乎都願意講他們的故事, 看客很多, 會屏蔽666, 老鐵, 233之類的評論 ...
  • 裝飾器模式詳解地址 原文總結 定義: 在不必改變原類文件和使用繼承的情況下, 動態的擴展一個對象的功能. 通過創建一個包裝對象, 也就是裝飾來包裹真實的對象 部分詳解提示 看了一些文檔, 裝飾器模式非常依賴構造器 與 重寫方法 裝飾器模式的特點: 不改變原來的類 , 不使用繼承 , 動態擴展 流這塊 ...
  • 本篇博客我們繼續的來聊SpringMVC的東西,下方我們將會聊到js、css這些靜態文件的載入配置,以及伺服器推送的兩種實現方式。當然我們在伺服器推送時,會用到jQuery的東西,所以我們先聊一下如何載入靜態資源文件,然後我們再聊如何實現伺服器推送。 下方給出了兩種實現伺服器推送的方式,一種是SSE ...
  • Hibernate的二級緩存 理解緩存定義: 緩存(Cache):電腦領域非常通用的概念。它介於應用程式和永久性數據存儲源(如硬碟上的文件或者資料庫)之間,其作用是降低應用程式直接讀寫永久性數據存儲源的頻率,從而提高應用的運行性能。緩存中的數據是數據存儲源中數據的拷貝。緩存的物理介質通常是記憶體。 ...
  • 使用 RegexString.with(string).pattern(pattern).start() + 後續操作(matches,find或者是replace) 源碼 示例 ...
  • 經過了一段時間的學習,慢慢的計入到了python的列表、元組的學習了,這一部分是後面函數的基礎,這也是無論何種語言都要學習的部分。其實過程很辛苦,不過對於小白的我不見得是一件壞事,反正都看不懂,倒也沒有什麼心理負擔。想想學成後的一覽眾山小,此時的積累,便是每一步都要堅實的。 列表、元組 列表是我們最 ...
  • 函數, get()與setdefault(), lambda表達式,三元運算, 遍歷list與dict的方法. ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...