DEV Community

loading...

How to use single dispatch to compose your class

hanpari profile image Pavel Morava ・2 min read

Originally written as a comment under Factory Pattern & Abstraction in Python

This is not about avoiding patterns or dismissing SOLID principles as useless. This is about realizing how dangerous is to mindlessly copy-paste Java code in Python. I gave up on three books about design patterns in Python because all of them just poorly translated Java to Python. Horrible! Most of their construction didn't make any sense because Java, especially the old one, is a different language based on pure OOP.

For instance, a static method. In Java, this is a necessity, in Python one has modules and functions. By the way, this is exactly how C# translates F# code involving functions and modules. By static classes and static methods.

Allow me to offer you a simple example of code, which is almost completely divided into datatypes (represented by NamedTuple, or I could've used dataclasses) and functions. Note I have added Circle after class Shape was declared. I could do that much later, I just need to import function calculate_perimeter and register it to new Shape.

The code is short and not redundant, at least in my opinion. By the way, tested in Python 3.8.

EDIT:

I rewrote the code to achieve a new typesafe version. PyCharm and mypy are going to complain immediately if you try something funny with your types. Unfortunately, I had to introduce dummy type CustomShape to achieve that.

If you know have better idea, please let me know.


from functools import singledispatch, cached_property
from typing import NamedTuple
from unittest import TestCase


class CustomShape:
    pass


class Rectangle(NamedTuple, CustomShape):
    length: int
    width: int


class Triangle(NamedTuple, CustomShape):
    a: int
    b: int
    c: int


@singledispatch
def calculate_perimeter(shape: CustomShape):
    raise NotImplementedError()


@calculate_perimeter.register
def _(shape: Rectangle):
    return shape.length + shape.width


@calculate_perimeter.register
def _(shape: Triangle):
    return shape.a + shape.b + shape.c


class Shape:
    def __init__(self, shape: CustomShape):
        self.shape = shape

    @cached_property
    def perimeter(self):
        return calculate_perimeter(self.shape)


class Circle(NamedTuple, CustomShape):
    radius: int


@calculate_perimeter.register
def _(shape: Circle):
    return 2 * 3.14 * shape.radius


if __name__ == '__main__':
    test = TestCase()
    test.assertEqual(
        Shape(Triangle(1, 1, 1)).perimeter, 3
    )
    test.assertEqual(
        Shape(Rectangle(width=1, length=1)).perimeter, 2
    )
    test.assertAlmostEqual(
        Shape(Circle(1)).perimeter, 6.28
    )
    with test.assertRaises(NotImplementedError) as _:
        # PyCharm and mypy are going to complain in advance!
        pass
        Shape((1, 2, 3)).perimeter



If you don't mind to step out of Python, I would recommend reading articles from S. Wlaschin here. It can help you understand better the type system.

Discussion (0)

pic
Editor guide