Search code examples
python-2.7scopeinstance-variables

Python2 referenced before assignment?


This gives me a referenced before assignment error for p1 but not for count?

class Bagadclass:
    def __init__(self):
        print "i n s t a n t i a t e d"
        pass

    def getRegion(self):
        count = [0]
        p1 = [0 , 0]
        p2 = [0 , 0]

        def on_click(x,y,button,pressed):
            count[0] += 1

            if count[0] > 5:
                self.Image = pyautogui.screenshot(region=p1+p2)
                listener.stop()
                pass

            if count[0] == 2:
                p1 = list(pyautogui.position())
                pass

            if count[0] == 4:
                print p1
                p2 = (pyautogui.position()[0] - p1[0] , pyautogui.position()[1] - p1[1])
                pass

            print count , x , y , button , pressed
            pass

        with Listener(on_click=on_click) as listener:
            listener.join()
        pass

Can anyone explain why? I think this is about the listener object or something but if it worked for count i think it should also work for the other variables.


Solution

  • The function on_click() is a closure, it has access to the variables count, p1, and p2, defined in the enclosing scope, getRegion(). These variables, (count, p1, p2) are known as free variables*, "If a variable is used in a code block but not defined there, it is a free variable" Link 1.

    *Actually, in your example, only count is a free variable; p1 and p2 are not free variables, as you have defined them in the local scope of on_click():

    ...
    
    if count[0] == 2:
        p1 = list(pyautogui.position()) # <-- here
        pass
    
    if count[0] == 4:
        print p1
        p2 = (pyautogui.position()[0] - p1[0] , pyautogui.position()[1] - p1[1]) # <-- and here
    
    ...
    

    And so are no longer free variables, they are now local variables. In each block, a name is either a local variable, a global variable or a free variable. A name does not transform from a free variable to a local variable after the assignment statement, instead, if an assignment statement appears anywhere within a block, the name is now a local variable. If you try to access a local variable that has not yet been bound (i.e. code execution has not yet reached the assignment statement of the local variable), you will get an UnboundLocalError exception.

    From Execution model — Python 2.7.15 documentation:

    If a name binding operation occurs anywhere within a code block, all uses of the name within the block are treated as references to the current block. This can lead to errors when a name is used within a block before it is bound. This rule is subtle. Python lacks declarations and allows name binding operations to occur anywhere within a code block. The local variables of a code block can be determined by scanning the entire text of the block for name binding operations.

    In essence, this basically means you can 'access' free variables (including modify/mutate them) but you can't 'write'/rebind them, Python will interpret this as defining an entirely new local variable. In Python 3, you can resolve this by using the nonlocal keyword, otherwise a common practice is to wrap your data in another structure (e.g. dictionary or list) and then modify the structure to alter your data instead; this way, you are not attempting to rebind the free variable, merely modifying the object it refers to.

    This is in fact what has already been done for the count variable, the actual count value is stored in a single element list and when on_click() needs to increment the count, it increments the first element of the count list (count[0]+=1), instead of trying to rebind it (something like count+=1 would not work).


    Example of how you might revise your code (# ! indicates changes):

    ...
    
        def getRegion(self):
            count = [0]
            p = [[0, 0], [0, 0]]  # [p1, p2]  # !
            def on_click(x,y,button,pressed):
                count[0] += 1
                if count[0] > 5:
                    self.Image = pyautogui.screenshot(region=p[0]+p[1])  # !
    
    ...
    
                if count[0] == 2:
                    p[0] = list(pyautogui.position())  # !
    
    ...
    
                if count[0] == 4:
                    print p[0] # !
                    p[1] = (pyautogui.position()[0] - p[0][0],
                            pyautogui.position()[1] - p[0][1])  # !
    
    ...
    

    Links:

    1. Execution model — Python 2.7.15 documentation
    2. Why am I getting an UnboundLocalError when the variable has a value? — Python 2.7.15 documentation
    3. How to define free-variable in python?
    4. python counter with closure