3

Take this simple code:

class A(object):
  numbers = [1, 2, 3]
  numberscopy = numbers[:]
  print(*(a for a in numberscopy))
  print(*(a for a in numberscopy if a in numbers))

I define the numbers variable inside the class. I can then use it to do other things, like make a copy, iterate over it, and print its contents.

But the last line, with the for-if statement, fails with NameError: global name 'numbers' is not defined. Not numberscopy, just numbers.

I tried on both python 2.7.14+ (with print_function imported) and 3.7.0, with the same result.

Why does this happen? Is it intended to work this way?

Heshy
  • 382
  • 1
  • 10
  • I get this same error -- no indentation issue. I can print `numbers` and `numberscopy` but the error is thrown when I add the `if` to the comprehension. This works with no error: `print(*(a for a in numberscopy if a in A.numbers))` – Mark May 13 '19 at 19:03
  • @ToothpickAnemone only as an example. In a real case, I want to define other class variables based on a `for` `if` statement involving previous variables. – Heshy May 13 '19 at 19:07
  • @Heshy see: https://stackoverflow.com/questions/13905741/accessing-class-variables-from-a-list-comprehension-in-the-class-definition – Mark May 13 '19 at 19:08
  • Sorry, I thought it was a function. But it's a class. IT's a duplicate – Jean-François Fabre May 13 '19 at 19:09

1 Answers1

2

The code inside class-bodies is somewhat messed in Python. It is not so much a bug, but an "implementation problem that got weird".

The problem being that: code usually runs at module level, or inside functions - when inside functions, there are well-defined "local" variables.

Code inside class bodies also run with a "local" scope - but if one creates functions that are run while the class body is being processed, these do not "see" the outer-level variables. And generator expressions, as do comprehensions (in Python 3, Python 2 is another language, which is on its way out, let's not complicate stuff). The expression used to create the iterator to the for inside the generator is run in a scope where it "sees" the outer variables. The main expression and if expressions themselves are inside the generator, and can't "see" those variables.

So, a workaround, if comprehensions are needed inside a class body is to have an intermediary function inside the class body, just to generate the values and variables you need, and have line to call that and update the class's own variables with the local namespace of that inner function:

class A:
   def create_vals():
       numbers = [1, 2, 3]
       numbers_copy = numbers[:]
       values = list(a for a in numbers if a in numbers_copy)
       return locals()
   locals().update(create_vals())
   del create_vals

So, inside the temporary create_vals function (it is not a method), usual scope-nesting rules apply - and with the last two lines we copy the created variables to the class itself, and remove the temporary function.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • 2
    well, what's the difference with the question & the answer in https://stackoverflow.com/questions/13905741/accessing-class-variables-from-a-list-comprehension-in-the-class-definition ? why reopening when Martijn answer is stellar? – Jean-François Fabre May 13 '19 at 19:16
  • Using locals like this is not guaranteed to work. It *happens* to work in this case, but the [documentation](https://docs.python.org/3/library/functions.html#locals) warns against relying on modifications to the returned `dict`. It would be better to simply `return` the values required, or better yet, just *don't use a list comprehension if it makes you do something like this*. – juanpa.arrivillaga May 13 '19 at 20:00
  • It would not work on a function scope, but it works inside a classbody, where locals is just a plain dictionary, or another mapping returned by the metaclass `__prepare__` - but indeed, I don't think it is pinned in any specs that the locals for classes is a plain dictionary by default. – jsbueno May 13 '19 at 20:17
  • As for reopenning the question, I had my answer typed in and ready to post - it used to be possible to post your work without loosing it when a question was closed mid-answer. I think this is concise enough to stay there - ant people can forward to the duplicate if they need more detauls. – jsbueno May 13 '19 at 20:18
  • @juanpa.arrivillaga - on updating `locals` on class scope, I just came across the official docs (when reading PEP 558) - and, in fact, it's is only tricky inside function scopes. """At class scope, it [`locals()`] returns the namespace that will be passed to the metaclass constructor. """ - cf. https://www.python.org/dev/peps/pep-0558/ – jsbueno May 21 '19 at 15:53