0%

Visitor Pattern in Python

Quoting wikipedia

In object-oriented programming and software engineering, the visitor design pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying the structures. It is one way to follow the open/closed principle.

As mentioned, the goal is to separate the algorithm from objects which it operates on.

As a result, it allow us to add new operations without modifying the objects.

In C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ivisitor.h
#ifndef INCLUDE_VISITOR_H
#define INCLUDE_VISITOR_H

struct Rider;
struct Skier;

struct IVisitor {
virtual ~IVisitor() = default;
virtual void visit(Rider *student) = 0;
virtual void visit(Skier *student) = 0;
};

#endif /* INCLUDE_VISITOR_H */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// student.h
#ifndef INCLUDE_STUDENT_H
#define INCLUDE_STUDENT_H

#include "ivisitor.h"

struct Student {
virtual ~Student() = default;
virtual void accept(IVisitor *visitor) = 0;
};

struct Skier : public Student {
void accept(IVisitor *visitor) override { visitor->visit(this); } // Second dispatch
};

struct Rider : public Student {
void accept(IVisitor *visitor) override { visitor->visit(this); } // Second dispatch
};

#endif /* INCLUDE_STUDENT_H */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// main.cc
#include "student.h"
#include "student_visitor.h"
#include <iostream>
#include <memory>
#include <vector>

using namespace std;

struct RentalVisitor : IVisitor {
void visit(Rider *rider) { std::cout << "Prepare snowboard..." << std::endl; }
void visit(Skier *skier) { std::cout << "Prepare ski..." << std::endl; };
};

struct TeachVisitor : IVisitor {
void visit(Rider *rider) { std::cout << "Teach a rider" << std::endl; }
void visit(Skier *skier) { std::cout << "Teach a skier" << std::endl; };
};

// Add new visitor to perform new functionality

int main(int, char **) {
vector<unique_ptr<Student>> students;
students.emplace_back(make_unique<Skier>());
students.emplace_back(make_unique<Rider>());

RentalVisitor rental_visitor;
TeachVisitor teach_visitor;
for (auto &s : students) {
s->accept(&rental_visitor); // First dispatch
s->accept(&teach_visitor); // First dispatch
}
}

In Python

Traditional double dispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
'''
Implement visitor pattern via double dispatch.
'''
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import List


class IVisitor(ABC):
'''
Visitor interface
'''
@abstractmethod
def visit_skier(self, student):
...

@abstractmethod
def visit_rider(self, student):
...


class Student(ABC):
@abstractmethod
def accept(self, visitor):
...


class Skier(Student):
def accept(self, visitor):
visitor.visit_skier(self)


class Rider(Student):
def accept(self, visitor):
visitor.visit_rider(self)


#
# Concrate visitors
#
class RentalVisitor(IVisitor):
def visit_skier(self, student):
print('Prepare ski...')

def visit_rider(self, student):
print('Prepare snowboard...')


class TeachVisitor(IVisitor):
def visit_skier(self, student):
print('Teach skier...')

def visit_rider(self, student):
print('Teach rider...')

# Add new visitor to perform new functionality

def main():
rental_visitor = RentalVisitor()
teach_visitor = TeachVisitor()

students: List[Student] = [Skier(), Skier(), Rider()]
for s in students:
s.accept(rental_visitor)
s.accept(teach_visitor)


if __name__ == "__main__":
main()

Annotation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import List


class Student(ABC):
...


class Skier(Student):
...


class Rider(Student):
...


# A couple helper functions first

def _qualname(obj):
'''Get the fully-qualified name of an object (including module).'''
return obj.__module__ + '.' + obj.__qualname__


def _declaring_class(obj):
'''Get the name of the class that declared an object.'''
name = _qualname(obj)
return name[:name.rfind('.')]


# Stores the actual visitor methods
_methods = {}


def _visitor_impl(self, arg):
'''Actual visitor method implementation.'''
method = _methods[(_qualname(type(self)), type(arg))]
return method(self, arg)


def visitor(arg_type):
'''Decorator that creates a visitor method.'''

def decorator(fn):
declaring_class = _declaring_class(fn)
_methods[(declaring_class, arg_type)] = fn

# Replace all decorated methods with _visitor_impl
return _visitor_impl

return decorator


class RentalVisitor:
@visitor(Skier)
def visit(self, student):
print('Prepare ski...')

@visitor(Rider)
def visit(self, student):
print('Prepare snowboard...')


# Add new visitor to perform new functionality

def main():
rental_visitor = RentalVisitor()

students: List[Student] = [Skier(), Skier(), Rider()]
for s in students:
rental_visitor.visit(s)

print(_methods)


if __name__ == '__main__':
main()