[Python-au] strange pre-acting bug

Ryan Kelly ryan at rfk.id.au
Tue May 18 23:40:56 UTC 2010

On Wed, 2010-05-19 at 09:09 +1000, PeterL wrote:
> Interesting. This is a common mis-understanding on the scoping rules of 
> Python functions. I might do a lightning talk on it at PyCon.
> You need to be clear on the error. An 'UnboundLocalError' means that you 
> are using a mainline variable inside a function before you change it.
> In Python, a function *can* _read_ a mainline's variable with no problem.
> If a function goes to change a mainline's variable, then it become 
> 'bound local', which means you must give it a value before you use it.

It might also be instructive to look at the bytecode that python
generates for each of these cases (or it might be that I just love
looking at bytecode...)

> Compare:
> -----------
> # Mainline variable available to a fn
> def fn():
>      print x    # Prints 5

>>> import dis
>>> dis.dis(fn)
  2           0 LOAD_GLOBAL              0 (x)
              3 PRINT_ITEM          
              4 PRINT_NEWLINE       
              5 LOAD_CONST               0 (None)
              8 RETURN_VALUE        

Since x is not given a value inside the function, it is accessed as a
global using the "LOAD GLOBAL" bytecode.

> ----------
> # Mainline variables can be localised inside a fn
> def fn():
>      x = 7
>      print x    # Prints 7
>      # Throw the localised value of x away

>>> dis.dis(fn)
  2           0 LOAD_CONST               1 (7)
              3 STORE_FAST               0 (x)

  3           6 LOAD_FAST                0 (x)
              9 PRINT_ITEM          
             10 PRINT_NEWLINE       
             11 LOAD_CONST               0 (None)
             14 RETURN_VALUE       

Since x is now given a value inside the function, it is treated as a
local variable and loaded using the "LOAD_FAST" bytecode.  Note also the
"STORE_FAST" opcode which sets the value of the local variable.

This is an optimisation by the python interpreter.  Accessing local
variables is actually a lot faster than accessing globals (basically,
it's an array lookup rather than a hash-table lookup).

> ----------
> # Local bound variables must be set before they're used
> def fn():
>      print x    # Error - UnboundLocalError - mainline variables that 
> are localised must be set before used
>      x = 7     # Localise x
>      print x    # Never reaches here

>>> dis.dis(fn)
  2           0 LOAD_FAST                0 (x)
              3 PRINT_ITEM          
              4 PRINT_NEWLINE       

  3           5 LOAD_CONST               1 (7)
              8 STORE_FAST               0 (x)

  4          11 LOAD_FAST                0 (x)
             14 PRINT_ITEM          
             15 PRINT_NEWLINE       
             16 LOAD_CONST               0 (None)
             19 RETURN_VALUE        

Whoops!  We now have a "LOAD_FAST" without a preceding "STORE_FAST",
which produces the UnboundLocalError you are seeing.

I've always considered this something of a "leaky optimisation" on the
part of the python interpreter, but once you know the rule it's quite
consistent and easy to avoid.

Basically, every variable has a single unambiguous scope and that scope
cannot change during function execution.  Assigning to an otherwise
undeclared variable makes its scope local.



Ryan Kelly
http://www.rfk.id.au  |  This message is digitally signed. Please visit
ryan at rfk.id.au        |  http://www.rfk.id.au/ramblings/gpg/ for details

-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 197 bytes
Desc: This is a digitally signed message part
URL: <http://starship.python.net/pipermail/python-au/attachments/20100519/9bbb0f3e/attachment.pgp>

More information about the python-au mailing list