Extend 3rd party classes. Rust's Implementations pattern in Python.
Tags: python, rust, pattern, opensource
Published: 2022-01-17 12:50:31

## Intro

There is a pattern called `Implementations` in the Rust language. Using it you create methods and interfaces for the structures.

```rust
struct Number {
    odd: bool,
    value: i32,
}

impl Number {
    fn is_strictly_positive(self) -> bool {
        self.value > 0
    }
}
```

I decided to make the same but for Python. Meet [Impler](https://github.com/roman-right/impler).

## The basic syntax

You can extend any class with your own methods or even interface (class) using the `@impl` decorator.

```python
from impler import impl


class Point:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y


point = Point(3, 4)


@impl(Point)
def distance_from_zero(self: Point):
    return (self.x ** 2 + self.y ** 2)**(1/2)


print(point.distance_from_zero()) # 5.0
```

The same way you can implement class or static methods

```python
@impl(Point)
@staticmethod
def distance(left, right):
    return ((left.x + right.x) ** 2 + (left.y + right.y) ** 2)**(1/2)

point1 = Point(1, 3)
point2 = Point(2, 1)

print(Point.distance(point1, point2)) # 5.0
```

You can implement the whole interface also.

Here is an example of the base interface

```python
from pathlib import Path


class BaseFileInterface:
    def dump(self, path: Path):
        ...

    @classmethod
    def parse(cls, path: Path):
        ...
```

This is how you can implement this interface for Pydantic `BaseModel` class:

```python
from impler import impl
from pydantic import BaseModel
from pathlib import Path


@impl(BaseModel, as_parent=True)
class ModelFileInterface(BaseFileInterface):
    def dump(self, path: Path):
        path.write_text(self.json())
        
    @classmethod
    def parse(cls, path: Path):
        return cls.parse_file(path)

```

If the `as_parent` parameter is `True` the implementation will be injected into the list of the target class parents.

Then you can check if the class or object implements the interface:

```python
print(issubclass(BaseModel, BaseFileInterfase))
# True
```

## Real world use-case

There is a csv file with data of some products:

```
+----+-----------+--------------+----------------+
|    | name      | created at   |   expire after |
|----+-----------+--------------+----------------|
|  0 | milk 3.5% | 2022-01-15   |              7 |
|  1 | sausage   | 2021-12-10   |             60 |
|  2 | yogurt    | 2021-12-01   |             20 |
+----+-----------+--------------+----------------+
```

I need to display if the product was expired already using pandas

For this, I need to convert dates to the number of passed days. It would be great if DataFrame would have this method. Let's add it there.

```python
from typing import Dict

import numpy as np
import pandas as pd
from impler import impl
from pandas import DataFrame


@impl(DataFrame)
def to_age(self: DataFrame, columns: Dict[str, str], unit: str = "Y"):
    for target, new in columns.items():
        self[new] = ((pd.to_datetime("now") - pd.to_datetime(
            self[target])) / np.timedelta64(1, unit)).astype(int)
    return self


df = pd.read_csv("products.csv").to_age({"created at": "age in days"}, "D")
df["expired"] = df["expire after"] < df["age in days"]
print(df)
```

result:

```
+----+-----------+--------------+----------------+---------------+-----------+
|    | name      | created at   |   expire after |   age in days | expired   |
|----+-----------+--------------+----------------+---------------+-----------|
|  0 | milk 3.5% | 2022-01-15   |              7 |             2 | False     |
|  1 | sausage   | 2021-12-10   |             60 |            38 | False     |
|  2 | yogurt    | 2021-12-01   |             20 |            47 | True      |
+----+-----------+--------------+----------------+---------------+-----------+
```

As you can see, the `to_age` method returns self. It means this can be used in pandas methods chain, like this:

```python
age_gt_10 = (pd.read_csv("products.csv").
    to_age({"created at": "age"}, "D").
    query("age > 10").shape[0]
)
print(age_gt_10)  # 2
```

## Conclusion

You probably noticed that the lib logo is a warning sign:

![Impler](https://raw.githubusercontent.com/roman-right/impler/main/assets/logo.svg)

I want to warn you here too. Please, be careful - this lib patches classes.

There are many scenarios where this tool could be used. I'd say it is good for extending 3rd party classes in research or experiment projects. It can be used in web services too but should be well tested.

## Links

GH Project: <https://github.com/roman-right/impler>
PyPi: <https://pypi.org/project/impler/>
API Doc: <https://github.com/roman-right/impler/blob/main/docs/api.md>