Naga
Naga

Reputation: 393

Workaround for not having __set_name__ in Python versions before 3.6

In Python 3.6, I can use the __set_name__ hook to get the class attribute name of a descriptor. How can I achieve this in python 2.x?

This is the code which works fine in Python 3.6:

class IntField:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise ValueError('expecting integer')
        instance.__dict__[self.name] = value

    def __set_name__(self, owner, name):
        self.name = name

class Example:
    a = IntField()

Upvotes: 6

Views: 1570

Answers (2)

geckos
geckos

Reputation: 6299

You may be looking for metaclasses, with it you can process the class attributes at class creation time.

class FooDescriptor(object):
    def __get__(self, obj, objtype):
        print('calling getter')

class FooMeta(type):
    def __init__(cls, name, bases, attrs):
        for k, v in attrs.iteritems():
            if issubclass(type(v), FooDescriptor):
                print('FooMeta.__init__, attribute name is "{}"'.format(k))

class Foo(object):
    __metaclass__ = FooMeta
    foo = FooDescriptor()


f = Foo()
f.foo

Output:

FooMeta.__init__, attribute name is "foo"
calling getter

If you need to change the class before it is created you need to override __new__ instead of __init__ at your metaclass. See this answer for more information on this topic: Is there any reason to choose __new__ over __init__ when defining a metaclass?

Upvotes: 6

timgeb
timgeb

Reputation: 78750

There are various solutions with different degrees of hackishness. I always liked to use a class decorator for this.

class IntField(object):
    def __get__(self, instance, owner):            
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):            
        if not isinstance(value, int):
            raise ValueError('expecting integer')
        instance.__dict__[self.name] = value

def with_intfields(*names):
    def with_concrete_intfields(cls):
        for name in names:
            field = IntField()
            field.name = name
            setattr(cls, name, field)
        return cls
    return with_concrete_intfields

You can use it like this:

@with_intfields('a', 'b')
class Example(object):
    pass

e = Example()

Demo:

$ python2.7 -i clsdec.py
>>> [x for x in vars(Example) if not x.startswith('_')]
['a', 'b']
>>> Example.a.name
'a'
>>> e.a = 3
>>> e.b = 'test'
[...]
ValueError: expecting integer

Make sure to explicitly subclass from object in Python 2.7, that got me tripped up when I drafted the first version of this answer.

Upvotes: 2

Related Questions