One of the strengths of the Python programming language is that it is both easy for beginners to learn and, at the same time, immensely rewarding for advanced users.
Data classes
Data classes are classes with the sole purpose of acting as containers of data. They usually do not contain any business logic inside them.
Python provides the @dataclass
decorator which, when added to a class, automatically generates several useful methods so you don't have to write them out by hand. These include the __init__
method for generating a constructor, __str__
and __repr__
for generating string representations and __eq__
for checking instances of the class for equality.
Defining a data class is simple.
@dataclass()
class Article:
title: str
author: str
published_on: datetime
views: int
likes: int
Optionally, you can also set default values for variables.
@dataclass()
class Article:
title: str = "John Doe"
author: str = "Unknown"
published_on: datetime = datetime.now()
views: int = 0
likes: int = 0
For the full list of automatically generated methods, you can check out the official documentation.
Any & All
Sometimes you need to have a check on multiple conditions before performing an action, such as in an if
statement. Python provides the boolean and
and or
operators for evaluating such logical expressions. However, when there are a very large number of conditions in play, your statement might begin to look a bit unwieldy.
The any()
and all()
methods, are simply a shorter and more readable way of evaluating a large number of boolean expressions at once. any()
is equivalent to a series of or
operations, while all()
is equivalent to a series of and
operations.
Suppose you are given a list of the marks a certain student scored ...
marks = [67, 85, 48, ]
passing_marks = 33
... and you want to check if the student qualifies to be promoted to the next year. One way to do this would be to simply join each comparison with the and
operator:
if marks[0] > passing_marks and marks[1] > passing_marks and marks[2] > passing_marks:
promoted = True
else:
promoted = False
You can make this code slightly more readable by using the all()
function:
if all([marks[0] > passing_marks, marks[1] > passing_marks, marks[2] > passing_marks]):
promoted = True
else:
promoted = False
Finally, you can combine it with a list comprehension to get a sweet, concise solution:
if all([subject_marks > passing_marks for subject_marks in marks]):
promoted = True
else:
promoted = False
The capabilities of these two functions can further be extended by combining them with the not
operator.
Below is a quick summary of these methods.
-
any()
: ReturnsTrue
if at least one of the arguments evaluates to True -
all()
: ReturnsTrue
if all of the arguments evaluate to True -
not any()
: ReturnsTrue
if none of the arguments evaluate to True -
not all()
: ReturnsTrue
if at least one of the arguments evaluates to False
Advanced Slice Operator
The slice operator is commonly used to access certain parts of a list or string.
string = "Python is awesome!"
string[3] # 'h'
string[4:7] # 'on '
It has many more advanced usages as well. For example, negative indexes can be used to slice from the end instead of the beginning.
string[-2] # 'e'
You can also specify a 'step' to skip over a certain number of elements while slicing.
string[2:9:2] # 'to s'
The step value can be negative. This causes the list/string to be sliced in reverse.
string[9:2:-2] # ' inh'
A common shortcut to reverse an array or list is to slice it with [::-1]
.
string[::-1] # '!emosewa si nohtyP'
Argument Unpacking
*
and **
are special operators that allow multiple items to be packed into (or unpacked from) a single variable.
You might have seen the words *args
and **kwargs
present in the definitions of functions. When present in the context of a function definition, the *
operator combines multiple arguments into a single tuple, while the **
operator combines multiple keyword arguments into a dictionary.
def product(*args):
res = 1
for arg in args:
res *= arg
return res
product(3, 4, 5)
def print_dict(**kwargs):
for key in kwargs:
print(f"{key}: {kwargs[key]}")
print_dict(firstname="Bikramjeet", lastname="Singh")
On the other had, when present in the context of a function call, they do the opposite - the *
operator spreads the contents of a list/tuple into individual arguments, while the **
operator spreads the contents of a dictionary into individual keyword arguments.
list_of_nums = [3, 4, 5]
product(*list_of_nums)
d = {"firstname": "Bikramjeet", "lastname": "Singh"}
print_dict(**d)
In functions that have a large number of parameters, it is often convenient to collect them into a dictionary before passing them to the function.
def my_function(arg1, arg2, arg3, arg4 ... ):
...
params = {'arg1': ...}
my_function(**params)
Another use of these operators is to combine lists and dictionaries.
combined_list = [*l1, *l2]
combined_dict = {**d1, **d2}
Functools
Python supports higher order functions - functions that can take and return other functions. The concept of higher order functions is central to several other Python features, such as decorators.
The functools package provides useful helper functions for when you're working with higher order functions. Let's take a look at some of them.
partial
There are cases where you might want to create a 'specialized' version of an existing, more generalized function. This is done by 'freezing' the values of some of the base functions parameters.
Consider a simple function that calculates the nth power of a number:
def pow(base, exp):
res = 1
for i in range(exp):
res *= base
return res
pow(2, 3) # returns 8
The operation of squaring a number is common enough that it is worth it to create a dedicated function for it, simply so we don't have to pass 2 as an argument each time. However, instead of rewriting our pow
function, we can simply reuse it with the functools.partial
method.
square = functools.partial(pow, exp=2)
square(4) # returns 16, equivalent to pow(4, 2)
Similarly, we can also create a cube function:
cube = functools.partial(pow, exp=3)
cube(5) # returns 125, equivalent to pow(5, 3)
cached_property
This is a decorator that allows you to cache the return values of potentially expensive methods. For example, database calls tend to be relatively long-running operations, so it's a good idea to cache them if you don't anticipate their values to change very often.
@functools.cached_property
def my_expensive_method():
...
return result
The method is evaluated in full the first time it is called, and its return value cached. The next time the same method is called, its result can simply be fetched from the cache.
Note that the cached_property
decorator is only available from Python 3.8 onwards. For lower versions of Python, there are separate packages available.
total_ordering
When you define a custom class, especially one that is a container for numeric data of some sort, it is useful to define comparison methods __eq__
(equals), __gt__
(greater than), __ge__
(greater than or equal to), __lt__
(less than) & __le__
(less than or equal to) to make comparing objects of those classes easier.
class Distance:
km: int # Kilometers
m: int # Meters
def __init__(self, km, m):
self.km = km
self.m = m
def __eq__(self, other):
return self.km == other.km and self.m == other.m
def __gt__(self, other):
if self.km > other.km:
return True
elif self.km < other.km:
return False
elif self.m > other.m:
return True
else:
return False
However, defining all of these methods can be cumbersome and result in a large amount of boilerplate code, especially since there are 5 of them. Luckily you don't have to! Given the logic of __eq__
and any one of __gt__
, __ge__
, __lt__
or __le__
, all the other comparison methods can be derived.
For example, in the example above, the __le__
method can be derived as
def __le__(self, other):
return not self > other
This is what the @total_ordering
decorator does. When applied onto a class that defines at least the __eq__
and one other comparison method, it derives all the others for you.
@functools.total_ordering
class Distance:
...
Dunder/Magic Methods
Dunder (double underscore) methods are special methods that allow you to make use of Python's built-in operators and keywords, such as +
, *
, len
, etc in user-defined classes. This is very useful in making code more concise and readable. You can read more about dunder methods in my previous article, here.
Image by Paul Brennan from Pixabay
Top comments (2)
And instead of
we just write
Good catch! Can't believe I missed that.