摘要:需求背景最近在工作上遇到了一個比較特殊的需求為了安全設(shè)計一個函數(shù)或者裝飾器然后用戶在定義調(diào)用函數(shù)時只能訪問到我們允許的內(nèi)置變量和全局變量通過例子來這解釋下上面的需求輸出函數(shù)功能簡單明了對于結(jié)果大家應(yīng)該也不會有太大的異議分別是取得全局命名空間
需求背景
最近在工作上, 遇到了一個比較特殊的需求:
為了安全, 設(shè)計一個函數(shù)或者裝飾器, 然后用戶在 "定義/調(diào)用" 函數(shù)時, 只能訪問到我們允許的內(nèi)置變量和全局變量
通過例子來這解釋下上面的需求:
a = 123 def func(): print a print id(a) func() # 輸出 123 32081168
函數(shù)功能簡單明了, 對于結(jié)果, 大家應(yīng)該也不會有太大的異議:func分別是取得全局命名空間中a的值和使用內(nèi)置命名空間中的函數(shù)id獲取了a的地址. 熟悉Python的童鞋, 對于LEGB肯定也是不陌生的,也正是因?yàn)?b>LEGB才讓函數(shù)func輸出正確的結(jié)果. 但是這個只是一個常規(guī)例子, 只是用來拋磚引玉而已. 我們真正想要討論的是下面的例子:
# 裝飾函數(shù) def wrap(f): # 調(diào)用用戶傳入的函數(shù) f() a = 123 # 用戶自定義函數(shù) def func(): import os print os.listdir(".") wrap(func) # 輸出 ["1.yml", "2.py", "2.txt", "2.yml", "ftp", "ftp.rar", "test", "tmp", "__init__.py"]潛在危險因素
在上面的例子可以看出, 如果在func中, 引入別的模塊, 然后再執(zhí)行模塊中的方法, 也是可行的! 而且這還是一個非常方便的功能! 但是除了方便, 更多的是一種潛在的危險.在日常使用, 或許我們不會考慮這些, 但是如果在模塊與模塊之間的協(xié)同作用時, 特別是多人參與的情況下, 這種危險的因素, 就不得不讓我們認(rèn)真對待!
或許有很多同學(xué)會覺得這些擔(dān)憂是過多的, 是沒必要的, 但是請思考一種場景: 我們有個主模塊, 暫時稱為main.py, 它允許用戶動態(tài)加載模塊, 也就是說只要用戶將對應(yīng)的模塊放到對應(yīng)的目錄, 然后利用消息機(jī)制去通知main.py, 告訴它應(yīng)該加載新模塊了, 并且執(zhí)行新模塊里面的b函數(shù), 那在這種情況下, main.py肯定不能直接傻傻的就去執(zhí)行, 因?yàn)槲覀儾荒芟嘈琶總€用戶都是誠實(shí)善良的, 也不能相信每個用戶編寫的模塊或者函數(shù)是符合我們的行為標(biāo)準(zhǔn)規(guī)范. 所以我們得有些措施去防范這些事情, 我們能做的大概也就下面幾種方式:
1.在用戶通知`main.py`時有新模塊加入并且要求執(zhí)行函數(shù)時, 先對模塊的代碼做檢查, 不符合標(biāo)準(zhǔn)或者帶有危險代碼的拒絕加載. 2.控制好`內(nèi)置命名空間`和`全局命名空間`, 使其只能用允許使用的內(nèi)容
在方案1, 其實(shí)也是我們最容易想到的方法, 但是這個方法的成本還是比較高, 因?yàn)槲覀冃枰獙⒖赡艹霈F(xiàn)的錯誤代碼或者關(guān)鍵詞,全部寫成一套規(guī)則, 而且這套規(guī)則還很大可能會誤傷, 不過也可能業(yè)界已經(jīng)有類似的成熟的方案, 只是我還沒接觸到而已.
所以我們只能用方案2的方法, 這種方法在我們看來, 是成本比較低的, 也比較容易控制的, 因?yàn)檫@就和防火墻一樣, 我們只放行我們允許的事物.
實(shí)現(xiàn)方案2最大的問題就是, 如何控制內(nèi)置命名空間 和全局命名空間
我們第一個想法肯定就是覆蓋它們, 因?yàn)槲覀兌贾啦还苁?b>內(nèi)置命名空間還是全局命名空間, 都是通過字典的形式在維護(hù):
print globals() print globals()["__builtins__"].__dict__ # 輸出 # 全局命名空間 {"__builtins__":, "__name__": "__main__", "__file__": "D:/Python_project/ftp/2.py", "__doc__": None, "__package__": None} #內(nèi)置命名空間 {"bytearray": , "IndexError": 注: globals函數(shù) 是用來打印當(dāng)前全局命名空間的函數(shù), 同樣, 也能通過修改這個函數(shù)返回的字典對應(yīng)的key, 實(shí)現(xiàn)全局命名空間的修改.例如:
s = globals() print s s["a"] = 3 print s print a # 輸出 {"__builtins__":, "__file__": "D:/Python_project/ftp/2.py", "__package__": None, "s": {...}, "__name__": "__main__", "__doc__": None} {"a": 3, "__builtins__": , "__file__": "D:/Python_project/ftp/2.py", "__package__": None, "s": {...}, "__name__": "__main__", "__doc__": None} 3 可以看出, 我們并沒有定義變量a, 只是在globals的返回值上面增加了key-value, 就變相實(shí)現(xiàn)了我們定義的操作, 這其實(shí)也能用于很多希望能夠動態(tài)賦值的需求場景! 比如說, 我不確定有多少個變量, 希望通過一個變量名列表, 動態(tài)生成這些變量, 在這種情況下, 就能參考這種方法, 不過還是希望謹(jǐn)慎使用, 因?yàn)樾薷牧诉@個, 就是就修改了全局命名空間.
好了, 回歸到本文, 我們已經(jīng)知道通過globals函數(shù)能夠代表全局命名空間, 但是為什么內(nèi)置命名空間要用globals()["__builtins__"].__dict__來表示? 其實(shí)這個和python自身的機(jī)制有關(guān), 因?yàn)槟K在編譯和初始化的過程中, 內(nèi)置命名空間就是以這種形式,寄放在全局命名空間:
static void initmain(void) { PyObject *m, *d; m = PyImport_AddModule("__main__"); if (m == NULL) Py_FatalError("can"t create __main__ module"); d = PyModule_GetDict(m); if (PyDict_GetItemString(d, "__builtins__") == NULL) { PyObject *bimod = PyImport_ImportModule("__builtin__"); if (bimod == NULL || PyDict_SetItemString(d, "__builtins__", bimod) != 0) Py_FatalError("can"t add __builtins__ to __main__"); Py_XDECREF(bimod); } }從上面代碼可以看出, 在初始化__main__時, 會有一個獲取__builtins__的動作, 如果這個結(jié)果是NULL, 那么就會用之前初始化好的__builtin__去存進(jìn)去, 這些代碼具體可以看Pythonrun.c, 在這不詳細(xì)展開了.
既然內(nèi)置命名空間(__builtins__)和全局命名空間(globals())都已經(jīng)找到對應(yīng)對象了, 那我們下一步就應(yīng)該是想法將這兩個空間替換成我們想要的.
# coding: utf8 # 修改全局命名空間 test_var = 123 # 測試變量 tmp = globals().keys() print globals() print test_var for i in tmp: del globals()[i] print globals() print test_var print id(2) # 輸出 {"tmp": ["__builtins__", "__file__", "__package__", "test_var", "__name__", "__doc__"], "__builtins__":, "__file__": "D:/Python_project/ftp/2.py", "__package__": None, "test_var": 123, "__name__": "__main__", "__doc__": None} 123 {"tmp": ["__builtins__", "__file__", "__package__", "test_var", "__name__", "__doc__"], "i": "__doc__"} Traceback (most recent call last): File "D:/Python_project/ftp/2.py", line 10, in print test_var NameError: name "test_var" is not defined 在上面的輸出可以看到, 在刪除前后, 通過print globals()可以看到全局命名空間確實(shí)已經(jīng)被修改了, 因?yàn)?b>test_var已經(jīng)無法打印了, 觸發(fā)了NameError, 這樣的話, 就有辦法能夠限制全局命令空間了:
# 偽代碼 # 裝飾函數(shù) def wrap(f): # 調(diào)用用戶傳入的函數(shù) .... 修改全局命名空間 f() .... 還原全局命名空間 a = 123 # 用戶自定義函數(shù) def func(): import os print os.listdir(".") wrap(func)為什么我只寫偽代碼, 因?yàn)槲野l(fā)現(xiàn)這個功能實(shí)現(xiàn)起來是非常蛋疼! 原因就是, 在實(shí)現(xiàn)之前, 我們必須要解決幾個問題:
1.全局命名空間對應(yīng)了一個字典, 所以如果我們想要修改, 只能從修改這個字典本身, 于是先清空再定義成我們約束的, 調(diào)用完之后, 又得反過來恢復(fù), 這些操作是十分之蛋疼. 2.涉及到共享的問題, 如果這個用戶函數(shù)處理很久, 而且是多線程的, 那么整個模塊都會變得很不穩(wěn)定, 甚至稱為"污染"那就先撇開不講, 講講內(nèi)置命名空間, 剛才我們已經(jīng)找到了能代表內(nèi)置命名空間的對象, 很幸運(yùn)的是, 這個是"真的能夠摸得到"的, 那我們試下直接就賦值個空字典, 看會怎樣:
s = globals() print s["__builtins__"] # __builtins__檢查是否存在 s["__builtins__"] = {} print s["__builtins__"] # __builtins__檢查是否存在 print id(3) # 試下內(nèi)置函數(shù)能否使用 print globals() # 輸出{} 32602360 {"__builtins__": {}, "__file__": "D:/Python_project/ftp/2.py", "__package__": None, "s": {...}, "__name__": "__main__", "__doc__": None} 結(jié)果有點(diǎn)尷尬, 似乎沒啥用, 但是其實(shí)這個__builtins__只是一個表現(xiàn), 真正的內(nèi)置命名空間是在它所指向的字典對象, 也就是: globals()["__builtins__"].__dict__!
print globals()["__builtins__"].__dict__ # 輸出 {"bytearray":, "IndexError": ....} # 省略 所以我們真正要覆蓋的, 是這個字典才對, 所以上面的代碼要改成:
s = globals() s["__builtins__"].__dict__ = {} # 覆蓋真正的內(nèi)置命名空間 print s["__builtins__"].__dict__ # __builtins__檢查是否存在 # 輸出 Traceback (most recent call last): File "D:/Python_project/ftp/2.py", line 3, ins["__builtins__"].__dict__ = {} TypeError: readonly attribute 失敗了...原來這個內(nèi)置命名空間是只讀的, 所以我們上面的方法都失敗了..那難道真的沒法解決了嗎? 一般這樣問, 通常都有解決方案滴~
完美方案這個解決方法, 需要一個庫的幫忙~, 那就是inspect庫, 這個庫是干嘛呢? 簡單來說就是用來自省. 它提供四種用處:
1.對是否是模塊,框架,函數(shù)等進(jìn)行類型檢查。 2.獲取源碼 3.獲取類或函數(shù)的參數(shù)的信息 4.解析堆棧在這里, 我們需要用到第二個功能, 其余的功能, 感興趣的童鞋可以去谷歌學(xué)習(xí)哦, 也可以參考: https://my.oschina.net/taisha...
除了inspect, 我們還需要用到exec, 這也是一大殺器, 可以先參考這個學(xué)習(xí)下: http://www.mojidong.com/pytho...方法大致的過程就是以下幾步:
1.根據(jù)用戶傳入的func對象, 利用inspect取出對應(yīng)的源碼 2.通過exec利用源碼并且傳入全局命名空間, 重新編譯代碼:
# coding: utf8 import inspect # 裝飾函數(shù) def wrap(f): # 調(diào)用用戶傳入的函數(shù) source = inspect.getsource(f) # 獲取源碼 exec("%s %s()" % (source, f.func_name), {"a": "this is inspect", "__builtins__": {}}) # 重新編譯, 并且重新構(gòu)造全局命名空間 a = 123 # 用戶自定義函數(shù) def func(): print a import os print os.listdir(".") wrap(func) # 輸出 this is inspect Traceback (most recent call last): File "D:/Python_project/ftp/2.py", line 19, inwrap(func) File "D:/Python_project/ftp/2.py", line 8, in wrap exec("%s func()" % source, {"a": "this is inspect", "__builtins__": {}}) File " ", line 6, in File " ", line 3, in func ImportError: __import__ not found 雖然上面報錯了, 但那不就我們求之不得結(jié)果嗎? 我們可以正確的輸出a的值this is inspe, 而且當(dāng)func想import時, 直接報錯! 這樣就能滿足我們的{{BANNED}}欲望了~ 嘿嘿!,
關(guān)于代碼運(yùn)行原理, 其實(shí)在關(guān)鍵部位的代碼, 都已經(jīng)加了注釋, 可能在exec那部分會比較迷惑, 但其實(shí)大家將對應(yīng)的變量代入字符串就能懂了, 替換之后, 其實(shí)也就是函數(shù)的定義+執(zhí)行, 可以通過print "%s %s()" % (source, f.func_name)幫助理解.而后面的字典, 也就是我們一直很糾結(jié)的全局命名空間, 其中內(nèi)置命名空間也被人為定義了, 所以能夠達(dá)到我們想要的效果了!
這種只是一種拋磚引玉, 讓有類似場景需求的童鞋, 有個參考的方向, 也歡迎分享你們實(shí)現(xiàn)的方案, 嘿嘿!
歡迎各位大神指點(diǎn)交流,轉(zhuǎn)載請注明來源: https://segmentfault.com/a/11...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/38621.html
本文關(guān)鍵闡述了處理python遞歸函數(shù)及遞歸算法頻次受限制難題,具有非常好的實(shí)用價值,希望能幫助到大家。如有誤或者未考慮到真正的地區(qū),望鼎力相助 遞歸函數(shù)及遞歸算法頻次受限制 一個函數(shù)在外部啟用自身,那么這樣的函數(shù)是遞歸函數(shù)。遞歸算法要反復(fù)應(yīng)用自身,每遞歸算法一回,越近最后的值。如果一個難題需要由很多類似小問題處理,可以選擇應(yīng)用遞歸函數(shù)。伴隨著遞歸算法的深層次,難題經(jīng)營規(guī)模對比之前都應(yīng)該所...
文章主要是詳細(xì)介紹了pythonGUI多做輸入文本Text的控制方式,具有非常好的實(shí)用價值,希望能幫助到大家。如有誤或者未考慮到真正的地區(qū),望鼎力相助 Text的屬性wrap fromtkinterimport* root=Tk() root.geometry('200x300') te=Text(root,height=20,width=15) #將多做輸...
此篇文章主要是詳細(xì)介紹了pythonGUI多列輸入文本Text的控制方式,具有非常好的實(shí)用價值,希望能幫助到大家。如有誤或者未考慮到真正的地區(qū),望鼎力相助 Text的屬性wrap fromtkinterimport* root=Tk() root.geometry('200x300') te=Text(root,height=20,width=15) #將多...
對python調(diào)用類特性方式詳細(xì)描述檢驗(yàn)前提下類開啟也經(jīng)常需要用到的,下面文中重要給大家介紹了相關(guān)Python類屬性和方法的開啟的相關(guān)資料,從文中根據(jù)實(shí)例編號介紹的非常詳細(xì),務(wù)必的朋友可以參考一下 Python從技術(shù)的時候就已經(jīng)已是一類面向?qū)ο笳Z言表述,也正因?yàn)槿绱耍赑ython中打造一個類和對象是非常簡單的。 一、類、總體目標(biāo)概述 在C語言程序設(shè)計中,把數(shù)據(jù)和信息以及對業(yè)務(wù)操作流程封...
小編寫這篇文章的主要目的,就是給大家介紹關(guān)于Python標(biāo)準(zhǔn)庫sys常用功能的一些介紹,這樣對我們以后的工作也是很有幫助的,具體的介紹,下面就給大家詳細(xì)解答下。 1、查看版本信息 #coding:utf-8 importsys #獲取Python版本信息 print(sys.version) #獲取解釋器中C的API版本 print(sys.api_version) ...
閱讀 841·2021-11-15 17:58
閱讀 3641·2021-11-12 10:36
閱讀 3779·2021-09-22 16:06
閱讀 956·2021-09-10 10:50
閱讀 1325·2019-08-30 11:19
閱讀 3309·2019-08-29 16:26
閱讀 928·2019-08-29 10:55
閱讀 3341·2019-08-26 13:48