Magic Methods are a broad and general term that refers to “special” methods in a Python class. There is no single definition for all of them, as their use is diverse. For example, a few common and well-known magic methods include:
What do all these methods have in common? Well, obviously they all start and end with double underscores ( class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.does_not_exist # 'DOES_NOT_EXIST' d.what_about_this_one # 'WHAT_ABOUT_THIS_ONE'6). But aside from that, what makes them “magic methods” is that they’re invoked somehow “specially”. We don’t manually invoke these methods; Python is the one doing it. For example, we don’t do class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.does_not_exist # 'DOES_NOT_EXIST' d.what_about_this_one # 'WHAT_ABOUT_THIS_ONE'7, we do class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.does_not_exist # 'DOES_NOT_EXIST' d.what_about_this_one # 'WHAT_ABOUT_THIS_ONE'8. There are many magic methods, but as we will focus on class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.does_not_exist # 'DOES_NOT_EXIST' d.what_about_this_one # 'WHAT_ABOUT_THIS_ONE'9and class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"0 in this post. __getattr__Let’s start with class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"1. This method will allow you to “catch” references to attributes that don’t exist in your object. Let’s see a simple example to understand how it works: class Dummy(object): passd = Dummy() d.does_not_exist # Fails with AttributeError In this example, the attribute access fails (with an class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"2) because the attribute class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"3 doesn’t exist. But using the __getattr__ magic method, we can intercept that inexistent attribute lookup and do something so it doesn’t fail: class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.does_not_exist # 'DOES_NOT_EXIST' d.what_about_this_one # 'WHAT_ABOUT_THIS_ONE' But if the attribute does exist, class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"1 won’t be invoked: class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python" __getattribute__class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"0 is similar to class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"1, with the important difference that class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"0 will intercept EVERY attribute lookup, doesn’t matter if the attribute exists or not. Let me show you a simple example: class Dummy(object): def __getattribute__(self, attr): return 'YOU SEE ME?'d = Dummy() d.value = "Python" print(d.value) # "YOU SEE ME?" In that example, the class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"8 object already has an attribute class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"9. But when we try to access it, we don’t get the original expected value (“Python”); we’re just getting whatever class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"0 returned. It means that we’ve virtually lost the class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"9 attribute; it has become “unreachable”. If you ever need to use class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"0 to simulate something similar to class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"1 you’ll have to do some more advanced Python handling: class Dummy(object): def __getattribute__(self, attr): __dict__ = super(Dummy, self).__getattribute__('__dict__') if attr in __dict__: return super(Dummy, self).__getattribute__(attr) return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python" print(d.does_not_exist) # "DOES_NOT_EXIST" A more realistic exampleIt’s not common to just randomly catch every attribute lookup. We generally use class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"1 with a clear objective in mind: whenever we want to provide some dynamic access to our objects. A good example could be extending the base Python tuple to add it some Scala flavor to it. In Scala, tuples are created really similarly to Python: val a_tuple = ("z", 3, "Python", -1) But they’re accessed in a different way: println(a_tuple._1) // “z” println(a_tuple._3) // “Python” Each element in the tuple is accessed as an attribute, with the first element being the attribute class Dummy(object): def __getattribute__(self, attr): return 'YOU SEE ME?'d = Dummy() d.value = "Python" print(d.value) # "YOU SEE ME?"5, the second class Dummy(object): def __getattribute__(self, attr): return 'YOU SEE ME?'d = Dummy() d.value = "Python" print(d.value) # "YOU SEE ME?"6, and so on. In that example, you can see how we’re catching missing attributes with class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"1, but if that attribute is not in the form of class Dummy(object): def __getattribute__(self, attr): return 'YOU SEE ME?'d = Dummy() d.value = "Python" print(d.value) # "YOU SEE ME?"8 (where class Dummy(object): def __getattribute__(self, attr): return 'YOU SEE ME?'d = Dummy() d.value = "Python" print(d.value) # "YOU SEE ME?"9 is an integer), we just raise the class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"2 manually. We can easily extend our common Python tuple to match this behavior, the code is really simple: class Tuple(tuple): def __getattr__(self, name): def _int(val): try: return int(val) except ValueError: return False if not name.startswith('_') or not _int(name[1:]): raise AttributeError("'tuple' object has no attribute '%s'" % name) index = _int(name[1:]) - 1 return self[index] t = Tuple(['z', 3, 'Python', -1]) print(t._1) # 'z' print(t._2) # 3 print(t._3) # 'Python' t = Tuple(['z', 3, 'Python', -1]) assert t._1 == 'z' assert t._2 == 3 assert t._3 == 'Python' assert t._4 == -1 getattr()class Dummy(object): def __getattribute__(self, attr): __dict__ = super(Dummy, self).__getattribute__('__dict__') if attr in __dict__: return super(Dummy, self).__getattribute__(attr) return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python" print(d.does_not_exist) # "DOES_NOT_EXIST"1is one of Python’s built-in functions, its role is to get the properties of the object.
Example: class Foo: def __init__(self, x): self.x = x f = Foo(10) getattr(f, 'x') f.x # 10 getattr(f, 'y', 'bar') # 'bar' __getattr__class Dummy(object): def __getattribute__(self, attr): __dict__ = super(Dummy, self).__getattribute__('__dict__') if attr in __dict__: return super(Dummy, self).__getattribute__(attr) return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python" print(d.does_not_exist) # "DOES_NOT_EXIST"2Is an object method that is called if the object’s properties are not found. This method should return the property value or throw class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"2. Note that if the object property can be found through the normal mechanism, it will not be called. class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"1method. Example: class Frob:... def __init__(self, bamf): self.bamf = bamf def __getattr__(self, name): return 'Frob does not have `{}` attribute.'.format(str(name)) f = Frob("bamf") f.bar # 'Frob does not have `bar` attribute.' f.bam # f'bamf' __getattribute__This method is called unconditionally when accessing the properties of an object. This method only works for new classes. If the class is also defined at the same time class Dummy(object): def __getattribute__(self, attr): __dict__ = super(Dummy, self).__getattribute__('__dict__') if attr in __dict__: return super(Dummy, self).__getattribute__(attr) return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python" print(d.does_not_exist) # "DOES_NOT_EXIST"5), it will not be called class Dummy(object): def __getattribute__(self, attr): __dict__ = super(Dummy, self).__getattribute__('__dict__') if attr in __dict__: return super(Dummy, self).__getattribute__(attr) return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python" print(d.does_not_exist) # "DOES_NOT_EXIST"6 unless class Dummy(object): def __getattribute__(self, attr): __dict__ = super(Dummy, self).__getattribute__('__dict__') if attr in __dict__: return super(Dummy, self).__getattribute__(attr) return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python" print(d.does_not_exist) # "DOES_NOT_EXIST"7 shows the call class Dummy(object): def __getattribute__(self, attr): __dict__ = super(Dummy, self).__getattribute__('__dict__') if attr in __dict__: return super(Dummy, self).__getattribute__(attr) return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python" print(d.does_not_exist) # "DOES_NOT_EXIST"6Or thrown class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"2. The method should return the property value or throw class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"2. To avoid infinite recursion in methods, you should always use the methods of the base class to get the properties: class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.does_not_exist # 'DOES_NOT_EXIST' d.what_about_this_one # 'WHAT_ABOUT_THIS_ONE'0 grammar: val a_tuple = ("z", 3, "Python", -1)1 Example: class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.does_not_exist # 'DOES_NOT_EXIST' d.what_about_this_one # 'WHAT_ABOUT_THIS_ONE'1 __ get __val a_tuple = ("z", 3, "Python", -1)2The method is one of the descriptor methods. Descriptors are used to transform access object properties into call descriptor methods. Example: class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.does_not_exist # 'DOES_NOT_EXIST' d.what_about_this_one # 'WHAT_ABOUT_THIS_ONE'2 ConclusionMagic Methods are a great mechanism to extend the basic features of Python classes and objects and provide more intuitive interfaces. You can provide dynamic attribute lookups with class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"1 for those missing attributes that you want to intercept. But be careful with class Dummy(object): def __getattr__(self, attr): return attr.upper()d = Dummy() d.value = "Python" print(d.value) # "Python"0, because it might be tricky to implement correctly without losing attributes in the Python void. |