2

I am porting a library to Python 3.

I have a class with a lot of class variables that are initialized with range(). Here's a cut-down example:

class Test(object):
    base = 3
    a,b,c,d = [base+y for y in range(4)]

print (Test.b)

This code works fine in Python 2.

$ python2 test.py
1001

But it fails in Python 3:

$ python3 test.py
Traceback (most recent call last):
  File "test.py", line 1, in <module>
    class Test(object):
  File "test.py", line 3, in Test
    a,b,c,d = [base+y for y in range(4)]
  File "test.py", line 3, in <listcomp>
    a,b,c,d = [base+y for y in range(4)]
NameError: name 'base' is not defined

Question: What is going on? How can I port this to Python 3 without a lot of ugly repetitive code?

markrages
  • 334
  • 3
  • 14

1 Answers1

4

The difference is because in Python3, list-comprehensions use a separate variable scope than the surrounding code, while in Python 2, they share the same variables.

Add to that the peculiar thing of variables in class body, in which inner scopes, no matter if they are functions or generator expressions can't "see" nonlocal variables, and you get your error.

I think the easiest work around will be to run your generator inside a one-time lambda function, and pass the needed variables as parameters to it. Inside the lambda function, the usual scope-nesting rules in which inner scopes can see outer variables apply:

class Test(object):
    base = 1000
    a,b,c,d = (lambda base: [base+y for y in range(4)])(base)

Since Python 3 allows one to use a custom object as the namespace for the class body, it is also possible to create a namespace that is visible on the global scope. In that way, code in inner scopes in the class body can use the variables there.

This short metaclass and helper mapping can do it - and an easy way to avoid name clashing in the global scope is just to use the class name itself. This way, one also gains in readability:

class AttrDict(dict):

    def __getattr__(self, attr):
        return self.__getitem__(attr)

    def __setattr__(self, attr, value):
        self.__setitem__(attr, value)


class Namespace(type):
    @classmethod
    def __prepare__(metacls, name, bases, **kwargs):
        import sys
        module_globals = sys._getframe(1).f_globals
        namespace = AttrDict()
        module_globals[name] = namespace
        return namespace

And now, you can write your code like this:

class Base(metaclass=Namespace):
    x = 10
    a, b, c, d = (i + Base.x for i in range(4))
    del x
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • Wow, that's ugly. I think I'll just stick with Python 2, but thank you for your answer. – markrages Nov 06 '17 at 01:20
  • The answer marked as "original" from this have all the technical guts. Don't go Python 2. Just use an unroled for loop instead, then. – jsbueno Nov 06 '17 at 01:21
  • Python 2 is really end-of-life by now. It is basically 2 years away from end of support, and you loose a lot, really a lot, of niceties. While your original assignment, that can be called an ugly hack - it just gets a little bit uglier. :-) – jsbueno Nov 06 '17 at 01:22
  • Yes, someday there will be feature in 3 that I can't live without. When that day comes, I will go through and uglify my code to work in 3. – markrages Nov 06 '17 at 01:31