-
-
Notifications
You must be signed in to change notification settings - Fork 525
/
iterator_class.py
160 lines (121 loc) · 5.44 KB
/
iterator_class.py
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
"""
Iterator classes implement the `__iter__` and `__next__` magic methods.
This module defines an employee iterator class that iterates through each
employee in a hierarchy one-by-one. This module also shows how a similar
approach can be achieved with a generator function.
"""
# Module-level constants
_ITERATION_MESSAGE = "Cyclic loop detected"
class Employee:
"""Generic employee class.
For this module, we're going to remove the inheritance hierarchy
in `abstract_class` and make all employees have a `direct_reports`
attribute.
Notice that if we continue adding employees in the `direct_reports`
attribute, those same employees have a `direct_reports` attribute
as well.
The tree-like structure of this class resembles the Composite design
pattern, and it can be found on Wikipedia:
https://en.wikipedia.org/wiki/Composite_pattern
Design patterns are battle-tested ways of structuring code to handle
common problems encountered while writing software in a team setting.
Here's a Wikipedia link for more design patterns:
https://en.wikipedia.org/wiki/Design_Patterns
"""
def __init__(self, name, title, direct_reports):
self.name = name
self.title = title
self.direct_reports = direct_reports
class IterationError(RuntimeError):
"""Any error that comes while iterating through objects.
Notice that this class inherits from `RuntimeError`. That way dependent
functions can handle this exception using either the package hierarchy
or the native hierarchy.
"""
class EmployeeIterator:
"""Employee iterator.
An iterator class is composed of three methods:
- A constructor which defines data structures
- An iterator returns the instance itself
- A retriever which gets the next element
We do this by providing what are called magic methods. Other people
call them d-under methods because they have double-underscores.
An iterator class resembles the Iterator design pattern, and it
can be found on Wikipedia:
https://en.wikipedia.org/wiki/Iterator_pattern
"""
def __init__(self, employee):
"""Constructor logic."""
self.employees_to_visit = [employee]
self.employees_visited = set()
def __iter__(self):
"""Iterator is self by convention."""
return self
def __next__(self):
"""Return the next employee available.
The logic may seem complex, but it's actually a common algorithm
used in traversing a relationship graph. It is called depth-first
search, and it can be found on Wikipedia:
https://en.wikipedia.org/wiki/Depth-first_search
"""
if not self.employees_to_visit:
raise StopIteration
employee = self.employees_to_visit.pop()
if employee.name in self.employees_visited:
raise IterationError(_ITERATION_MESSAGE)
self.employees_visited.add(employee.name)
for report in employee.direct_reports:
self.employees_to_visit.append(report)
return employee
def employee_generator(top_employee):
"""Employee generator.
It is essentially the same logic as above except constructed as a
generator function. Notice that the generator code is in a single
place, whereas the iterator code is in multiple places. Also notice
that we are using the `yield` keyword in the generator code.
It is a matter of preference and context that we choose one approach
over the other. If we want something simple, go with the generator.
Otherwise, go with the iterator to fulfill more demanding requirements.
In this case, examples of such requirements are tasks like encrypting
the employee's username, running statistics on iterated employees or
excluding the reports under a particular set of managers.
For more on the subject of using a function versus a class, check
out this post from Microsoft Developer Blogs:
https://devblogs.microsoft.com/python/idiomatic-python-functions-versus-classes/
"""
to_visit = [top_employee]
visited = set()
while len(to_visit) > 0:
employee = to_visit.pop()
if employee.name in visited:
raise IterationError(_ITERATION_MESSAGE)
visited.add(employee.name)
for report in employee.direct_reports:
to_visit.append(report)
yield employee
def main():
# Manager with two direct reports
manager = Employee("Max Doe", "Engineering Manager", [
Employee("John Doe", "Software Engineer", []),
Employee("Jane Doe", "Software Engineer", [])
])
# We should provide the same three employees in the same order regardless
# of whether we use the iterator class or the generator function
employees = [emp for emp in EmployeeIterator(manager)]
assert employees == [emp for emp in employee_generator(manager)]
assert len(employees) == 3
# Make sure that the employees are who we expect them to be
assert all(isinstance(emp, Employee) for emp in employees)
# This is not a good day for this company
hacker = Employee("Unknown", "Hacker", [])
hacker.direct_reports.append(hacker)
for iter_obj in (EmployeeIterator, employee_generator):
call_failed = False
try:
list(iter_obj(hacker))
except IterationError as e:
call_failed = True
assert str(e) == _ITERATION_MESSAGE
assert call_failed is True
if __name__ == "__main__":
main()