簡體   English   中英

識別python函數內全局變量的無意讀/寫? 例如使用靜態分析?

[英]Identify unintentional read/write of global variables inside a python function? For example using static analysis?

我對 python 感到沮喪的一件事是,如果我編寫這樣的函數:

def UnintentionalValueChangeOfGlobal(a):
    SomeDict['SomeKey'] = 100 + a
    b = 0.5 * SomeDict['SomeKey']
    return b

然后像這樣運行它:

SomeDict = {}
SomeDict['SomeKey'] = 0
b = UnintentionalValueChangeOfGlobal(10)
print(SomeDict['SomeKey'])

Python 將: 1) 在函數調用期間查找並使用SomeDict ,即使我忘記將其作為函數的輸入提供; 2) 永久更改SomeDict['SomeKey']的值,即使它不包含在函數的 return 語句中。

對我來說,這通常會導致變量無意中更改值 - 在這種情況下, SomeDict['SomeKey']在調用函數后變為 110,而目的是僅操作函數輸出b

在這種情況下,我更喜歡 python: 1) 崩潰並在函數內部出現錯誤,說明SomeDict未定義; 2) 在任何情況下都不會在調用函數后永久更改輸出b以外的任何變量的值。

我知道不可能在 python 中一起禁用全局變量,但是有沒有一種簡單的方法(模塊或 IDE 等)可以對我的 python 函數執行靜態分析並在函數使用時警告我和/或更改不是函數輸出的變量的值? 即,每當使用或操作不是函數本地的變量時警告我?

Python 沒有提供任何明顯且簡單的方法來防止訪問(未聲明的)函數中的全局名稱的原因之一是,在 Python 中,一切(好吧,至少可以分配給名稱的一切)都是一個對象,包括函數、類和模塊,因此阻止函數訪問未聲明的全局名稱會導致代碼非常冗長......嵌套范圍(閉包等)也無濟於事。

而且,當然,盡管全局對象是邪惡的,但有時仍然有合理的理由來改變全局對象。 FWIW,即使是短絨(至少是 pylint 和 pyflakes)似乎也沒有任何選項來檢測這個 AFAICT - 但你必須自己仔細檢查,因為我可能忽略了它,或者它可能作為 pylint 存在擴展或另一個短絨。

OTHO,在 20 多年的時間里,我很少遇到來自此類問題的錯誤(我實際上不記得發生過一次)。 定期應用基本的良好實踐——盡可能避免副作用的短函數、有意義的名稱和良好的命名約定等,至少對關鍵部分進行單元測試等——似乎足以有效地防止此類問題。

這里的要點之一是我有一條關於不可調用的全局變量被視為(偽)常量的規則,通過將它們命名為 ALL_UPPER 來表示。 這使得當你真正改變或重新綁定一個時變得非常明顯......

作為一個更一般的規則:Python 本質上是一種非常動態的語言(哎呀,你甚至可以在運行時更改對象的類......)並且具有“我們都同意成年人”的理念,所以它確實“缺乏” 您會在 Java 等更多“B&D”語言中找到的大多數安全防護措施都依賴於約定、良好實踐和普通常識。

現在,Python是不僅合租動態的,但也暴露了它的內件的多,所以你當然可以(如果尚不存在)寫一個pylint的擴展,將至少檢測功能代碼的全局名稱(提示:您可以訪問使用yourfunc.co_code (py2) 或yourfunc.__code__ (py3)編譯函數對象的代碼,然后檢查代碼中使用了哪些名稱)。 但是,除非您必須與一群草率無紀律的開發人員打交道(在這種情況下,您會遇到另一個問題 - 沒有技術解決愚蠢問題),否則我非常謙虛的意見是您在浪費時間。

理想情況下,我希望我正在尋找的全局檢查功能在 IDE 中實現,並持續用於評估函數中全局變量的使用。 但由於這似乎不存在,我將一個臨時函數放在一起,該函數將 python 函數作為輸入,然后查看該函數的字節碼指令以查看是否存在任何LOAD_GLOBALSTORE_GLOBAL指令。 如果找到,它會嘗試評估全局類型並將其與用戶提供的類型列表(int、float 等)進行比較。 然后打印出該函數使用的所有全局變量的名稱。

該解決方案遠非完美,而且很容易出現誤報。 例如,如果np.unique(x)import numpy as np之前在函數中使用( import numpy as np ),它將錯誤地將np識別為全局變量而不是模塊。 它也不會查看嵌套函數等。

但是對於像這篇文章中的示例這樣的簡單情況,它似乎工作正常。 我只是用它來掃描我代碼庫中的所有函數,它發現了另一個我不知道的全局用法——所以至少對我來說它很有用!

這是函數:

def CheckAgainstGlobals(function, vartypes):
    """
    Function for checking if another function reads/writes data from/to global
    variables. Only variables of the types contained within 'vartypes' and
    unknown types are included in the output.

     Inputs:
        function - a python function
        vartypes - a list of variable types (int, float, dict,...)
     Example:
        # Define a function
        def testfcn(a):
            a = 1 + b
            return a

        # Check if the function read/writes global variables.    
        CheckAgainstGlobals(testfcn,[int, float, dict, complex, str])

        # Should output:
        >> Global-check of function: testfcn
        >> Loaded global variable: b (of unknown type)
    """
    import dis
    globalsFound = []
    # Disassemble the function's bytecode in a human-readable form.
    bytecode = dis.Bytecode(function)
    # Step through each instruction in the function.
    for instr in bytecode:
        # Check if instruction is to either load or store a global.
        if instr[0] == 'LOAD_GLOBAL' or instr[0] == 'STORE_GLOBAL':
            # Check if its possible to determine the type of the global.
            try:
                type(eval(instr[3]))
                TypeAvailable = True
            except:
                TypeAvailable = False
            """
            Determine if the global variable is being loaded or stored and
            check if 'argval' of the global variable matches any of the 
            vartypes provided as input.
            """
            if instr[0] == 'LOAD_GLOBAL':
                if TypeAvailable:
                    for t in vartypes:
                        if isinstance(eval(instr[3]), t):
                            s = ('Loaded global variable: %s (of type %s)' %(instr[3], t))
                            if s not in globalsFound:
                                globalsFound.append(s)
                else:
                    s = ('Loaded global variable: %s (of unknown type)' %(instr[3]))
                    if s not in globalsFound:
                        globalsFound.append(s)
            if instr[0] == 'STORE_GLOBAL':
                if TypeAvailable:
                    for t in vartypes:
                        if isinstance(eval(instr[3]), t):
                            s = ('Stored global variable: %s (of type %s)' %(instr[3], t))
                            if s not in globalsFound:
                                globalsFound.append(s)
                else:
                    s = ('Stored global variable: %s (of unknown type)' %(instr[3]))
                    if s not in globalsFound:
                        globalsFound.append(s)
    # Print out summary of detected global variable usage.
    if len(globalsFound) == 0:
        print('\nGlobal-check of fcn: %s. No read/writes of global variables were detected.' %(function.__code__.co_name))
    else:
        print('\nGlobal-check of fcn: %s' %(function.__code__.co_name))
        for s in globalsFound:
            print(s)

當在函數聲明后直接在示例中的函數上使用時,它會發現關於全局變量SomeDict的使用的警告,但它不會知道它的類型:

def UnintentionalValueChangeOfGlobal(a):
    SomeDict['SomeKey'] = 100 + a
    b = 0.5 * SomeDict['SomeKey']
    return b
# Will find the global, but not know its type.
CheckAgainstGlobals(UnintentionalValueChangeOfGlobal,[int, float, dict, complex, str])

>> Global-check of fcn: UnintentionalValueChangeOfGlobal
>> Loaded global variable: SomeDict (of unknown type)

在定義SomeDict之后使用時,它還會檢測到全局是一個字典:

SomeDict = {}
SomeDict['SomeKey'] = 0
b = UnintentionalValueChangeOfGlobal(10)
print(SomeDict['SomeKey'])
# Will find the global, and also see its type.
CheckAgainstGlobals(UnintentionalValueChangeOfGlobal,[int, float, dict, complex, str])

>> Global-check of fcn: UnintentionalValueChangeOfGlobal
>> Loaded global variable: SomeDict (of type <class 'dict'>)

注意:在當前狀態下,該函數無法檢測SomeDict['SomeKey']更改值。 即,它只檢測加載指令,而不是操作全局的先前值。 這是因為在這種情況下似乎使用指令STORE_SUBSCR而不是STORE_GLOBAL 但是仍然檢測到全局的使用(因為它正在加載),這對我來說已經足夠了。

您可以使用 globals() 檢查變量:

def UnintentionalValueChangeOfGlobal(a):

    if 'SomeDict' in globals():
        raise Exception('Var in globals')

    SomeDict['SomeKey'] = 100 + a
    b = 0.5 * SomeDict['SomeKey']
    return b

SomeDict = {}
SomeDict['SomeKey'] = 0
b = UnintentionalValueChangeOfGlobal(10)
print(SomeDict['SomeKey'])

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM