Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Executing statements in a class definition: Which variables does the interpreter know about?

Tags:

python

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.

like image 451
Louis Thibault Avatar asked May 28 '12 16:05

Louis Thibault


2 Answers

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.

like image 96
Zeugma Avatar answered Oct 31 '22 21:10

Zeugma


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.

like image 22
martineau Avatar answered Oct 31 '22 20:10

martineau