{"id":291,"date":"2024-12-01T07:02:37","date_gmt":"2024-12-01T07:02:37","guid":{"rendered":"https:\/\/mailitics.com\/index.php\/2024\/12\/01\/dunder-methods-the-hidden-gems-of-python-a234e29b192d\/"},"modified":"2024-12-01T07:02:37","modified_gmt":"2024-12-01T07:02:37","slug":"dunder-methods-the-hidden-gems-of-python-a234e29b192d","status":"publish","type":"post","link":"https:\/\/mailitics.com\/index.php\/2024\/12\/01\/dunder-methods-the-hidden-gems-of-python-a234e29b192d\/","title":{"rendered":"Dunder Methods: The Hidden Gems of Python"},"content":{"rendered":"<p>    Dunder Methods: The Hidden Gems of Python<br \/>\n \t<BR><br \/>\n<BR><\/BR><br \/>\n    <!-- no image --><br \/>\n \t<BR><br \/>\n<BR><\/BR><\/p>\n<div>\n<h4>Real-world examples on how actively using special methods can simplify coding and improve readability.<\/h4>\n<p>Dunder methods, though possibly a basic topic in Python, are something I have often noticed being understood only superficially, even by people who have been coding for quite some\u00a0time.<\/p>\n<p><strong>Disclaimer: <\/strong>This is a forgivable gap, as in most cases, actively using dunder methods \u201csimply\u201d speeds up and standardize tasks that can be done differently. Even when their use is essential, programmers are often unaware that they are writing special methods that belong to the broader category of dunder\u00a0methods.<\/p>\n<p>Anyway, if you code in Python and are not familiar with this topic, or if you happen to be a code geek intrigued by the more native aspects of a programming language like I am, this article might just be what you\u2019re looking\u00a0for.<\/p>\n<h4><strong>Appearances can deceive\u2026 even in\u00a0Python!<\/strong><\/h4>\n<p>If there is one thing I learned in my life is that not everything is what it seems like at a first look, and Python is no exception.<\/p>\n<figure><img decoding=\"async\" alt=\"\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1024\/0*_Wt6GwCNJEfYDcGi\"><figcaption>Photo by <a href=\"https:\/\/unsplash.com\/@ro_ka?utm_source=medium&amp;utm_medium=referral\">Robert Katzki<\/a> on\u00a0<a href=\"https:\/\/unsplash.com\/?utm_source=medium&amp;utm_medium=referral\">Unsplash<\/a><\/figcaption><\/figure>\n<p>Let us consider a seemingly simple\u00a0example:<\/p>\n<pre>class EmptyClass:<br>    pass<\/pre>\n<p>This is the \u201cemptiest\u201d custom class we can define in Python, as we did not define attributes or methods. It is so empty you would think you can do nothing with\u00a0it.<\/p>\n<p>However, this is not the case. For example, Python will not complain if you try to create an instance of this class or even compare two instances for equality:<\/p>\n<pre>empty_instance = EmptyClass()<br>another_empty_instance = EmptyClass()<br><br>&gt;&gt;&gt; empty_instance == another_empty_instance<br>False<\/pre>\n<p>Of course, this is not magic. Simply, leveraging a standard <strong><em>object <\/em><\/strong>interface, any object in Python inherits some default attributes and methods that allow the user to always have a minimal set of possible interactions with\u00a0it.<\/p>\n<p>While these methods may seem hidden, they are not invisible. To access the available methods, including the ones assigned by Python itself, just use the <strong><em>dir()<\/em><\/strong> built-in function. For our empty class, we\u00a0get:<\/p>\n<pre>&gt;&gt;&gt; dir(EmptyClass)<br>['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', <br>'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', <br>'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', <br>'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', <br>'__str__', '__subclasshook__', '__weakref__']<\/pre>\n<p>It is these methods that can explain the behaviour we observed earlier. For example, since the class actually has an <strong><em>__init__ <\/em><\/strong>method we should not be surprised that we can instantiate an object of the\u00a0class.<\/p>\n<h4><strong>Meet the Dunder\u00a0Methods<\/strong><\/h4>\n<p>All the methods shown in the last output belongs to the special group of\u200a\u2014\u200aguess what\u200a\u2014\u200adunder methods. The term \u201cdunder\u201d is short for double underscore, referring to the double underscores at the beginning and end of these method\u00a0names.<\/p>\n<p>They are special for several\u00a0reasons:<\/p>\n<ol>\n<li>\n<strong>They are built into every object<\/strong>: every Python object is equipped with a specific set of dunder methods determined by its\u00a0type.<\/li>\n<li>\n<strong>They are invoked implicitly<\/strong>: many dunder methods are triggered automatically through interactions with Python\u2019s native operators or built-in functions. For example, comparing two objects with <strong><em>==<\/em><\/strong> is equivalent to calling their <strong><em>__eq__<\/em><\/strong>\u00a0method.<\/li>\n<li>\n<strong>They are customizable<\/strong>: you can override existing dunder methods or define new ones for your classes to give them custom behavior while preserving their implicit invocation.<\/li>\n<\/ol>\n<p>For most Python developers, the first dunder they encounter is <strong><em>__init__<\/em><\/strong>, the constructor method. This method is automatically called when you create an instance of a class, using the familiar syntax <strong><em>MyClass(*args, **kwargs)<\/em><\/strong> as a shortcut for explicitly calling <strong><em>MyClass.__init__(*args, **kwargs).<\/em><\/strong><\/p>\n<p>Despite being the most commonly used, <strong><em>__init__<\/em><\/strong> is also one of the most specialized dunder methods. It does not fully showcase the flexibility and power of dunder methods, which can allow you to redefine how your objects interact with native Python features.<\/p>\n<h4>Make an object\u00a0pretty<\/h4>\n<p>Let us define a class representing an item for sale in a shop and create an instance of it by specifying the name and\u00a0price.<\/p>\n<pre>class Item:<br>    def __init__(self, name: str, price: float) -&gt; None:<br>        self.name = name<br>        self.price = price<br><br><br>item = Item(name=\"Milk (1L)\", price=0.99)<\/pre>\n<p>What happens if we try to display the content of the <em>item <\/em>variable? Right now, the best Python can do is tell us what type of object it is and where it is allocated in\u00a0memory:<\/p>\n<pre>&gt;&gt;&gt; item<br>&lt;__main__.Item at 0x00000226C614E870&gt;<\/pre>\n<p>Let\u2019s try to get a more informative and pretty\u00a0output!<\/p>\n<figure><img decoding=\"async\" alt=\"\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1024\/0*OG_hDPXIFjiIWP9W\"><figcaption>Photo by <a href=\"https:\/\/unsplash.com\/@shamblenstudios?utm_source=medium&amp;utm_medium=referral\">Shamblen Studios<\/a> on\u00a0<a href=\"https:\/\/unsplash.com\/?utm_source=medium&amp;utm_medium=referral\">Unsplash<\/a><\/figcaption><\/figure>\n<p>To do that, we can override the <strong><em>__repr__ <\/em><\/strong>dunder, which output will be exactly what gets printed when typing a class instance in the interactive Python console but also\u200a\u2014\u200aas soon as the other dunder method <strong><em>__str__<\/em><\/strong> is not override\u200a\u2014\u200awhen attempting a <strong><em>print()<\/em><\/strong>\u00a0call.<\/p>\n<p><strong>Note<\/strong>: it is a common practice to have <strong><em>__repr__<\/em><\/strong> provide the necessary syntax to recreate the printed instance. So in that latter case we expect the output to be <em>Item(name=\u201dMilk (1L)\u201d, price=0.99).<\/em><\/p>\n<pre>class Item:<br>    def __init__(self, name: str, price: float) -&gt; None:<br>        self.name = name<br>        self.price = price<br><br>    def __repr__(self) -&gt; str:<br>        return f\"{self.__class__.__name__}('{self.name}', {self.price})\"<br><br><br>item = Item(name=\"Milk (1L)\", price=0.99)<br><br>&gt;&gt;&gt; item # In this example it is equivalent also to the command: print(item)<br>Item('Milk (1L)', 0.99)<\/pre>\n<p>Nothing special, right? And you would be right: we could have implemented the same method and named it <em>my_custom_repr <\/em>without getting indo dunder methods. However, while anyone immediately understands what we mean with <strong><em>print(item) <\/em><\/strong>or just <strong><em>item<\/em><\/strong>, can we say the same for something like <strong><em>item.my_custom_repr()<\/em><\/strong>?<\/p>\n<p><strong>Define interaction between an object and Python\u2019s native operators<\/strong><\/p>\n<p>Imagine we want to create a new class<em>, Grocery<\/em>, that allows us to build a collection of <em>Item <\/em>along with their quantities.<\/p>\n<p>In this case, we can use dunder methods for allowing some standard operations like:<\/p>\n<ol>\n<li>Adding a specific quantity of <em>Item <\/em>to the <em>Grocery <\/em>using the <strong><em>+<\/em><\/strong>\u00a0operator<\/li>\n<li>Iterating directly over the <em>Grocery <\/em>class using a <strong><em>for<\/em><\/strong>\u00a0loop<\/li>\n<li>Accessing a specific Item from the Grocery class using the bracket<strong><em> []\u00a0<\/em><\/strong>notation<\/li>\n<\/ol>\n<p>To achieve this, we will <strong>define (<\/strong>we already see that<strong> <\/strong>a generic class do not have these methods by default) the dunder methods <strong><em>__add__<\/em><\/strong>, <strong><em>__iter__<\/em><\/strong> and <strong><em>__getitem__<\/em><\/strong> respectively.<\/p>\n<pre>from typing import Optional, Iterator<br>from typing_extensions import Self<br><br><br>class Grocery:<br><br>    def __init__(self, items: Optional[dict[Item, int]] = None):<br>        self.items = items or dict()<br><br>    def __add__(self, new_items: dict[Item, int]) -&gt; Self:<br><br>        new_grocery = Grocery(items=self.items)<br><br>        for new_item, quantity in new_items.items():<br><br>            if new_item in new_grocery.items:<br>                new_grocery.items[new_item] += quantity<br>            else:<br>                new_grocery.items[new_item] = quantity<br><br>        return new_grocery<br><br>    def __iter__(self) -&gt; Iterator[Item]:<br>        return iter(self.items)<br><br>    def __getitem__(self, item: Item) -&gt; int:<br><br>        if self.items.get(item):<br>            return self.items.get(item)<br>        else:<br>            raise KeyError(f\"Item {item} not in the grocery\")<\/pre>\n<p>Let us initialize a <em>Grocery <\/em>instance and print the content of its main attribute, <em>items.<\/em><\/p>\n<pre>item = Item(name=\"Milk (1L)\", price=0.99)<br>grocery = Grocery(items={item: 3})<br><br>&gt;&gt;&gt; print(grocery.items)<br>{Item('Milk (1L)', 0.99): 3}<\/pre>\n<p>Then, we use the <strong><em>+<\/em><\/strong> operator to add a new Item and verify the changes have taken\u00a0effect.<\/p>\n<pre>new_item = Item(name=\"Soy Sauce (0.375L)\", price=1.99)<br>grocery = grocery + {new_item: 1} + {item: 2}<br><br>&gt;&gt;&gt; print(grocery.items)<br>{Item('Milk (1L)', 0.99): 5, Item('Soy Sauce (0.375L)', 1.99): 1}<\/pre>\n<p>Friendly and explicit, right?<\/p>\n<p>The<strong><em> __iter__ <\/em><\/strong>method allows us to loop through a <em>Grocery <\/em>object following the logic implemented in the method (i.e., implicitly the loop will iterate over the elements contained in the iterable attribute <em>items<\/em>).<\/p>\n<pre>&gt;&gt;&gt; print([item for item in grocery])<br>[Item('Milk (1L)', 0.99), Item('Soy Sauce (0.375L)', 1.99)]<\/pre>\n<p>Similarly, accessing elements is handled by defining the <strong><em>__getitem__<\/em><\/strong> dunder:<\/p>\n<pre>&gt;&gt;&gt; grocery[new_item]<br>1<br><br>fake_item = Item(\"Creamy Cheese (500g)\", 2.99)<br>&gt;&gt;&gt; grocery[fake_item]<br>KeyError: \"Item Item('Creamy Cheese (500g)', 2.99) not in the grocery\"<\/pre>\n<p>In essence, we assigned some standard dictionary-like behaviours to our Grocery class while also allowing some operations that would not be natively available for this data\u00a0type.<\/p>\n<p><strong>Enhance functionality: make classes callable for simplicity and\u00a0power.<\/strong><\/p>\n<p>Let us wrap up this deep-dive on dunder methods with a final eample showcasing how they can be a powerful tool in our\u00a0arsenal.<\/p>\n<figure><img decoding=\"async\" alt=\"\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1024\/0*VySYur0IfnoN_bok\"><figcaption>Photo by <a href=\"https:\/\/unsplash.com\/@jccards?utm_source=medium&amp;utm_medium=referral\">Marek Studzinski<\/a> on\u00a0<a href=\"https:\/\/unsplash.com\/?utm_source=medium&amp;utm_medium=referral\">Unsplash<\/a><\/figcaption><\/figure>\n<p>Imagine we have implemented a function that performs deterministic and slow calculations based on a certain input. To keep things simple, as an example we will use an identity function with a built-in <strong><em>time.sleep<\/em><\/strong> of some\u00a0seconds.<\/p>\n<pre>import time <br><br><br>def expensive_function(input):<br>    time.sleep(5)<br>    return input<\/pre>\n<p>What happens if we run the function twice on the same input? Well, right now calculation would be executed twice, meaning that we twice get the same output waiting two time for the whole execution time (i.e., a total of 10 seconds).<\/p>\n<pre>start_time = time.time()<br><br>&gt;&gt;&gt; print(expensive_function(2))<br>&gt;&gt;&gt; print(expensive_function(2))<br>&gt;&gt;&gt; print(f\"Time for computation: {round(time.time()-start_time, 1)} seconds\")<br>2<br>2<br>Time for computation: 10.0 seconds<\/pre>\n<p>Does this make sense? Why should we do the same calculation (which leads to the same output) for the same input, especially if it\u2019s a slow\u00a0process?<\/p>\n<p>One possible solution is to \u201cwrap\u201d the execution of this function inside the <strong><em>__call__<\/em><\/strong> dunder method of a\u00a0class.<\/p>\n<p>This makes instances of the class callable just like functions\u200a\u2014\u200ameaning we can use the straightforward syntax <strong><em>my_class_instance(*args, **kwargs)<\/em><\/strong>\u200a\u2014\u200awhile also allowing us to use attributes as a cache to cut computation time.<\/p>\n<p>With this approach we also have the flexibility to create multiple process (i.e., class instances), each with its own local\u00a0cache.<\/p>\n<pre>class CachedExpensiveFunction:<br><br>    def __init__(self) -&gt; None:<br>        self.cache = dict()<br><br>    def __call__(self, input):<br>        if input not in self.cache:<br>            output = expensive_function(input=input)<br>            self.cache[input] = output<br>            return output<br>        else:<br>            return self.cache.get(input)<br><br><br>start_time = time.time()<br>cached_exp_func = CachedExpensiveFunction()<br><br>&gt;&gt;&gt; print(cached_exp_func(2))<br>&gt;&gt;&gt; print(cached_exp_func(2))<br>&gt;&gt;&gt; print(f\"Time for computation: {round(time.time()-start_time, 1)} seconds\")<br>2<br>2<br>Time for computation: 5.0 seconds<\/pre>\n<p>As expected, the function is cached after the first run, eliminating the need for the second computation and thus cutting the overall time in\u00a0half.<\/p>\n<p>As above mentioned, we can even create separate instances of the class, each with its own cache, if\u00a0needed.<\/p>\n<pre>start_time = time.time()<br>another_cached_exp_func = CachedExpensiveFunction()<br><br>&gt;&gt;&gt; print(cached_exp_func(3))<br>&gt;&gt;&gt; print(another_cached_exp_func (3))<br>&gt;&gt;&gt; print(f\"Time for computation: {round(time.time()-start_time, 1)} seconds\")<br>3<br>3<br>Time for computation: 10.0 seconds<\/pre>\n<p>Here we are! A simple yet powerful optimization trick made possible by dunder methods that not only reduces redundant calculations but also offers flexibility by allowing local, instance-specific caching.<\/p>\n<h4>My final considerations<\/h4>\n<p>Dunder methods are a broad and ever-evolving topic, and this writing does not aim to be an exhaustive resource on the subject (for this purpose, you can refer to the <a href=\"https:\/\/docs.python.org\/3\/reference\/datamodel.html\">3. Data model\u200a\u2014\u200aPython 3.12.3 documentation<\/a>).<\/p>\n<p>My goal here was rather to explain clearly what they are and how they can be used effectively to handle some common use\u00a0cases.<\/p>\n<p>While they may not be mandatory for all programmers all the time, once I got a good grasp of how they work they have made a ton of difference for me and hopefully they may work for you as\u00a0well.<\/p>\n<p>Dunder methods indeed are a way to avoid reinventing the wheel. They also align closely with Python\u2019s philosophy, leading to a more concise, readable and convention-friendly code. And that never hurts,\u00a0right?<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/medium.com\/_\/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=a234e29b192d\" width=\"1\" height=\"1\" alt=\"\"><\/p>\n<hr>\n<p><a href=\"https:\/\/towardsdatascience.com\/dunder-methods-the-hidden-gems-of-python-a234e29b192d\">Dunder Methods: The Hidden Gems of Python<\/a> was originally published in <a href=\"https:\/\/towardsdatascience.com\/\">Towards Data Science<\/a> on Medium, where people are continuing the conversation by highlighting and responding to this story.<\/p>\n<\/div>\n<p> \t<BR><br \/>\n <BR><\/BR><br \/>\n    Federico Zabeo<br \/>\n \t<BR><br \/>\n<BR><\/BR><br \/>\n<a href=\"https:\/\/medium.com\/m\/global-identity-2?redirectUrl=https%3A%2F%2Ftowardsdatascience.com%2Fdunder-methods-the-hidden-gems-of-python-a234e29b192d\">Go to original source<\/a><br \/>\n \t<BR><br \/>\n <BR><\/BR><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Dunder Methods: The Hidden Gems of Python Real-world examples on how actively using special methods can simplify coding and improve readability. Dunder methods, though possibly a basic topic in Python, are something I have often noticed being understood only superficially, even by people who have been coding for quite some\u00a0time. Disclaimer: This is a forgivable [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[62,83,271,160,157,158],"tags":[272,102],"class_list":["post-291","post","type-post","status-publish","format-standard","hentry","category-aimldsaimlds","category-data-science","category-dunder-method","category-programming","category-python","category-tips-and-tricks","tag-methods","tag-python"],"_links":{"self":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/posts\/291"}],"collection":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/comments?post=291"}],"version-history":[{"count":0,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/posts\/291\/revisions"}],"wp:attachment":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/media?parent=291"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/categories?post=291"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/tags?post=291"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}