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)
user2004245
  • 399
  • 1
  • 6
  • 16

4 Answers4

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()
Community
  • 1
  • 1
Joran Beasley
  • 110,522
  • 12
  • 160
  • 179
  • I was just running to ipython to try a dir(self) inside a class when you added the answer. Bravo! I learned something new! – nivix zixer May 21 '15 at 18:53
  • Would I be able to reference that instance property again inside of a @classmethod? – user2004245 May 21 '15 at 18:54
  • @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). – poke May 21 '15 at 18:55
  • 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. – user2004245 May 21 '15 at 18:56
  • 1
    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. – user2357112 May 21 '15 at 18:57
  • you never specified that you needed it in a @classmethod (not sure why you need it in a @classmethod ... since you can just access `cls.__name__` – Joran Beasley May 21 '15 at 18:58
  • added a solution that makes it available at the classmethod level – Joran Beasley May 21 '15 at 19:03
  • Never mind about the data descriptor thing - I forgot that those only override the instance dict, not subclass dicts. – user2357112 May 21 '15 at 19:23
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'
user2357112
  • 260,549
  • 28
  • 431
  • 505
  • great answer and you beat my edit that does this as well (+1 from me good sir :) ) – Joran Beasley May 21 '15 at 19:04
  • 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 May 21 '15 at 19:13
  • @chepner: Working on that. – user2357112 May 21 '15 at 19:15
  • 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. – chepner May 21 '15 at 20:10
  • @chepner: That's the kind of thing that makes me consider the metaclass more hassle than it's worth for this. Two unrelated descriptors mix just fine; two unrelated metaclasses produces a TypeError. If someone wants to mix-in `collections.Sequence` or something like that in a subclass, I'd prefer to let them. – user2357112 May 21 '15 at 20:15
  • 1
    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. – chepner May 21 '15 at 20:25
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.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • Could you point to some explanation about why `super(Labeller, meta)` must be used instead of just `super()`? – mkrieger1 Jan 10 '17 at 23:04
  • 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. – chepner Jan 11 '17 at 01:06
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
nirvana-msu
  • 3,877
  • 2
  • 19
  • 28