5

I'm working as an application with classes and subclasses. For each class, both super and sub, there is a class variable called label. I would like the label variable for the super class to default to the class name. For example:

class Super():
    label = 'Super'

class Sub(Super):
    label = 'Sub'

Rather than manually type out the variable for each class, is it possible to derive the variable from the class name in the super class and have it automatically populated for the subclasses?

class Super():
    label = # Code to get class name

class Sub(Super)
    pass
    # When inherited Sub.label == 'Sub'.

The reason for this is that this will be the default behavior. I'm also hoping that if I can get the default behavior, I can override it later by specifying an alternate label.

class SecondSub(Super):
    label = 'Pie'  # Override the default of SecondSub.label == 'SecondSub'

I've tried using __name__, but that's not working and just gives me '__main__'.

I would like to use the class variable label in @classmethod methods. So I would like to be able to reference the value without having to actually create a Super() or Sub() object, like below:

class Super():
    label = # Magic

    @classmethod
    def do_something_with_label(cls):
        print(cls.label)
2
  • you should have specified that you needed it to work with uninstantiated classes. you would have gotten a more correct solution sooner Commented May 21, 2015 at 19:05
  • You are right, I should have. Will fix that now. Commented May 21, 2015 at 19:24

4 Answers 4

10

you can return self.__class__.__name__ in label as a property

class Super:
    @property
    def label(self):
        return self.__class__.__name__

class Sub(Super):
   pass

print Sub().label

alternatively you could set it in the __init__ method

def __init__(self):
    self.label = self.__class__.__name__

this will obviously only work on instantiated classes

to access the class name inside of a class method you would need to just call __name__ on the cls

class XYZ:
    @classmethod
    def my_label(cls):
        return cls.__name__

print XYZ.my_label()

this solution might work too (snagged from https://stackoverflow.com/a/13624858/541038)

class classproperty(object):
    def __init__(self, fget):
        self.fget = fget
    def __get__(self, owner_self, owner_cls):
        return self.fget(owner_cls)

class Super(object): 
    @classproperty
    def label(cls):
        return cls.__name__

class Sub(Super):
   pass

print Sub.label  #works on class
print Sub().label #also works on an instance

class Sub2(Sub):
   @classmethod
   def some_classmethod(cls):
       print cls.label

Sub2.some_classmethod()
Sign up to request clarification or add additional context in comments.

8 Comments

I was just running to ipython to try a dir(self) inside a class when you added the answer. Bravo! I learned something new!
Would I be able to reference that instance property again inside of a @classmethod?
@user2004245 self.__class__ can only be evaluated after the class has been completely defined, so you can get the label at run-time but not at design time of the class (so you can use it inside of a method, but not as a value of a class attribute).
Using __init__ would establish that as an instance variable, I specifically need it as a class variable so that it can be referenced in @classmethod.
You'll probably want to make this a custom descriptor instead of a property, so it shows up properly when you do Super.label instead of just working on instances. Depending on whether label = whatever in SecondSub should be inherited by class ThirdSub(SecondSub), it might also be worth making it a data descriptor and checking the class dict for overrides yourself.
|
7

You can use a descriptor:

class ClassNameDescriptor(object):
    def __get__(self, obj, type_):
        return type_.__name__

class Super(object):
    label = ClassNameDescriptor()

class Sub(Super):
    pass

class SecondSub(Super):
    label = 'Foo'

Demo:

>>> Super.label
'Super'
>>> Sub.label
'Sub'
>>> SecondSub.label
'Foo'
>>> Sub().label
'Sub'
>>> SecondSub().label
'Foo'

If class ThirdSub(SecondSub) should have ThirdSub.label == 'ThirdSub' instead of ThirdSub.label == 'Foo', you can do that with a bit more work. Assigning label at the class level will be inherited, unless you use a metaclass (which is a lot more hassle than it's worth for this), but we can have the label descriptor look for a _label attribute instead:

class ClassNameDescriptor(object):
    def __get__(self, obj, type_):
        try:
            return type_.__dict__['_label']
        except KeyError:
            return type_.__name__

Demo:

>>> class SecondSub(Super):
...     _label = 'Foo'
...
>>> class ThirdSub(SecondSub):
...     pass
...
>>> SecondSub.label
'Foo'
>>> ThirdSub.label
'ThirdSub'

6 Comments

great answer and you beat my edit that does this as well (+1 from me good sir :) )
There is a certain asymmetry here, though. SecondSub.label is an actual string, no longer a descriptor. A class such as class ThirdSub(SecondSub): pass would have a label of "Foo", not "ThirdSub".
@chepner: Working on that.
Nice. A metaclass is more elegant, but this is easier to use if another metaclass (like ABCMeta) is already in use in the class hierarchy.
PEP-487 aims at providing a simpler way of customizing classes, using a new magic method rather than a metaclass. Once accepted, it would be useful for this type of problem.
|
6

A metaclass might be useful here.

class Labeller(type):
    def __new__(meta, name, bases, dct):
        dct.setdefault('label', name)
        return super(Labeller, meta).__new__(meta, name, bases, dct)

# Python 2
# class Super(object):
#    __metaclass__ = Labeller

class Super(metaclass=Labeller):
    pass

class Sub(Super):
    pass

class SecondSub(Super):
    label = 'Pie'

class ThirdSub(SecondSub):
    pass

Disclaimer: when providing a custom metaclass for your class, you need to make sure it is compatible with whatever metaclass(es) are used by any class in its ancestry. Generally, this means making sure your metaclass inherits from all the other metaclasses, but it can be nontrivial to do so. In practice, metaclasses aren't so commonly used, so it's usually just a matter of subclassing type, but it's something to be aware of.

2 Comments

Could you point to some explanation about why super(Labeller, meta) must be used instead of just super()?
Python 3 introduced the ability of super to be used without explicit arguments; I am more accustomed to Python 2, and added the arguments out of habit.
1

As of Python 3.6, the cleanest way to achieve this is with __init_subclass__ hook introduced in PEP 487. It is much simpler (and easier to manage with respect to inheritance) than using a metaclass.

class Base:
    @classmethod
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        if 'label' not in cls.__dict__:  # Check if label has been set in the class itself, i.e. not inherited from any of its superclasses
            cls.label = cls.__name__  # If not, default to class's __name__

class Sub1(Base):
    pass
    
class Sub2(Base):
    label = 'Custom'

class SubSub(Sub2):
    pass

print(Sub1.label)    # Sub1
print(Sub2.label)    # Custom
print(SubSub.label)  # SubSub

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.