Python: 受限制的 "函數調用"
(點擊
上方藍字
,快速關注我們)
來源:Lin_R
segmentfault.com/a/1190000009878322
如有好文章投稿,請點擊 → 這裡了解詳情
需求背景
最近在工作上, 遇到了一個比較特殊的需求:
為了安全, 設計一個函數或者裝飾器, 然後用戶在 「定義/調用」 函數時, 只能訪問到我們允許的內置變數和全局變數。
通過例子來這解釋下上面的需求:
a
=
123
def
func
()
:
a
id
(
a
)
func
()
# 輸出
123
32081168
函數功能簡單明了, 對於結果, 大家應該也不會有太大的異議:func分別是取得全局命名空間中a的值和使用內置命名空間中的函數id獲取了a的地址. 熟悉Python的童鞋, 對於LEGB肯定也是不陌生的,也正是因為LEGB才讓函數func輸出正確的結果. 但是這個只是一個常規例子, 只是用來拋磚引玉而已. 我們真正想要討論的是下面的例子:
# 裝飾函數
def
wrap
(
f
)
:
# 調用用戶傳入的函數
f
()
a
=
123
# 用戶自定義函數
def
func
()
:
import
os
os
.
listdir
(
"."
)
wrap
(
func
)
# 輸出
[
"1.yml"
,
"2.py"
,
"2.txt"
,
"2.yml"
,
"ftp"
,
"ftp.rar"
,
"test"
,
"tmp"
,
"__init__.py"
]
潛在危險因素
在上面的例子可以看出, 如果在func中, 引入別的模塊, 然後再執行模塊中的方法, 也是可行的! 而且這還是一個非常方便的功能! 但是除了方便, 更多的是一種潛在的危險.在日常使用, 或許我們不會考慮這些, 但是如果在模塊與模塊之間的協同作用時, 特別是多人參與的情況下, 這種危險的因素, 就不得不讓我們認真對待!
或許有很多同學會覺得這些擔憂是過多的, 是沒必要的, 但是請思考一種場景: 我們有個主模塊, 暫時稱為main.py, 它允許用戶動態載入模塊, 也就是說只要用戶將對應的模塊放到對應的目錄, 然後利用消息機制去通知main.py, 告訴它應該載入新模塊了, 並且執行新模塊裡面的b函數, 那在這種情況下, main.py肯定不能直接傻傻的就去執行, 因為我們不能相信每個用戶都是誠實善良的, 也不能相信每個用戶編寫的模塊或者函數是符合我們的行為標準規範. 所以我們得有些措施去防範這些事情, 我們能做的大概也就下面幾種方式:
在用戶通知main.py時有新模塊加入並且要求執行函數時, 先對模塊的代碼做檢查, 不符合標準或者帶有危險代碼的拒絕載入.
控制好內置命名空間和全局命名空間, 使其只能用允許使用的內容
在方案1, 其實也是我們最容易想到的方法, 但是這個方法的成本還是比較高, 因為我們需要將可能出現的錯誤代碼或者關鍵詞,全部寫成一套規則, 而且這套規則還很大可能會誤傷, 不過也可能業界已經有類似的成熟的方案, 只是我還沒接觸到而已.
所以我們只能用方案2的方法, 這種方法在我們看來, 是成本比較低的, 也比較容易控制的, 因為這就和防火牆一樣, 我們只放行我們允許的事物.
具體實現
實現方案2最大的問題就是, 如何控制內置命名空間 和全局命名空間
我們第一個想法肯定就是覆蓋它們, 因為我們都知道不管是內置命名空間還是全局命名空間, 都是通過字典的形式在維護:
globals
()
globals
()[
"__builtins__"
].
__dict__
# 輸出
# 全局命名空間
{
"__builtins__"
: <
module
"__builtin__"
(
built
-
in
)
>
,
"__name__"
:
"__main__"
,
"__file__"
:
"D:/Python_project/ftp/2.py"
,
"__doc__"
:
None
,
"__package__"
:
None
}
#內置命名空間
{
"bytearray"
: <
type
"bytearray"
>
,
"IndexError"
: <
type
"
excep
.....(
省略過多部分)..}
注: globals函數 是用來列印當前全局命名空間的函數, 同樣, 也能通過修改這個函數返回的字典對應的key, 實現全局命名空間的修改.例如:
s
=
globals
()
s
s
[
"a"
]
=
3
s
a
# 輸出
{
"__builtins__"
: <
module
"__builtin__"
(
built
-
in
)
>
,
"__file__"
:
"D:/Python_project/ftp/2.py"
,
"__package__"
:
None
,
"s"
:
{...},
"__name__"
:
"__main__"
,
"__doc__"
:
None
}
{
"a"
:
3
,
"__builtins__"
: <
module
"__builtin__"
(
built
-
in
)
>
,
"__file__"
:
"D:/Python_project/ftp/2.py"
,
"__package__"
:
None
,
"s"
:
{...},
"__name__"
:
"__main__"
,
"__doc__"
:
None
}
3
可以看出, 我們並沒有定義變數a, 只是在globals的返回值上面增加了key-value, 就變相實現了我們定義的操作, 這其實也能用於很多希望能夠動態賦值的需求場景! 比如說, 我不確定有多少個變數, 希望通過一個變數名列表, 動態生成這些變數, 在這種情況下, 就能參考這種方法, 不過還是希望謹慎使用, 因為修改了這個, 就是就修改了全局命名空間.
好了, 回歸到本文, 我們已經知道通過globals函數能夠代表全局命名空間, 但是為什麼內置命名空間要用globals()[『__builtins__』].__dict__來表示? 其實這個和python自身的機制有關, 因為模塊在編譯和初始化的過程中, 內置命名空間就是以這種形式,寄放在全局命名空間:
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__的動作, 如果這個結果是NULL, 那麼就會用之前初始化好的__builtin__去存進去, 這些代碼具體可以看Pythonrun.c, 在這不詳細展開了.
既然內置命名空間(__builtins__)和全局命名空間(globals())都已經找到對應對象了, 那我們下一步就應該是想法將這兩個空間替換成我們想要的.
# coding: utf8
# 修改全局命名空間
test_var
=
123
# 測試變數
tmp
=
globals
().
keys
()
globals
()
test_var
for
i
in
tmp
:
del
globals
()[
i
]
globals
()
test_var
id
(
2
)
# 輸出
{
"tmp"
:
[
"__builtins__"
,
"__file__"
,
"__package__"
,
"test_var"
,
"__name__"
,
"__doc__"
],
"__builtins__"
: <
module
"__builtin__"
(
built
-
in
)
>
,
"__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
<
module
>
test_var
NameError
:
name
"test_var"
is
not
defined
在上面的輸出可以看到, 在刪除前後, 通過print globals()可以看到全局命名空間確實已經被修改了, 因為test_var已經無法列印了, 觸發了NameError, 這樣的話, 就有辦法能夠限制全局命令空間了:
# 偽代碼
# 裝飾函數
def
wrap
(
f
)
:
# 調用用戶傳入的函數
....
修改全局命名空間
f
()
....
還原全局命名空間
a
=
123
# 用戶自定義函數
def
func
()
:
import
os
os
.
listdir
(
"."
)
wrap
(
func
)
為什麼我只寫偽代碼, 因為我發現這個功能實現起來是非常蛋疼! 原因就是, 在實現之前, 我們必須要解決幾個問題:
全局命名空間對應了一個字典, 所以如果我們想要修改, 只能從修改這個字典本身, 於是先清空再定義成我們約束的, 調用完之後, 又得反過來恢復, 這些操作是十分之蛋疼.
涉及到共享的問題, 如果這個用戶函數處理很久, 而且是多線程的, 那麼整個模塊都會變得很不穩定, 甚至稱為」污染」
那就先撇開不講, 講講內置命名空間, 剛才我們已經找到了能代表內置命名空間的對象, 很幸運的是, 這個是」真的能夠摸得到」的, 那我們試下直接就賦值個空字典, 看會怎樣:
s
=
globals
()
s
[
"__builtins__"
]
# __builtins__檢查是否存在
s
[
"__builtins__"
]
=
{}
s
[
"__builtins__"
]
# __builtins__檢查是否存在
id
(
3
)
# 試下內置函數能否使用
globals
()
# 輸出
<
module
"__builtin__"
(
built
-
in
)
>
{}
32602360
{
"__builtins__"
:
{},
"__file__"
:
"D:/Python_project/ftp/2.py"
,
"__package__"
:
None
,
"s"
:
{...},
"__name__"
:
"__main__"
,
"__doc__"
:
None
}
結果有點尷尬, 似乎沒啥用, 但是其實這個__builtins__只是一個表現, 真正的內置命名空間是在它所指向的字典對象, 也就是: globals()[『__builtins__』].__dict__!
globals
()[
"__builtins__"
].
__dict__
# 輸出
{
"bytearray"
: <
type
"bytearray"
>
,
"IndexError"
: <
type
"exceptions.IndexError"
>
....}
# 省略
所以我們真正要覆蓋的, 是這個字典才對, 所以上面的代碼要改成:
s
=
globals
()
s
[
"__builtins__"
].
__dict__
=
{}
# 覆蓋真正的內置命名空間
s
[
"__builtins__"
].
__dict__
# __builtins__檢查是否存在
# 輸出
Traceback
(
most recent call
last
)
:
File
"D:/Python_project/ftp/2.py"
,
line
3
,
in
<
module
>
s
[
"__builtins__"
].
__dict__
=
{}
TypeError
:
readonly
attribute
失敗了…原來這個內置命名空間是只讀的, 所以我們上面的方法都失敗了..那難道真的沒法解決了嗎? 一般這樣問, 通常都有解決方案滴~
完美方案
這個解決方法, 需要一個庫的幫忙~, 那就是inspect庫, 這個庫是幹嘛呢? 簡單來說就是用來自省. 它提供四種用處:
對是否是模塊,框架,函數等進行類型檢查。
獲取源碼
獲取類或函數的參數的信息
解析堆棧
在這裡, 我們需要用到第二個功能, 其餘的功能, 感興趣的童鞋可以去谷歌學習哦, 也可以參考: https://my.oschina.net/taisha/blog/55597
除了inspect, 我們還需要用到exec, 這也是一大殺器, 可以先參考這個學習下: http://www.mojidong.com/python/2013/05/10/python-exec-eval/
方法大致的過程就是以下幾步:
根據用戶傳入的func對象, 利用inspect取出對應的源碼
通過exec利用源碼並且傳入全局命名空間, 重新編譯
代碼:
# coding: utf8
import
inspect
# 裝飾函數
def
wrap
(
f
)
:
# 調用用戶傳入的函數
source
=
inspect
.
getsource
(
f
)
# 獲取源碼
exec
(
"%s
%s()"
%
(
source
,
f
.
func_name
),
{
"a"
:
"this is inspect"
,
"__builtins__"
:
{}})
# 重新編譯, 並且重新構造全局命名空間
a
=
123
# 用戶自定義函數
def
func
()
:
a
import
os
os
.
listdir
(
"."
)
wrap
(
func
)
# 輸出
this
is
inspect
Traceback
(
most recent call
last
)
:
File
"D:/Python_project/ftp/2.py"
,
line
19
,
in
<
module
>
wrap
(
func
)
File
"D:/Python_project/ftp/2.py"
,
line
8
,
in
wrap
exec
(
"%s
func()"
%
source
,
{
"a"
:
"this is inspect"
,
"__builtins__"
:
{}})
File
"<string>"
,
line
6
,
in
<
module
>
File
"<string>"
,
line
3
,
in
func
ImportError
:
__import__
not
found
雖然上面報錯了, 但那不就我們求之不得結果嗎? 我們可以正確的輸出a的值this is inspe, 而且當func想import時, 直接報錯! 這樣就能滿足我們的變態慾望了~ 嘿嘿!,
關於代碼運行原理, 其實在關鍵部位的代碼, 都已經加了注釋, 可能在exec那部分會比較迷惑, 但其實大家將對應的變數代入字元串就能懂了, 替換之後, 其實也就是函數的定義+執行, 可以通過print 『%s
%s()』 % (source, f.func_name)幫助理解.而後面的字典, 也就是我們一直很糾結的全局命名空間, 其中內置命名空間也被人為定義了, 所以能夠達到我們想要的效果了!
這種只是一種拋磚引玉, 讓有類似場景需求的童鞋, 有個參考的方向, 也歡迎分享你們實現的方案, 嘿嘿!
看完本文有收穫?請轉
發分享給更多人
關注「P
ython開發者」,提升Python技能
※玩轉面試演算法,帶你佔領BAT!
※用神經網路訓練一個文本分類器
※深入理解 Python 非同步編程(上)
TAG:Python開發者 |
※Python 3 print 函數用法總結
※Python中的lambda函數
※使用Istio控制Serverless架構Fn Project中的函數間流量路由
※Python的"print「函數在」Hello World"之外的延伸
※Python的"print「函數在」Hello World"之外的延伸
※request,ProxyHandle與HttpCookiProcessor函數用法
※基於fminsearch的約束優化函數fminsearchbnd, fminsearchcon
※揭秘 Python 中的 enumerate 函數
※學習Python必知的Numpy函數
※excel函數left與right的使用教程
※Python 特殊函數(lambda,map,filter,reduce)
※python實現並繪製 sigmoid函數,tanh函數,ReLU函數,PReLU函數
※mysql中concat函數,concat_ws函數,concat_group函數之間的區別
※徹底理解 Node.js 中的回調(Callback)函數
※在Python中如何使用sorted()和sort()函數
※python中的函數
※DeBug Python代碼全靠print函數?換用這個一天2K+Star的工具吧
※LoadRunner常用函數匯總
※在Python中定義Main函數
※NPM酷庫:pify 將非同步函數Promise化