6 Nifty Python Language Features You Might Not Know About
Tags: python, codenewbie
Published: 2021-02-07 17:58:58

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.

```python
@dataclass()
class Article:
    title: str
    author: str
    published_on: datetime
    views: int
    likes: int
```

Optionally, you can also set default values for variables.

```python
@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](https://docs.python.org/3/library/dataclasses.html).

# 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 ...

```python
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:

```python
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:

```python
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:

```python
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()`: 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.

```python
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.

```python
string[-2] # 'e'
```

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

```python
string[2:9:2] # 'to s'
```

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

```python
string[9:2:-2] # ' inh'
```

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

```python
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.

```python
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.

```python
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.

```python
def my_function(arg1, arg2, arg3, arg4 ... ):
    ...

params = {'arg1': ...}
my_function(**params)
```

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

```python
combined_list = [*l1, *l2]
combined_dict = {**d1, **d2}
```

# Functools
Python supports [higher order functions](https://composingprograms.com/pages/16-higher-order-functions.html) - 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](https://docs.python.org/3/library/functools.html) 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:

```python
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.

```python
square = functools.partial(pow, exp=2)
square(4) # returns 16, equivalent to pow(4, 2)
```

Similarly, we can also create a cube function:

```python
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.

```python
@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](https://pypi.org/project/cached-property/) 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.

```python
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

```python
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.

```python
@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](https://dev.to/bikramjeetsingh/how-to-write-better-python-classes-using-magic-methods-4166).

Image by <a href="https://pixabay.com/users/paulbr75-2938186/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=1674179">Paul Brennan</a> from <a href="https://pixabay.com/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=1674179">Pixabay</a>