Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding the __str__ method for @classmethods in python

I was trying to override __str__ and __repr__ for class as shown in the code below. The new methods are called whenever I call the instance_method but the object calls for class_method remains the same (Please see the code snippet and the output below for clarity). Is there a way possible to override __str__ and __repr__ for @classmethod so that the value of cls can be changed?

I have also tried adding __str__ and __repr__ as @classmethod but nothing changed.

class Abc:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f"Added {self.name}"

    def __repr__(self):
        return f"instance method repr"

    def instance_method(self):
        print(f"instance method {self}")

    @classmethod
    def __repr__(cls):
        return f"class method"

    @classmethod
    def __str__(cls):
        return f"class method"

    @classmethod
    def class_method(cls):
        print(f"class method '{cls}'")

    @staticmethod
    def static_method():
        print(f"static method")

    def add(self, a: int,b: int,c: int) -> int:
        return a+b+c


o = Abc("alpha")
o.class_method()
o.static_method()
o.instance_method()
Abc.static_method()
Abc.class_method()
print(o.add(1,2,3))

Output of the above code:

class method '<class '__main__.Abc'>'
static method
instance method class method
static method
class method '<class '__main__.Abc'>'
6
like image 682
shrey khare Avatar asked Sep 12 '19 17:09

shrey khare


Video Answer


1 Answers

Python doesn't look for a __str__ on the class itself, just like it won't use __str__ set on an instance. This applies to all special methods, see Special method lookup in the Python datamodel:

For custom classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not in the object’s instance dictionary.

In short, str(something) does not use something.__str__(), it essentially uses type(something).__str__(something) (*) exactly because you wouldn't want the definition of __str__ on a class to break when you use str(class_object), where class_object.__str__() doesn't have an instance to pass in as self.

You'll have to define a metaclass, because that's the 'thing' that makes classes and is returned by type(class_object):

class MetaAbc(type):
    def __repr__(cls):
        return "__repr__ on the metaclass"

    def __str__(cls):
        return "__str__ on the metaclass"

class Abc(metaclass=MetaAbc):
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f"Added {self.name}"

    def __repr__(self):
        return "instance method repr"

The metaclass=MetaAbc syntax tells Python to use MetaAbc instead of just type as metaclass of the Abc class; now type(Abc) returns MetaAbc:

>>> type(Abc)
<class '__main__.MetaAbc'>

and MetaAbc.__repr__ and MetaAbc.__str__ are used when representing a class, or converting it to a string; the methods on the class are used when dealing with an instance:

>>> Abc
__repr__ on the metaclass
>>> print(Abc)
__str__ on the metaclass
>>> Abc('foo')
instance method repr
>>> print(Abc('foo'))
Added foo

The @classmethod decorator does not put a method into a different namespace; class methods are normal attributes of a class and are simply bound differently. @classmethod's are still accessible on the instance, for example, but will always be passed the class object, even when accessed via the instance:

>>> Abc.class_method()
class method '__str__ on the metaclass'
>>> Abc("foo").class_method()
class method '__str__ on the metaclass'

(*) Python uses descriptor binding to implement methods, classmethods and staticmethods. Special method lookups look up the function object directly by traversing the class hierarchy to avoid triggering the normal binding process, then bind them manually. So str(something) translates to next(c.__dict__['__str__'] for c in type(something).__mro__ if '__str__' in c.__dict__).__get__(something, type(something))(). That's a bit of a mouthful, for normal methods this can be simplified to type(something).__str__(something) as that has the same effect.

like image 132
Martijn Pieters Avatar answered Nov 03 '22 03:11

Martijn Pieters