Extend 3rd party classes. Rust's Implementations pattern in Python.

romanright

Roman Right

Posted on January 17, 2022

Extend 3rd party classes. Rust's Implementations pattern in Python.

Intro

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

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

impl Number {
    fn is_strictly_positive(self) -> bool {
        self.value > 0
    }
}
Enter fullscreen mode Exit fullscreen mode

I decided to make the same but for Python. Meet Impler.

The basic syntax

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

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

The same way you can implement class or static methods

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

You can implement the whole interface also.

Here is an example of the base interface

from pathlib import Path


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

    @classmethod
    def parse(cls, path: Path):
        ...
Enter fullscreen mode Exit fullscreen mode

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

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)

Enter fullscreen mode Exit fullscreen mode

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:

print(issubclass(BaseModel, BaseFileInterfase))
# True
Enter fullscreen mode Exit fullscreen mode

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

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.

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

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

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

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

Conclusion

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

Impler

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

💖 💪 🙅 🚩
romanright
Roman Right

Posted on January 17, 2022

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

Sign up to receive the latest update from our blog.

Related