Search code examples
pythonpython-3.xlambdaclosuresfirst-class-functions

Python 3 parameter closed upon by inner/nested method falls out of scope and triggers UnboundLocalError


Edit: Why are people downvoting this post? Are Python developers really this inept? It's a legitimate question, not one that's been answered in other places. I searched for a solution. I'm not an idiot. One parameter has a value and the other one is undefined, but if you actually read the post, you will see that both of them appear to be equally scoped.

First of all, I assure you that this question is unlike other questions involving the error message:

UnboundLocalError: local variable referenced before assignment closure method

As I'm looking at this code, it appears that the parameter, uuidString, of the top-level method, getStockDataSaverFactory, should actually be in-scope when the method returns its inner method, saveData, as a first-class function object... because to my amazement, the parameter tickerName IS in-scope and does have the value 'GOOG' when the saveData() method is called (e.g. by the test method testDataProcessing_getSaverMethodFactory), so we can actually see that it has an actual value when the method, getDataMethodFactory(..) is called, unlike uuidString.

To make the matter more obvious, I added the lines:

localUuidString = uuidString

and

experimentUuidString = localUuidString

to show that the parameter uuidString has an available value when the method is inspected by a breakpoint.

def getStockDataSaverFactory(self, tickerName, uuidString, methodToGetData, columnList):
    # This method expects that methodToGetData returns a pandas dataframe, such as the method returned by: self.getDataFactory(..)
    localUuidString = uuidString
    def saveData():
        (data, meta_data) = methodToGetData()
        experimentUuidString = localUuidString
        methodToNameFile = self.getDataMethodFactory(tickerName, uuidString)
        (full_filepathname, full_filename, uuidString) = methodToNameFile()
        methodToSaveData = self.getDataFrameSaverFactory(methodToGetData, columnList, full_filepathname)
        # We might want try/catch here:
        methodToSaveData()
        # A parameterless method that has immutable state (from a closure) is often easier to design around than one that expects parameters when we want to pass it with a list of similar methods
        return (full_filepathname, full_filename, uuidString)
    return saveData


def testDataProcessing_getSaverMethodFactory(self):
    dataProcessing = DataProcessing()
    getSymbols = dataProcessing.getSymbolFactory(
        dataProcessing.getNasdaqSymbols(dataProcessing.getListOfNASDAQStockTickers))
    tickers = getSymbols()
    uuidString = 'FAKEUUID'
    columnList = ['low', 'high']
    tickerSubset = tickers[0:2]
    methodsToPullData = map(lambda ticker: dataProcessing.getStockDataSaverFactory(ticker,
                                                                         uuidString,
                                                                         dataProcessing.getDataFactory(
                                                                             ticker),
                                                                         columnList), tickerSubset)
    savedPathTuples = [f() for f in methodsToPullData]
    savedFileNames = [pathTuple[0] for pathTuple in savedPathTuples]


    for fileName in savedFileNames:
        self.assertTrue(os.path.isfile(fileName))
        os.remove(fileName)

Just to make it clear that uuidString has no value but ticker does have a value, I'm including this screenshot:

Screenshot of PyCharm with breakpoint

Notice that in the variable watch window, uuidString is undefined, but ticker has the string value of "A".

Is there something unique about Python (or Python 3) that is resulting in this behavior?


Solution

  • The problem is that you reference uuidString in the call to self.getMethodThatProvidesFullFilePathNameForPricesCsvFromUUIDAndTickerName before you assign to it. The assignment makes it local to the scope of the innermost function and therefore, it is unassigned when you reference it.

    A full description of the scoping rules is provided by: https://stackoverflow.com/a/292502/7517724

    This simpler example reproduces your error to make the problem more clear:

    class aclass():
    
        def outer(self, uuidString):
            def inner():
                print(uuidString)
                uuidString = 'new value'
                return uuidString
            return inner
    
    a = aclass()
    func = a.outer('a uuid')
    val = func()
    print(val)
    

    The assignment in inner() causes the uuidString to be local to inner() and therefore it is unassigned when the print(uuidString) is call, which causes Python to raise the UnboundLocalError.

    You can fix the error by passing the variable in to your function with a default argument. Changing the definition of saveData to pass uuidString as a default argument, as:

    def saveData(uuidString=uuidString):
    

    will make it work as you expect.