7

Below is a partial class definition of mine:

class Trial:
    font = pygame.font.Font(None, font_size)
    target_dic = {let: font.render(let, True, WHITE, BG) for let in list("ABCDEFGHJKLMNPRSTUVWX")}

The last line of the partial class definition, target_dic = {let: font.render(let, True, WHITE, BG) for let in list("ABCDEFGHJKLMNPRSTUVWX") returns the error: global name 'font' is not defined. Fair enough.

However, I tried the following test case and got no error:

class x:
    dat = 1
    datlist = [dat for i in range(10)]

Why should the first case not work? Doesn't the member font exist by the time the dictionary comprehension is reached?

Do I need to move these operations to __init__ or is it possible to define the list exactly once when the class object is created?

EDIT:

For clarity, I'd like to be able to populate the list at class object creation time to cut down on the time spent creating Trial objects.

Louis Thibault
  • 20,240
  • 25
  • 83
  • 152
  • 3
    The `list()` in `list("ABC...")` is superfluous; strings are iterables in themselves so `for ... in "ABC.."` works on it's own. – Martijn Pieters May 28 '12 at 16:28
  • The second test case should cause an error as well (`self` is not defined). – interjay May 28 '12 at 16:32
  • I think you should use `Trial.font` in the first case. I don't think you can reference class attributes directly like that. – ubik May 28 '12 at 16:32
  • @PedroFerreira: `Trial` does not exist yet while the class block is executed. – ThiefMaster May 28 '12 at 16:34
  • @interjay, indeed! I copied the wrong test case. I've corrected the example. – Louis Thibault May 28 '12 at 16:37
  • @ThiefMaster, yes indeed. Well, he could declare an empty `class Trial` immediately before, but well... no sense at all. – ubik May 28 '12 at 16:39
  • 1
    Interesting. This gives an error when using the variable `font` in a dict comprehension or in a generator expression. But there is no error when using it in a list comprehension. – interjay May 28 '12 at 16:41
  • @MartijnPieters I see the problem now. I suppose I could just write out the whole loop, although I'm not sure whether this is legal in class definitions – Louis Thibault May 28 '12 at 16:49
  • 4
    possible duplicate of [Class attribute evaluation and generators](http://stackoverflow.com/questions/1773636/class-attribute-evaluation-and-generators). That question is about generator expressions rather than dict comprehensions, but the reason is the same. – interjay May 28 '12 at 17:00

3 Answers3

4

Partial answer, as it is more to cut some wrong paths to follow.

If I take back your working sample and put a dict comprehension:

class x:
    dat = 1
    datlist = {i:dat for i in range(10)}

I get also this:

>>> NameError: global name 'dat' is not defined

So it looks like dict comprehension is hiding the temporary dict use during class statement execution, but the list comprehension doesn't.

No further information found by now in the docs about this...

Edit based on @interjay comment: class construction not fulfilling scope norm is addressed in this post. Short story is that list comprehension are buggy in 2.x and see class members but they shouldn't.

Community
  • 1
  • 1
Zeugma
  • 31,231
  • 9
  • 69
  • 81
  • Note that the list comprehension does not work either in Python 3. –  May 28 '12 at 17:32
  • OK so we are talking about CPython implementation and side effects / bugs in the implementation against what is expected by the norm... I would say that the code with the list working is more the result of exploiting a bug of the 2.x interpreter – Zeugma May 28 '12 at 17:37
  • 2
    List comprehensions in 2.x don't have their own scope (in 3.x they do, and generator expressions as well as dict/set comprehensions have them in all versions). It may have started as a bug, and it's nasty, but I *believe* it's documented and kept for compatibility (including being supported by other Python interpreters). I'll look it up. –  May 28 '12 at 17:39
  • Found a footnote regarding it: http://docs.python.org/reference/expressions.html?highlight=list%20comprehension#id20 So at least it's acknowledged and even granted the honor of being deprecated. –  May 28 '12 at 17:46
  • @delman I know this comment but in the example above it would apply to `i`variable, not `dat`... – Zeugma May 28 '12 at 17:52
1

Because class decorators are invoked after the class is created, you could use one to work around the limitations of referencing class attributes in the class body and effectively "post-process" the class just created and add any missing attributes:

def trial_deco(cls):
    cls.target_dic = {let: cls.font.render(let, True, WHITE, BG) for let in "ABCDEFGHJKLMNPRSTUVWXZ"}
    return cls

@trial_deco
class Trial:
    font = pygame.font.Font(None, font_size)
    target_dic = None  # redefined by decorator

An alternative way would be to make the attribute initially a data-descriptor (aka "property") that was (only) invoked the first time the attribute was read. This is accomplished by overwriting the descriptor with the computed value, so it only happens once.

class AutoAttr(object):
    """ data descriptor for just-in-time generation of instance attribute """
    def __init__(self, name, font, antialias, color, background):
        self.data = name, font, antialias, color, background

    def __get__(self, obj, cls=None):
        name, font, antialias, color, background = self.data
        setattr(obj, name, {let: font.render(let, antialias, color, background)
                                for let in "ABCDEFGHJKLMNPRSTUVWXZ"})
        return getattr(obj, name)

class Trial(object):
    font = pygame.font.Font(None, fontsize)    
    target_dic = AutoAttr('target_dic', font, TRUE, WHITE, BG)

There's also other ways to do things like this, such as with meta-classes or defining a __new__ method, but it's doubtful that any would work better or be any less complex.

martineau
  • 119,623
  • 25
  • 170
  • 301
-1

The names which are available during class creation are the global variables, and names already defined at the top level of the class (that is, as names are defined, they become available further down the class code).

Note that objects at class level may not be as you get them through an instance, or even through the class object; most notably, during class-creation, the result of a def is still a function rather than a method.

In relation to OP's code, this example shows that font becomes defined at the class level:

class Trial:
    font = 'foo'
    target_dic = dict((lambda fnt:((let,fnt) for let in "ABCDEFGHJKLMNPRSTUVWX"))(font))
    target_two = []
    for let in "ABCDEFGHJKLMNPRSTUVWX":
        target_two.append(let)

print(Trial.target_dic)
print(Trial.target_two)

This yields:

{'A': 'foo', 'C': 'foo', 'B': 'foo', 'E': 'foo', 'D': 'foo', 'G': 'foo', 'F': 'foo', 'H': 'foo', 'K': 'foo', 'J': 'foo', 'M': 'foo', 'L': 'foo', 'N': 'foo', 'P': 'foo', 'S': 'foo', 'R': 'foo', 'U': 'foo', 'T': 'foo', 'W': 'foo', 'V': 'foo', 'X': 'foo'}
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'U', 'V', 'W', 'X']

(http://ideone.com/aC7VY)

font is available to be passed to the lambda, as expected.

The issue is not the names available in class scope (font is available there). The issue is that comprehensions introduce a new scope of their own (in python 3): http://docs.python.org/py3k/reference/expressions.html#displays-for-lists-sets-and-dictionaries

Marcin
  • 48,559
  • 18
  • 128
  • 201
  • This doesn't explain why there is an error accessing `font` in the dict comprehension, when it was defined in the previous line. – interjay May 28 '12 at 16:44
  • @Marcin, by this logic, shouldn't the `font` attribute be available? Unless I'm misunderstanding you, this doesn't explain anything -- it restates the problem. – Louis Thibault May 28 '12 at 16:45
  • @blz Given that you don't give a stack trace, or even complete and correct code, I have no comment to make on your code. – Marcin May 28 '12 at 16:53
  • 1
    @Marcin: You have no comment to make but still felt the need to post an (incorrect) answer? And what stack trace do you expect when the code doesn't have a function? – interjay May 28 '12 at 16:57
  • Your answer is 95% correct. Unfortunately the other 5% is exactly what this question is about. According to your answer, accessing `font` should cause no error, and this is obviously incorrect. – interjay May 28 '12 at 17:06
  • @interjay According to the documentation, and tests not using a comprehension, there should be no error. The problem is that the interpreter does not behave as specified. – Marcin May 28 '12 at 17:13
  • This is by design (not a bug), and has to do with the late-binding behavior of generator expressions, and the fact that the `class` statement does not create a closure. You got around it by creating a closure with the `lambda`. This is explained in the duplicate link I put in the comments above. – interjay May 28 '12 at 17:16
  • @interjay It does not explain why the behaviour does not conform with the part of the specification quoted. – Marcin May 28 '12 at 17:25
  • @Marcin Even assuming that each individual statement in your answer is correct, your "answer" does not answer this question at all (as in, it does not do anything to resolve the problem OP is facing). -1 from me too. Now, if you changed your answer to feature the statements from your comments ("it's a bug, it should work, report it"), it would be an answer (though I'm not saying it'd be correct). –  May 28 '12 at 17:28
  • 1
    You didn't quote the most important part: "Note that the comprehension is executed in a separate scope". This means that it can't access variables from another scope unless they are available in a closure (such as the one provided by your lambda). – interjay May 28 '12 at 17:30
  • @delnan My answer contains, and contained at the time you left your comment the information that this is a bug. – Marcin May 28 '12 at 17:41