Extend 3rd party classes. Rust's Implementations pattern in Python.
Roman Right
Posted on January 17, 2022
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
}
}
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
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
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):
...
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)
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
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.
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:
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:
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
Posted on January 17, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.