surprising interaction between function scope and class namespace

Peter Otten __peter__ at web.de
Mon Aug 15 06:42:01 EDT 2011


Stefan Behnel wrote:

> Hi,
> 
> I just stumbled over this:
> 
>    >>> A = 1
>    >>> def foo(x):
>    ...     A = x
>    ...     class X:
>    ...         a = A
>    ...     return X
>    ...
>    >>> foo(2).a
>    2
>    >>> def foo(x):
>    ...     A = x
>    ...     class X:
>    ...         A = A
>    ...     return X
>    ...
>    >>> foo(2).A
>    1

That's subtle.
 
> Works that way in Py2.7 and Py3.3.
> 
> I couldn't find any documentation on this, but my *guess* about the
> reasoning is that the second case contains an assignment to A inside of
> the class namespace, and assignments make a variable local to a scope, in
> this case, the function scope. Therefore, the A on the rhs is looked up in
> that scope as well. However, this is just a totally hand waving guess.

> Does anyone have a better explanation or know of a place where this
> specific behaviour is documented?

I think it's an implementation accident.

Classes have a special opcode, LOAD_NAME, that allows for

>>> x = 42
>>> class A:
...     x = x
...
>>> A.x
42

which would fail in a function

>>> def f():
...     x = x
...
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
UnboundLocalError: local variable 'x' referenced before assignment

LOAD_NAME is pretty dumb, it looks into the local namespace and if that 
lookup fails falls back to the global namespace. Someone probably thought "I 
can do better", and reused the static name lookup for nested functions for 
names that occur only on the right-hand side of assignments in a class.

Here's a slightly modified version of your demo:

>>> x = "global"                                                      
>>> def foo():                                                        
...     x = "local"                                                   
...     class A:                                                      
...             x = x                                                 
...     return A                                                      
...                                                                   
>>> def bar():                                                        
...     x = "local"                                                   
...     class A:                                                      
...             y = x                                                 
...     return A                                                      
...                                                                   
>>> foo().x
'global'   
>>> bar().y
'local'    

Now let's have a glimpse at the bytecode:

>>> import dis
>>> foo.func_code.co_consts
(None, 'local', 'A', <code object A at 0x7ffe311bdb70, file "<stdin>", line 
3>, ())
>>> dis.dis(foo.func_code.co_consts[3])
  3           0 LOAD_NAME                0 (__name__)
              3 STORE_NAME               1 (__module__)

  4           6 LOAD_NAME                2 (x)
              9 STORE_NAME               2 (x)
             12 LOAD_LOCALS                   
             13 RETURN_VALUE                  
>>> bar.func_code.co_consts      
(None, 'local', 'A', <code object A at 0x7ffe311bd828, file "<stdin>", line 
3>, ())
>>> dis.dis(bar.func_code.co_consts[3])
  3           0 LOAD_NAME                0 (__name__)
              3 STORE_NAME               1 (__module__)

  4           6 LOAD_DEREF               0 (x)
              9 STORE_NAME               2 (y)
             12 LOAD_LOCALS                   
             13 RETURN_VALUE                  





More information about the Python-list mailing list