Hamed Zaghaghi
Posted on July 14, 2023
It all started when I was browsing Rust documentation and noticed that in Rust language struct methods are defined separately from the struct itself. To understand what I mean, see below example extracted from Rust documentation.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
What a nice feature! I told to myself. Then I started thinking about this in Python.
Can we do the same in Python?
The short answer is Yes, although we need to write a little bit of code to make this style possible in Python. Here is a simple solution to it with the help of decorators and monkey-patching.
def impl(cls):
def wrapper(fn):
setattr(cls, fn.__name__, fn)
return fn
return wrapper
And here is a simple usage. dataclass
is used here to resemble Rust struct
as there is no struct
in Python.
from dataclasses import dataclass
@dataclass
class Rectangle:
width: int
height: int
@impl(Rectangle)
def area(self: Rectangle) -> int:
return self.height * self.width
if __name__ == "__main__":
rect = Rectangle(width=10, height=20)
print(rect.area())
The definition of area
method is now outside of the class definition.
Use cases
I think this style offers a few use cases that can be used in our future Python projects.
Separated Implementation Modules
In some cases, implementing a method requires a few other dependencies which may be large and slow to import. Using this style you can have a class definition and multiple implementation files. Developer only import what they want to use.
Feature Toggles
Using this style, we can have feature toggles to choose between different implementations. Take a look at the following example.
def impl(cls: T, condition: bool=True):
def wrapper(fn):
if condition:
setattr(cls, fn.__name__, fn)
return fn
return wrapper
Note that the above condition is executed when a method is being defined, and not when the method is called.
from dataclasses import dataclass
from enum import IntFlag, auto
@dataclass
class Rectangle:
width: int
height: int
class FeatureFlag(IntFlag):
FAST_AREA = auto()
@impl(FeatureFlag)
def read_from_config():
return FeatureFlag.FAST_AREA
The implementation file has two definition for area
method, and the selection between these two is controlled by feature flags.
FLAGS = FeatureFlag.read_from_config()
@impl(Rectangle, FeatureFlag.FAST_AREA in FLAGS)
def area(self: Rectangle) -> int:
return self.height * self.width
@impl(Rectangle, FeatureFlag.FAST_AREA not in FLAGS)
def area(self: Rectangle) -> int:
area_value = 0
for _ in range(self.width):
area_value += self.height
return area_value
if __name__ == "__main__":
rect1 = Rectangle(width=10, height=20)
print(rect1.area())
Issues
Doing this kind of hacking in Python breaks type checkers like mypy
.
I'd like to play with programming languages, and make programming more fun to work with.
It would be greatly appreciated if you could share your thoughts/ideas in the comments bellow.
Posted on July 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
December 8, 2023