☰
Current Page
Main Menu
Home
Home
Editing Amazing Functools Features in Python
Edit
Preview
H1
H2
H3
default
Set your preferred keybinding
default
vim
emacs
markdown
Set this page's format to
AsciiDoc
Creole
Markdown
MediaWiki
Org-mode
Plain Text
RDoc
Textile
Rendering unavailable for
BibTeX
Pod
reStructuredText
Help 1
Help 1
Help 1
Help 2
Help 3
Help 4
Help 5
Help 6
Help 7
Help 8
Autosaved text is available. Click the button to restore it.
Restore Text
https://python.plainenglish.io/amazing-functools-features-in-python-3130684a9c37 # Amazing Functools Features in Python | by Vivek K. Singh | Python in Plain English [  ](https://vivekhere.medium.com/?source=post_page---byline--3130684a9c37--------------------------------) [  ](https://python.plainenglish.io/?source=post_page---byline--3130684a9c37--------------------------------) I was recently reading Django’s Source Code, and I came across the @wraps decorator, which led me to the functools docs, where I discovered some fantastic functools features. That discovery led to the creation of this article. This tutorial will teach you how to use some fantastic functools methods to make your life simpler. What is `functools?` -------------------- functools is a Python built-in module that contains Higher Order functions that can interact with other functions. A complete functools documentation may be found [here](https://docs.python.org/3/library/functools.html). Let’s see some decorators in action. lru\_cache ---------- When invoking a function with the same arguments, this decorator in the functools module saves n number of function calls in cache, which saves a lot of time. Assume for the sake of demonstration that we have a very large function that takes a long time to execute. The function _a\_heavy\_operation()_ takes 3 seconds to execute in this example. ``` import time start = time.time() def a_heavy_operation(): time.sleep(3) return 11 + 22 print(a_heavy_operation()) print(a_heavy_operation()) print(time.time() - start) # Output # 33 # 33 # 6.024240255355835 ``` It takes about 6 seconds to run the above code. To the above function, we’ll add lru cache. ``` import time from functools import lru_cache start = time.time() @lru_cache() def a_heavy_operation(): time.sleep(3) return 11 + 22 print(a_heavy_operation()) print(a_heavy_operation()) print(time.time() - start) # Output # 33 # 33 # 3.0158064365386963 ``` Take a look at how using lru cache made our code run faster. Python saved the function’s cache and retrieved the cached value, reducing our execution time. wraps ----- Wraps is used in functools to keep the function details. When we decorate a function, the function’s information is gone. We utilise the @wraps decorator on the decorator wrapper function to prevent this. Take a look at this code to see what I mean. ``` from functools import lru_cache def my_decorator(func): def log(*args, **kwargs): print("Running ") return func(*args, *kwargs) return log @my_decorator def add(a, b): """my beautiful doc""" return a + b ``` Run the above code in -i mode using, `python -i file.py` Let's see what we have: ``` >>> add(1,2) Running 3 >>> add(3,4) Running 7 >>> add.__name__ log >>> add.__doc__ >>> ``` We can see that our decorator is operating properly in the previous example, since it is consistently “Running” on each run. However, our function’s information has been lost, and it is unable to return the name or the docstring. We have @wraps to help us with this problem. Make the changes below to the code. ``` from functools import wraps def my_decorator(func): @wraps(func) def log(*args, **kwargs): print("Running ") return func(*args, *kwargs) return log @my_decorator def add(a, b): """my beautiful doc""" return a + b ``` Now again run the code using `python -i file.py` ``` >>> add(1,2) Running 3 >>> add.__name__ 'add' >>> add.__doc__ 'my beautiful doc' >>> ``` Voila! The function information is now saved in our function. singledispatch -------------- To create a generic function, singledispatch is utilised. Generic functions are those that perform the same operation on a variety of data types. Assume I want to create a function that returns the first value from an iterable of several data types. ``` def return_first_element(data): if isinstance(data, list): print(data[0]) elif isinstance(data, str): print(data.split()[0]) elif isinstance(data, dict): print(list(data.values())[0] ) else: print(print(data)) ``` Now run `python -i file.py` to run the code in interactive mode. ``` >>> return_first_element({"Age":20, "Height": 180}) 20 >>> return_first_element("Hello Mr Python") Hello >>> return_first_element([12,432,563]) 12 >>> ``` Our function is effective, but it isn’t clean. Using if/elif/else statements to create generic functions is not recommended in Python. So, what’s the solution? singledispatch, of course. Let’s make a few modifications to our code. ``` from functools import singledispatch @singledispatch def return_first_el(data): return data @return_first_el.register(list) def _(data): return data[0] @return_first_el.register(dict) def _(data): return list(data.values())[0] @return_first_el.register(str) def _(data): return data.split()[0] ``` To check the results, run the code again in interactive mode with _python -i file.py._ ``` >>> return_first_el({"Age":20, "Height": 180}) 20 >>> return_first_el("Hello Mr Python") 'Hello' >>> return_first_el([124, 765, 897]) 124 >>> return_first_el({12,31,1}) {1, 12, 31} ``` Look how our `return_first_el` function acted as a fallback function when no data type matched for ‘set’. Look at how much cleaner our code is now; the singledispatch made it easier to add more data types, and each datatype now gets its own place where we can perform further operations on the data. total\_ordering --------------- The total\_ordering decorator saves a ton of time in Object Oriented Progrmming. Consider this example, the below class declares a class `Man` with name and age property and (=) \_\_**eq\_\_** and (<) \_\_l**t\_\_** dunder methods. ``` class Man: def __init__(self, name, age): self.name = name self.age = age def __eq__(self, o): return self.age == o.age def __lt__(self, o): return self.age < o.age ``` Let’s see what happens if we run the code. ``` >>> obj = Man("Vivek", 20) >>> obj2 = Man("Alex", 24) >>> obj = obj >>> obj == obj2 False >>> obj < obj2 True >>> obj >= obj2 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: '>=' not supported between instances of 'Man' and 'Man' ``` Our code worked for (==) and (<), but it didn’t work when we used an operator that wasn’t defined in the class. Given that we create at least one operator dunder method and \_\_eq\_\_ method, @total\_ordering generates the,>,=,>=, and more comparison operators for our class. Let’s add our decorator just above the class. ``` from functools import total_ordering @total_ordering class Man: ..... ..... ``` Now again run thee code in interactive mode to see the results ``` >>> o = Man("Vivek", 20) >>> b = Man("Alex", 24) >>> o == b False >>> o >= b False >>> o <= b True ``` Take a look at how total ordering generated our class’s comparison operators. Conclusion ---------- I hope you found this post useful; I strongly advise you to study the documentation in order to fully comprehend the internal mechanics of these higher level functions. If you enjoyed this, please consider following me on [Twitter](https://twitter.com/vivekthedev), where I share stuff related to Python, Web development, and open source software. I’ll see you there.
Uploading file...
Edit message:
Cancel