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:
- Execution model — Python 2.7.15 documentation
- Why am I getting an UnboundLocalError when the variable has a value? — Python 2.7.15 documentation
- How to define free-variable in python?
- python counter with closure