Accessing class variables from a list comprehension in the class definition
Asked Answered
C

9

248

How do you access other class variables from a list comprehension within the class definition? The following works in Python 2 but fails in Python 3:

class Foo:
    x = 5
    y = [x for i in range(1)]

Python 3.11 gives the error:

NameError: name 'x' is not defined

Trying Foo.x doesn't work either. Any ideas on how to do this in Python 3?

A slightly more complicated motivating example:

from collections import namedtuple

class StateDatabase:
    State = namedtuple('State', ['name', 'capital'])
    db = [State(*args) for args in [
        ['Alabama', 'Montgomery'],
        ['Alaska', 'Juneau'],
        # ...
    ]]

In this example, apply() would have been a decent workaround, but it is sadly removed from Python 3.

Cohe answered 16/12, 2012 at 21:42 Comment(3)
Interesting... One obvious workaround is to assign y after you exit the class definition. Foo.y = [Foo.x for i in range(1)]Jennee
+martijn-pieters link to a duplicate is right, there's a comment from +matt-b in there with the explanation: Python 2.7 list comprehensions do not have their own namespace (unlike set or dict comprehensions or generator expressions... replace your [] with {} to see that in action). They all have their own namespace in 3.Jennee
@gps: Or use a nested scope, by inserting a (temporary) function in the class definition suite.Cadaverous
C
356

Class scope and list, set or dictionary comprehensions, as well as generator expressions, do not mix.

The why; or, the official word on this

You cannot access the class scope from functions, list comprehensions or generator expressions enclosed in that scope; they act as if that scope does not exist. In Python 2, list comprehensions were implemented using a shortcut so actually could access the class scope, but in Python 3 they got their own scope (as they should have had all along) and thus your example breaks. Other comprehension types have their own scope regardless of Python version, so a similar example with a set or dict comprehension would break in Python 2.

# Same error, in Python 2 or 3
y = {x: x for i in range(1)}

More details

In Python 3, list comprehensions were given a proper scope (local namespace) of their own, to prevent their local variables bleeding over into the surrounding scope (see List comprehension rebinds names even after scope of comprehension. Is this right?). That's great when using such a list comprehension in a module or in a function, but in classes, scoping is a little, uhm, strange.

This is documented in pep 227:

Names in class scope are not accessible. Names are resolved in the innermost enclosing function scope. If a class definition occurs in a chain of nested scopes, the resolution process skips class definitions.

and in the class compound statement documentation:

The class’s suite is then executed in a new execution frame (see section Naming and binding), using a newly created local namespace and the original global namespace. (Usually, the suite contains only function definitions.) When the class’s suite finishes execution, its execution frame is discarded but its local namespace is saved. [4] A class object is then created using the inheritance list for the base classes and the saved local namespace for the attribute dictionary.

Emphasis mine; the execution frame is the temporary scope.

Because the scope is repurposed as the attributes on a class object, allowing it to be used as a nonlocal scope as well leads to undefined behaviour; what would happen if a class method referred to x as a nested scope variable, then manipulates Foo.x as well, for example? More importantly, what would that mean for subclasses of Foo? Python has to treat a class scope differently as it is very different from a function scope.

Last, but definitely not least, the linked Naming and binding section in the Execution model documentation mentions class scopes explicitly:

The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods – this includes comprehensions and generator expressions since they are implemented using a function scope. This means that the following will fail:

class A:
     a = 42
     b = list(a + i for i in range(10))

The (small) exception; or, why one part may still work

There's one part of a comprehension or generator expression that executes in the surrounding scope, regardless of Python version. That would be the expression for the outermost iterable. In your example, it's the range(1):

y = [x for i in range(1)]
#               ^^^^^^^^

Thus, using x in that expression would not throw an error:

# Runs fine
y = [i for i in range(x)]

This only applies to the outermost iterable; if a comprehension has multiple for clauses, the iterables for inner for clauses are evaluated in the comprehension's scope:

# NameError
y = [i for i in range(1) for j in range(x)]
#      ^^^^^^^^^^^^^^^^^ -----------------
#      outer loop        inner, nested loop

This design decision was made in order to throw an error at genexp creation time instead of iteration time when creating the outermost iterable of a generator expression throws an error, or when the outermost iterable turns out not to be iterable. Comprehensions share this behavior for consistency.

Looking under the hood; or, way more detail than you ever wanted

You can see this all in action using the dis module. I'm using Python 3.3 in the following examples, because it adds qualified names that neatly identify the code objects we want to inspect. The bytecode produced is otherwise functionally identical to Python 3.2.

To create a class, Python essentially takes the whole suite that makes up the class body (so everything indented one level deeper than the class <name>: line), and executes that as if it were a function:

>>> import dis
>>> def foo():
...     class Foo:
...         x = 5
...         y = [x for i in range(1)]
...     return Foo
... 
>>> dis.dis(foo)
  2           0 LOAD_BUILD_CLASS     
              1 LOAD_CONST               1 (<code object Foo at 0x10a436030, file "<stdin>", line 2>) 
              4 LOAD_CONST               2 ('Foo') 
              7 MAKE_FUNCTION            0 
             10 LOAD_CONST               2 ('Foo') 
             13 CALL_FUNCTION            2 (2 positional, 0 keyword pair) 
             16 STORE_FAST               0 (Foo) 

  5          19 LOAD_FAST                0 (Foo) 
             22 RETURN_VALUE         

The first LOAD_CONST there loads a code object for the Foo class body, then makes that into a function, and calls it. The result of that call is then used to create the namespace of the class, its __dict__. So far so good.

The thing to note here is that the bytecode contains a nested code object; in Python, class definitions, functions, comprehensions and generators all are represented as code objects that contain not only bytecode, but also structures that represent local variables, constants, variables taken from globals, and variables taken from the nested scope. The compiled bytecode refers to those structures and the python interpreter knows how to access those given the bytecodes presented.

The important thing to remember here is that Python creates these structures at compile time; the class suite is a code object (<code object Foo at 0x10a436030, file "<stdin>", line 2>) that is already compiled.

Let's inspect that code object that creates the class body itself; code objects have a co_consts structure:

>>> foo.__code__.co_consts
(None, <code object Foo at 0x10a436030, file "<stdin>", line 2>, 'Foo')
>>> dis.dis(foo.__code__.co_consts[1])
  2           0 LOAD_FAST                0 (__locals__) 
              3 STORE_LOCALS         
              4 LOAD_NAME                0 (__name__) 
              7 STORE_NAME               1 (__module__) 
             10 LOAD_CONST               0 ('foo.<locals>.Foo') 
             13 STORE_NAME               2 (__qualname__) 

  3          16 LOAD_CONST               1 (5) 
             19 STORE_NAME               3 (x) 

  4          22 LOAD_CONST               2 (<code object <listcomp> at 0x10a385420, file "<stdin>", line 4>) 
             25 LOAD_CONST               3 ('foo.<locals>.Foo.<listcomp>') 
             28 MAKE_FUNCTION            0 
             31 LOAD_NAME                4 (range) 
             34 LOAD_CONST               4 (1) 
             37 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
             40 GET_ITER             
             41 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
             44 STORE_NAME               5 (y) 
             47 LOAD_CONST               5 (None) 
             50 RETURN_VALUE         

The above bytecode creates the class body. The function is executed and the resulting locals() namespace, containing x and y is used to create the class (except that it doesn't work because x isn't defined as a global). Note that after storing 5 in x, it loads another code object; that's the list comprehension; it is wrapped in a function object just like the class body was; the created function takes a positional argument, the range(1) iterable to use for its looping code, cast to an iterator. As shown in the bytecode, range(1) is evaluated in the class scope.

From this you can see that the only difference between a code object for a function or a generator, and a code object for a comprehension is that the latter is executed immediately when the parent code object is executed; the bytecode simply creates a function on the fly and executes it in a few small steps.

Python 2.x uses inline bytecode there instead, here is output from Python 2.7:

  2           0 LOAD_NAME                0 (__name__)
              3 STORE_NAME               1 (__module__)

  3           6 LOAD_CONST               0 (5)
              9 STORE_NAME               2 (x)

  4          12 BUILD_LIST               0
             15 LOAD_NAME                3 (range)
             18 LOAD_CONST               1 (1)
             21 CALL_FUNCTION            1
             24 GET_ITER            
        >>   25 FOR_ITER                12 (to 40)
             28 STORE_NAME               4 (i)
             31 LOAD_NAME                2 (x)
             34 LIST_APPEND              2
             37 JUMP_ABSOLUTE           25
        >>   40 STORE_NAME               5 (y)
             43 LOAD_LOCALS         
             44 RETURN_VALUE        

No code object is loaded, instead a FOR_ITER loop is run inline. So in Python 3.x, the list generator was given a proper code object of its own, which means it has its own scope.

However, the comprehension was compiled together with the rest of the python source code when the module or script was first loaded by the interpreter, and the compiler does not consider a class suite a valid scope. Any referenced variables in a list comprehension must look in the scope surrounding the class definition, recursively. If the variable wasn't found by the compiler, it marks it as a global. Disassembly of the list comprehension code object shows that x is indeed loaded as a global:

>>> foo.__code__.co_consts[1].co_consts
('foo.<locals>.Foo', 5, <code object <listcomp> at 0x10a385420, file "<stdin>", line 4>, 'foo.<locals>.Foo.<listcomp>', 1, None)
>>> dis.dis(foo.__code__.co_consts[1].co_consts[2])
  4           0 BUILD_LIST               0 
              3 LOAD_FAST                0 (.0) 
        >>    6 FOR_ITER                12 (to 21) 
              9 STORE_FAST               1 (i) 
             12 LOAD_GLOBAL              0 (x) 
             15 LIST_APPEND              2 
             18 JUMP_ABSOLUTE            6 
        >>   21 RETURN_VALUE         

This chunk of bytecode loads the first argument passed in (the range(1) iterator), and just like the Python 2.x version uses FOR_ITER to loop over it and create its output.

Had we defined x in the foo function instead, x would be a cell variable (cells refer to nested scopes):

>>> def foo():
...     x = 2
...     class Foo:
...         x = 5
...         y = [x for i in range(1)]
...     return Foo
... 
>>> dis.dis(foo.__code__.co_consts[2].co_consts[2])
  5           0 BUILD_LIST               0 
              3 LOAD_FAST                0 (.0) 
        >>    6 FOR_ITER                12 (to 21) 
              9 STORE_FAST               1 (i) 
             12 LOAD_DEREF               0 (x) 
             15 LIST_APPEND              2 
             18 JUMP_ABSOLUTE            6 
        >>   21 RETURN_VALUE         

The LOAD_DEREF will indirectly load x from the code object cell objects:

>>> foo.__code__.co_cellvars               # foo function `x`
('x',)
>>> foo.__code__.co_consts[2].co_cellvars  # Foo class, no cell variables
()
>>> foo.__code__.co_consts[2].co_consts[2].co_freevars  # Refers to `x` in foo
('x',)
>>> foo().y
[2]

The actual referencing looks the value up from the current frame data structures, which were initialized from a function object's .__closure__ attribute. Since the function created for the comprehension code object is discarded again, we do not get to inspect that function's closure. To see a closure in action, we'd have to inspect a nested function instead:

>>> def spam(x):
...     def eggs():
...         return x
...     return eggs
... 
>>> spam(1).__code__.co_freevars
('x',)
>>> spam(1)()
1
>>> spam(1).__closure__
>>> spam(1).__closure__[0].cell_contents
1
>>> spam(5).__closure__[0].cell_contents
5

So, to summarize:

  • List comprehensions get their own code objects in Python 3 (up to Python 3.11), and there is no difference between code objects for functions, generators or comprehensions; comprehension code objects are wrapped in a temporary function object and called immediately.
  • Code objects are created at compile time, and any non-local variables are marked as either global or as free variables, based on the nested scopes of the code. The class body is not considered a scope for looking up those variables.
  • When executing the code, Python has only to look into the globals, or the closure of the currently executing object. Since the compiler didn't include the class body as a scope, the temporary function namespace is not considered.

A workaround; or, what to do about it

If you were to create an explicit scope for the x variable, like in a function, you can use class-scope variables for a list comprehension:

>>> class Foo:
...     x = 5
...     def y(x):
...         return [x for i in range(1)]
...     y = y(x)
... 
>>> Foo.y
[5]

The 'temporary' y function can be called directly; we replace it when we do with its return value. Its scope is considered when resolving x:

>>> foo.__code__.co_consts[1].co_consts[2]
<code object y at 0x10a5df5d0, file "<stdin>", line 4>
>>> foo.__code__.co_consts[1].co_consts[2].co_cellvars
('x',)

Of course, people reading your code will scratch their heads over this a little; you may want to put a big fat comment in there explaining why you are doing this.

The best work-around is to just use __init__ to create an instance variable instead:

def __init__(self):
    self.y = [self.x for i in range(1)]

and avoid all the head-scratching, and questions to explain yourself. For your own concrete example, I would not even store the namedtuple on the class; either use the output directly (don't store the generated class at all), or use a global:

from collections import namedtuple
State = namedtuple('State', ['name', 'capital'])

class StateDatabase:
    db = [State(*args) for args in [
       ('Alabama', 'Montgomery'),
       ('Alaska', 'Juneau'),
       # ...
    ]]

PEP 709, part of Python 3.12, changes some of this all again

In Python 3.12, comprehensions have been made a lot more efficient by removing the nested function and inlining the loop, while still maintaining a separate scope. The details of how this was done are outlined in PEP 709 - Inlined comprehensions, but the long and short of it is that instead of creating a new function object and then calling it, with LOAD_CONST, MAKE_FUNCTION and CALL bytecodes, any clashing names used in the loop are first moved to the stack before executing the comprehension bytecode inline.

It is important to note that this change only affects performance and interaction with the class scope has not changed. You still can't access names created in a class scope, for the reasons outlined above.

Using Python 3.12.0b4 the bytecode for the Foo class now looks like this:

# creating `def foo()` and its bytecode elided

Disassembly of <code object Foo at 0x104e97000, file "<stdin>", line 2>:
  2           0 RESUME                   0
              2 LOAD_NAME                0 (__name__)
              4 STORE_NAME               1 (__module__)
              6 LOAD_CONST               0 ('foo.<locals>.Foo')
              8 STORE_NAME               2 (__qualname__)

  3          10 LOAD_CONST               1 (5)
             12 STORE_NAME               3 (x)

  4          14 PUSH_NULL
             16 LOAD_NAME                4 (range)
             18 LOAD_CONST               2 (1)
             20 CALL                     1
             28 GET_ITER
             30 LOAD_FAST_AND_CLEAR      0 (.0)
             32 LOAD_FAST_AND_CLEAR      1 (i)
             34 LOAD_FAST_AND_CLEAR      2 (x)
             36 SWAP                     4
             38 BUILD_LIST               0
             40 SWAP                     2
        >>   42 FOR_ITER                 8 (to 62)
             46 STORE_FAST               1 (i)
             48 LOAD_GLOBAL              6 (x)
             58 LIST_APPEND              2
             60 JUMP_BACKWARD           10 (to 42)
        >>   62 END_FOR
             64 SWAP                     4
             66 STORE_FAST               2 (x)
             68 STORE_FAST               1 (i)
             70 STORE_FAST               0 (.0)
             72 STORE_NAME               5 (y)
             74 RETURN_CONST             3 (None)

Here, the most important bytecode is the one at offset 34:

             34 LOAD_FAST_AND_CLEAR      2 (x)

This takes the value for the variable x in the local scope and pushes it on the stack, and then clears the name. If there is no variable x in the current scope, this stores a C NULL value on the stack. The name is now gone from the local scope now until the bytecode at offset 66 is reached:

             66 STORE_FAST               2 (x)

This restores x to what it was before the list comprehension; if a NULL was stored on the stack to indicate that there was no variable named x, then there still won't be a variable x after this bytecode has been executed.

The rest of the bytecode between the LOAD_FAST_AND_CLEAR and STORE_FAST calls is more or less the same it was before, with SWAP bytecodes used to access the iterator for the range(1) object instead of LOAD_FAST (.0) in the function bytecode in earlier Python 3.x versions.

Cadaverous answered 17/12, 2012 at 12:11 Comment(8)
You can also use a lambda to fix the binding: y = (lambda x=x: [x for i in range(1)])()Chenab
@ecatmur: Exactly, lambda are just anonymous functions, after all.Cadaverous
For the record, the work-around that uses a default argument (to a lambda or a function) to pass in the class variable has a gotcha. Namely, it passes the current value of the variable. So, if the variable changes later, and then the lambda or function is called, the lambda or function will be using the old value. This behavior differs from the behavior of a closure (which would capture a reference to the variable, rather than its value), so may be unexpected.Aquila
If it requires a page of technical information to explain why something doesn't work intuitively, I call that a bug.Marcum
@JonathanLeaders: Don't call it a bug, call it a tradeoff. If you want A and B, but can get only one of them, then no matter how you decide, in some situations you will dislike the result. That's life.Roye
I assume all this is also True for staticmethods in the class? … #59474874Khachaturian
@Khachaturian yes. Note that a staticmethod object is stored as just another name in the class scope. It doesn’t matter what type of object it is.Cadaverous
Gawd, what a waste of time. Solutions: (1) Declare it as a global -- it works. (2) Use a for loop instead. (3) [may work YMMV] Create an artificial list/tuple object and read it in from inside the comprehension. (4) Use a lambda but that turns code into define then call, may as well use a loop or function. Item (3) won't work in scenarios like re.sub(precompiled_re, ..., ...) within a comprehension as the scope isn't picked up transitively. You cannot even use ClassName.variable to get it.Eparchy
M
26

In my opinion it is a flaw in Python 3. I hope they change it.

New Way:

class Foo:
    x = 5
    y = (lambda x=x: [x for i in range(1)])()

Because the syntax is so ugly I just initialize all my class variables in the constructor typically

Marcum answered 24/1, 2015 at 22:25 Comment(4)
The problem is present in Python 2 as well, when using generator expressions, as well as with set and dictionary comprehensions. It is not a bug, it is a consequence of how class namespaces work. It'll not change.Cadaverous
And I note that your workaround does exactly what my answer already states: create a new scope (a lambda is no different here from using def to create a function).Cadaverous
yep. While it is nice to have an answer with the work-around at a glance, this one incorrecly states the behavior as a bug, when it is a side-effect of the way the language works (and therefore, won't be changed)Unpredictable
This is a different problem, that actually isn't a problem in Python 3. It only occurs in IPython when you call it in embed mode using say python -c "import IPython;IPython.embed()". Run IPython directly using say ipython and the problem will disappear.Nesta
W
8

The accepted answer provides excellent information, but there appear to be a few other wrinkles here -- differences between list comprehension and generator expressions. A demo that I played around with:

class Foo:

    # A class-level variable.
    X = 10

    # I can use that variable to define another class-level variable.
    Y = sum((X, X))

    # Works in Python 2, but not 3.
    # In Python 3, list comprehensions were given their own scope.
    try:
        Z1 = sum([X for _ in range(3)])
    except NameError:
        Z1 = None

    # Fails in both.
    # Apparently, generator expressions (that's what the entire argument
    # to sum() is) did have their own scope even in Python 2.
    try:
        Z2 = sum(X for _ in range(3))
    except NameError:
        Z2 = None

    # Workaround: put the computation in lambda or def.
    compute_z3 = lambda val: sum(val for _ in range(3))

    # Then use that function.
    Z3 = compute_z3(X)

    # Also worth noting: here I can refer to XS in the for-part of the
    # generator expression (Z4 works), but I cannot refer to XS in the
    # inner-part of the generator expression (Z5 fails).
    XS = [15, 15, 15, 15]
    Z4 = sum(val for val in XS)
    try:
        Z5 = sum(XS[i] for i in range(len(XS)))
    except NameError:
        Z5 = None

print(Foo.Z1, Foo.Z2, Foo.Z3, Foo.Z4, Foo.Z5)
Wendiwendie answered 5/8, 2018 at 2:53 Comment(7)
You may have to re-read my answer, all points you raise I did cover. :-) The implementation for list comprehensions changed between Python 2 and 3, look for In Python 2, list comprehensions were implemented using a shortcut, but in Python 3 they got their own function scope (as they should have had all along) and thus your example breaks.Cadaverous
My answer also covers your work-around: by creating a lambda or def you create a new scope, as covered in the 'workaround' section: If you were to create an explicit scope for the x variable, like in a function, you can use class-scope variables for a list comprehension.Cadaverous
The Z5 example follows from the 'exception' section: There's one part of a comprehension or generator expression that executes in the surrounding scope, regardless of Python version. That would be the expression for the outermost iterable. Here that's range(len(XS)); the result of that expression is passed into the generator expression scope as the iterable. This is also why you can't refer to XS anywhere else in the generator expression; it is not that the name is passed in, only the object that a name references is passed in, it is a local variable inside the comprehension scope.Cadaverous
@MartijnPieters I'm pretty sure the situation looked different on Aug 5 2018.Wendiwendie
The only substantial change after August 2018 was an explicit section on the iterator expression being executed in the surrounding scope, but I also covered that in the under the hood section.Cadaverous
I'm pointing out that your answer isn't covering new ground, that the parts you thought were different or new were not.Cadaverous
@MartijnPieters Having looked at the edit history, and jogged my memory, the material you added after Aug 2018 is part of what I puzzled over while I experimented with this topic back then. That puzzle led me to write this code and post the answer. So the situation did look different in Aug 2018.Wendiwendie
K
3

Simply adding for x in [x] as the first for clause to make x available in the comprehension's scope:

class Foo:
    x = 5
    y = [x for x in [x] for i in range(1)]

And with for State in [State] in your other case:

from collections import namedtuple
class StateDatabase:
    State = namedtuple('State', ['name', 'capital'])
    db = [State(*args) for State in [State] for args in [
        ['Alabama', 'Montgomery'],
        ['Alaska', 'Juneau'],
        # ...
    ]]

Or with more than one variable:

class Line:
    a = 19
    b = 4
    y = [a*x + b
         for a, b in [(a, b)]
         for x in range(10)]
Kamin answered 5/7, 2023 at 16:29 Comment(3)
So ugly it renders comprehensions unappealing, and in embedded function scopes it still doesn't work.Eparchy
@Eparchy What embedded function scope? Please show an example.Kamin
Please read the top comment. You are not going to solve this issue -- it's a language shortcoming. Look at the hand waving here ... Creating a collection to pass in a simple scalar. Try a few more complex data types or using pre-compiled regex patterns in an re.sub() operation with complex dict() values. Notwithstanding that the point of comprehensions are elegance, compactness and convenience, adding superfluous data collections and extra clauses don't do this language construct justice. Thanks, but I am done with the rabbit hole.Eparchy
E
2

Since the outermost iterator is evaluated in the surrounding scope we can use zip together with itertools.repeat to carry the dependencies over to the comprehension's scope:

import itertools as it

class Foo:
    x = 5
    y = [j for i, j in zip(range(3), it.repeat(x))]

One can also use nested for loops in the comprehension and include the dependencies in the outermost iterable:

class Foo:
    x = 5
    y = [j for j in (x,) for i in range(3)]

For the specific example of the OP:

from collections import namedtuple
import itertools as it

class StateDatabase:
    State = namedtuple('State', ['name', 'capital'])
    db = [State(*args) for State, args in zip(it.repeat(State), [
        ['Alabama', 'Montgomery'],
        ['Alaska', 'Juneau'],
        # ...
    ])]
Edithe answered 15/3, 2019 at 11:3 Comment(0)
S
1

One can use a for loop:

class Foo:
    x = 5
    y = []
    for i in range(1):
        y.append(x)

Please correct me i'm not wrong...

Stieglitz answered 13/10, 2022 at 23:16 Comment(2)
Yes this works. It's also clumsy.. but there isnt anything not so : It's as good [/bad] as the other altenatives.Isobar
This has the unfortunate side-effect of adding another class variable i, so I wouldn't use this personally. You could put del i after the loop though.Hollingsworth
G
0

Just a fun example.

If you want to keep it as a list comprehension, this also works with nested list comprehensions. Bring the value out in the global namespace but keep its class name.

class Foo:
  global __x
  __x = 5
  y = [_Foo__x for i in range(1)]
Glary answered 12/4, 2023 at 13:33 Comment(2)
OP also sets Foo.x, so you could do x = 5; __x = x.Hollingsworth
A slightly more sensible approach, I think, is to move the variable entirely into the global namespace, like _Foo_x = 5; class Foo: x = _Foo_x; y = [_Foo_x for i in range(1)]Hollingsworth
A
-2

This is a bug in Python. Comprehensions are advertised as being equivalent to for loops, but this is not true in classes. At least up to Python 3.6.6, in a comprehension used in a class, only one variable from outside the comprehension is accessible inside the comprehension, and it must be used as the outermost iterator. In a function, this scope limitation does not apply.

To illustrate why this is a bug, let's return to the original example. This fails:

class Foo:
    x = 5
    y = [x for i in range(1)]

But this works:

def Foo():
    x = 5
    y = [x for i in range(1)]

The limitation is stated at the end of this section in the reference guide.

Archbishop answered 30/12, 2018 at 12:0 Comment(1)
This is not a bug. This is by design, that's how class scopes work, and why the name resolution reference documentation explicitly points out that it will fail.Cadaverous
B
-3

I spent quite some time to understand why this is a feature, not a bug.

Consider the simple code:

a = 5
def myfunc():
    print(a)

Since there is no "a" defined in myfunc(), the scope would expand and the code will execute.

Now consider the same code in the class. It cannot work because this would completely mess around accessing the data in the class instances. You would never know, are you accessing a variable in the base class or the instance.

The list comprehension is just a sub-case of the same effect.

Bethanybethe answered 12/10, 2021 at 20:37 Comment(1)
It's not clear what you mean by "consider the same code in the class". What would that look like? How would it be "accessing the data in the class instances" if it's not using self?Hollingsworth

© 2022 - 2024 — McMap. All rights reserved.