6 Nifty Python Language Features You Might Not Know About

bikramjeetsingh

Bikramjeet Singh

Posted on February 7, 2021

6 Nifty Python Language Features You Might Not Know About

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

... 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(): Returns True if at least one of the arguments evaluates to True
  • all(): Returns True if all of the arguments evaluate to True
  • not any(): Returns True if none of the arguments evaluate to True
  • not all(): Returns True 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 '
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

You can also specify a 'step' to skip over a certain number of elements while slicing.

string[2:9:2] # 'to s'
Enter fullscreen mode Exit fullscreen mode

The step value can be negative. This causes the list/string to be sliced in reverse.

string[9:2:-2] # ' inh'
Enter fullscreen mode Exit fullscreen mode

A common shortcut to reverse an array or list is to slice it with [::-1].

string[::-1] # '!emosewa si nohtyP'
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Another use of these operators is to combine lists and dictionaries.

combined_list = [*l1, *l2]
combined_dict = {**d1, **d2}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Similarly, we can also create a cube function:

cube = functools.partial(pow, exp=3)
cube(5) # returns 125, equivalent to pow(5, 3)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:
    ...
Enter fullscreen mode Exit fullscreen mode

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

๐Ÿ’– ๐Ÿ’ช ๐Ÿ™… ๐Ÿšฉ
bikramjeetsingh
Bikramjeet Singh

Posted on February 7, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

ยฉ TheLazy.dev

About