Using Python types for fun and profit: the Visitor Pattern

Introduction

The visitor pattern is a design pattern that allows for adding new operations to a collection of objects, without, and that is important, modifying the objects themselves.

It looks like this:

There are two main functionalities:

  1. The visit functionality as implemented in the Visitor interface. This ensures that Elements can be visited.
  2. The accept functionality as implemented in the Element interface. This ensures that when an Element is visited, an operation can be performed.

Implementation in Python

Since we will be using classes before they are defined, we need this at the head of our program:

from __future__ import annotations

The Visitor class

We will start by defining the Visitor class:

class Visitor:

    def visit_person(self, person: Person):
        pass

    def visit_organization(self, organization: Organization):
        pass

In our example, a visitor can only visit persons and organizations. Note that because of our preliminary import we are able to refer to the Person and Organization classes before we define them.

The Element class

In order for our persons and organizations to accept a visitor, they need an accept() method, provided by this class:

class Element:
    def accept(self, new_visitor: Visitor):
        pass

The Person class

Now we can see how this accept method actually works in the Person class:

class Person(Element):
    _name: str = None
    _email: str = None

    def __init__(self, name: str, email: str):
        self._name = name
        self._email = email

    def accept(self, new_visitor: Visitor):
        new_visitor.visit_person(self)

    @property
    def name(self) -> str:
        return self._name

    @property
    def email(self) -> str:
        return self._email

Some notes:

  • Person in this example just has a name and an email
  • The accept() method has a visitor as its parameter and a reference to the current instance which is of type Person. That is why we can pass self to visit_person. This is the actual visiting the visitor does.
  • Note the two @property decorators. A property is a kind of virtual attribute, each time we access person.name for example, the name() method will be called and its return value used. This is a very easy way to make read-only attributes. An excellent article on this subject you can find here.

The Organization class

The Organization class is unsurprisingly very similar to the Person class:

class Organization(Element):
    _name: str = None
    _address: str = None

    def __init__(self, name: str, address: str):
        self._name = name
        self._address = address

    def accept(self, new_visitor: Visitor):
        new_visitor.visit_organization(self)

    @property
    def name(self)->str:
        return self._name

    @property
    def address(self)->str:
        return self._address

Defining the visitor

Now we can define the visitor with the class EmailVisitor:

class EmailVisitor(Visitor):
    def visit_person(self, person: Person):
        print(f"Sending email to {person.name} at {person.email}")

    def visit_organization(self, organization: Organization):
        print(f"Sending mail to {organization.name} at {organization.address}")

This is an implementation of the Visitor class we defined in the beginning.

The functionality is rather limited, on purpose. All either function does is print out some message about an action. These message are of course different for different elements.

Testing time

Time to test our code:

if __name__ == "__main__":
    elements: list[Element] = [Person("Alice", "alice@example"),
                               Organization("Acme Inc.", "123 Main Street"),
                               Person("Bob", "bob@example.com")
                               ]
    visitor: Visitor = EmailVisitor()
    for e in elements:
        e.accept(visitor)

Line by line:

  • We initialize an array of objects from classes which derive from Element
  • Then we construct a Visitor, an EmailVisitor in this case.
  • Next we iterator over all the elements in the array and visit them one by one.

Conclusion

As you can see, the implementation of this pattern is both elegant and painless, as well as flexible. That is painless for new implementations, I realize that may not be the case for existing codebases.

An extension would be to make visiting multithreaded and therefore threadsafe, but that will be the subject for another post.

Leave a Reply

Your email address will not be published. Required fields are marked *