Design Patterns That Make Sense in Python: Simple Factory

Design Patterns That Make Sense in Python: Simple Factory

Featured on Hashnode

In the first post of this series, I'll talk about Design Patterns that make sense in Python. We'll see how to implement them and how they are used in the standard library and other third-party packages. We'll see what is and how we can use the Simple Factory Pattern. Not only that, by the end of this article you will be able to understand the problems it solves and why it makes sense in Python.

via GIPHY

Introduction

Design Patterns has been a popular subject since the Design Patterns: Elements of Reusable Object-Oriented Software (a.k.a GoF) book was released back in 1994. GoF’s goals were to show techniques, a.k.a patterns, to improve an object-oriented design. In total, the book demonstrated 23 patterns, classified in 3 groups:

  • Creational

  • Behavioral

  • Structural

Among the creational patterns, we have the Factory Method. According to the book, the goal of this pattern is to define an interface to create an object. The sub classes will then decide which class will be instantiated. There’s also another variation called Simple Factory. This pattern creates an instance of an object without exposing the details behind the construction. In this article, we’ll see how to do that in Python in an idiomatic way.

When this is useful? Can we just call the constructor directly?

This pattern is helpful when you need to perform an extra setup before calling a constructor. In the next section we’ll see several examples on how they are used in the Python standard library and also in third-party packages such as pandas.

Usage

In this part, we’ll see how this pattern is used in practice and how you can implement it yourself.

Python Standard Library

The datetime module is one of the most important ones in the standard library. It defines a few classes such as date, datetime, and timedelta. This module uses the simple factory pattern extensively. A real example is the date class. It has a method called fromtimestamp that creates date instances given a timestamp.

In [3]: from datetime import date

In [4]: date.fromtimestamp(time.time())
Out[4]: datetime.date(2020, 11, 10)

If we look at the implementation, we can see that it extracts the year, month and day from the time instance and the call the constructor (cls). This is the kind of setup that is abstracted away from the user.

    @classmethod
    def fromtimestamp(cls, t):
        "Construct a date from a POSIX timestamp (like time.time())."
        y, m, d, hh, mm, ss, weekday, jday, dst = _time.localtime(t)
        return cls(y, m, d)

Another great example is the fromisocalendar method, which performs an extensive setup. Instead of leaving it to the user, the class provides the functionality “for free” by hiding that from you.

# https://github.com/python/cpython/blob/c304c9a7efa8751b5bc7526fa95cd5f30aac2b92/Lib/datetime.py#L860-L893
...
    @classmethod
    def fromisocalendar(cls, year, week, day):
        """Construct a date from the ISO year, week number and weekday.
        This is the inverse of the date.isocalendar() function"""
        # Year is bounded this way because 9999-12-31 is (9999, 52, 5)
        if not MINYEAR <= year <= MAXYEAR:
            raise ValueError(f"Year is out of range: {year}")

        if not 0 < week < 53:
            out_of_range = True

            if week == 53:
                # ISO years have 53 weeks in them on years starting with a
                # Thursday and leap years starting on a Wednesday
                first_weekday = _ymd2ord(year, 1, 1) % 7
                if (first_weekday == 4 or (first_weekday == 3 and
                                           _is_leap(year))):
                    out_of_range = False

            if out_of_range:
                raise ValueError(f"Invalid week: {week}")

        if not 0 < day < 8:
            raise ValueError(f"Invalid weekday: {day} (range is [1, 7])")

        # Now compute the offset from (Y, 1, 1) in days:
        day_offset = (week - 1) * 7 + (day - 1)

        # Calculate the ordinal day for monday, week 1
        day_1 = _isoweek1monday(year)
        ord_day = day_1 + day_offset

        return cls(*_ord2ymd(ord_day))
....

Pandas

pandas is one of the most used Python packages thanks to the rise of Data Science and Machine Learning. Just like Python, pandas also makes use of factory methods. A classic example is the from_dict method that belongs to the DataFrame class.

        >>> data = {'row_1': [3, 2, 1, 0], 'row_2': ['a', 'b', 'c', 'd']}
        >>> pd.DataFrame.from_dict(data, orient='index')
               0  1  2  3
        row_1  3  2  1  0
        row_2  a  b  c  d

When we inspect the implementation we can also see a lot of setup and extra checks.

    @classmethod
    def from_dict(cls, data, orient="columns", dtype=None, columns=None) -> DataFrame:
        ...
        index = None
        orient = orient.lower()
        if orient == "index":
            if len(data) > 0:
                # TODO speed up Series case
                if isinstance(list(data.values())[0], (Series, dict)):
                    data = _from_nested_dict(data)
                else:
                    data, index = list(data.values()), list(data.keys())
        elif orient == "columns":
            if columns is not None:
                raise ValueError("cannot use columns parameter with orient='columns'")
        else:  # pragma: no cover
            raise ValueError("only recognize index or columns for orient")

        return cls(data, index=index, columns=columns, dtype=dtype)

How to Implement It

The most idiomatic way of implementing factory methods in Python is by decorating them as classmethod. In Python, regular methods are attached to an object instance. We can access the objects’ fields via the self argument. classmethod, on the other hand, are bound not to an instance but to a class. That means when we call MyClass.factory_method we are passing MyClass as the first argument, called cls. This property makes them an excellent alternative for factory methods since calling cls(args) inside a classmethod is the same as MyClass(args).

To design your own factory methods, it’s sufficient to decorate it as a classmethod and return a new instance built with the cls argument. For example, presume that we want to implement a Point class and we want it also be constructed from Polar coordinates. The extra setup to convert from Polar to Cartesian is kept inside the method. Not simply it’s more readable, but also simplifies the constructor.

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

    @classmethod
    def from_polar(cls, r: float, theta: float) -> "Point":
        """
        Converts a polar coordinate into cartesian point.

        >>> Point.from_polar(r=-2**0.5, theta=math.pi / 4)
        Point(x=-1.00, y=-1.00)
        """
        return cls(r * math.cos(theta), r * math.sin(theta))

    def __repr__(self):
        return f"{self.__class__.__name__}(x={self.x:.2f}, y={self.y:.2f})"

>>> Point.from_polar(r=-2**0.5, theta=math.pi / 4)
Point(x=-1.00, y=-1.00)

Conclusion

That's pretty much it! I hope you’ve learned something different and useful. Simple Factory methods are very cool and can abstract a lot of boilerplate. Not to mention that it makes your code clean and readable. In this post I showed how this pattern is used in the standard library and in other packages such as pandas.

Other posts you may like:

See you next time!