Skip to content

Commit d1a5673

Browse files
committed
Add example of using protocols
1 parent 73c78ad commit d1a5673

File tree

2 files changed

+188
-21
lines changed

2 files changed

+188
-21
lines changed

source-code/object-orientation/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ Some examples of object-oriented programming in Python.
1111
* `polynomials`: additional example of object-orientated code.
1212
* `abstract_classes.ipynb`: illustration of using abstract base
1313
classes.
14-
* `avoiding_class_hierarchies.ipynb`: illustration of duck typing
15-
and mix-in.
14+
* `avoiding_class_hierarchies.ipynb`: illustration of duck typing,
15+
protocols and mix-in.
1616
* `overloading.ipynb`: illustration of function overloading using
1717
single dispatch.
1818
* `inheritance.py`: illustration of inheritance of class attributes.

source-code/object-orientation/avoiding_class_hierarchies.ipynb

Lines changed: 186 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,10 @@
5151
"outputs": [],
5252
"source": [
5353
"class Duck:\n",
54-
" \n",
55-
" _sound: str = 'quack'\n",
5654
" species: str\n",
5755
" \n",
5856
" def make_sound(self):\n",
59-
" return self._sound\n",
57+
" return 'quack'\n",
6058
" \n",
6159
" def __init__(self, species):\n",
6260
" self.species = species"
@@ -69,12 +67,10 @@
6967
"outputs": [],
7068
"source": [
7169
"class Timer:\n",
72-
" \n",
73-
" _sound: str = 'beep'\n",
7470
" time: int\n",
7571
" \n",
7672
" def make_sound(self):\n",
77-
" return self._sound\n",
73+
" return 'ring'\n",
7874
" \n",
7975
" def __init__(self, time):\n",
8076
" self.time = time"
@@ -113,7 +109,7 @@
113109
"output_type": "stream",
114110
"text": [
115111
"<class '__main__.Duck'> says quack\n",
116-
"<class '__main__.Timer'> says beep\n"
112+
"<class '__main__.Timer'> says ring\n"
117113
]
118114
}
119115
],
@@ -129,6 +125,167 @@
129125
"Note that the sound faculty of these classes is not derived from a common ancestor class by inheritance."
130126
]
131127
},
128+
{
129+
"cell_type": "markdown",
130+
"metadata": {},
131+
"source": [
132+
"# Protocols"
133+
]
134+
},
135+
{
136+
"cell_type": "markdown",
137+
"metadata": {},
138+
"source": [
139+
"Duck typing can be formalized in Python 3.8+ using protocols. This will allow type checkers such as `mypy` to find potential mistakes. Consider the code as above, but now implemented using a `Protocol`.\n",
140+
"\n",
141+
"A protocol is defined as a class that inherits from `Protocol` that is defined in the `typing` module. It defines the method signatures that should be implemented by any class that supports the protocol. In the example below, any class that can make a sound should have a `make_sound` method that returns a `str`.\n",
142+
"\n",
143+
"The classes that implement the protocol `SoundMaker` *do not* inherit from that class, they simply implement the `make_sound` method as specified by the protocol, i.e., a method that takes no arguments besides the object, and that returns a `str`.\n",
144+
"\n",
145+
"The `make_sound` function takes an object of type `SoundMaker` as an argument. Although neither `Duck` nor `Timer` inherit from `SoundMaker`, the type checker will nevertheless be satisfied since both classes implement the protocol `SoundMaker` since they have a `make_sound` method implementation.\n",
146+
"\n",
147+
"Since the `Dog` class doesn't implement `make_sound`, a static type checker will now report an error when a `Dog` object is passed as an argument to the `make_sound` function."
148+
]
149+
},
150+
{
151+
"cell_type": "code",
152+
"execution_count": 6,
153+
"metadata": {},
154+
"outputs": [
155+
{
156+
"name": "stdout",
157+
"output_type": "stream",
158+
"text": [
159+
"Writing protocols_toremove.py\n"
160+
]
161+
}
162+
],
163+
"source": [
164+
"%%writefile protocols_toremove.py\n",
165+
"#!/usr/bin/env python\n",
166+
"\n",
167+
"from typing import Protocol\n",
168+
"\n",
169+
"\n",
170+
"class SoundMaker(Protocol):\n",
171+
" def make_sound(self) -> str: ...\n",
172+
"\n",
173+
"\n",
174+
"class Duck:\n",
175+
" species: str\n",
176+
"\n",
177+
" def __init__(self, species: str):\n",
178+
" self.species = species\n",
179+
"\n",
180+
" def make_sound(self) -> str:\n",
181+
" return 'quack'\n",
182+
"\n",
183+
"\n",
184+
"class Timer:\n",
185+
" time: int\n",
186+
"\n",
187+
" def __init__(self, time: int):\n",
188+
" self.time = time\n",
189+
"\n",
190+
" def make_sound(self) -> str:\n",
191+
" return 'ring'\n",
192+
"\n",
193+
"\n",
194+
"class Dog:\n",
195+
" name: str\n",
196+
"\n",
197+
" def __init__(self, name: str):\n",
198+
" self.name = name\n",
199+
"\n",
200+
"\n",
201+
"def make_sound(stuff: SoundMaker) -> None:\n",
202+
" print(stuff.make_sound())\n",
203+
"\n",
204+
"\n",
205+
"if __name__ == \"__main__\":\n",
206+
" duck = Duck('Mallard')\n",
207+
" timer = Timer(5)\n",
208+
" dog = Dog('Fido')\n",
209+
"\n",
210+
" make_sound(duck)\n",
211+
" make_sound(timer)\n",
212+
"\n",
213+
" things: list[SoundMaker] = [duck, timer]\n",
214+
" for thing in things:\n",
215+
" make_sound(thing)\n",
216+
"\n",
217+
" # Does't pass type check, will result in runtime error \n",
218+
" make_sound(dog)"
219+
]
220+
},
221+
{
222+
"cell_type": "markdown",
223+
"metadata": {},
224+
"source": [
225+
"Now we can run `mypy` to do type checking."
226+
]
227+
},
228+
{
229+
"cell_type": "code",
230+
"execution_count": 7,
231+
"metadata": {},
232+
"outputs": [
233+
{
234+
"name": "stdout",
235+
"output_type": "stream",
236+
"text": [
237+
"protocols_toremove.py:54: \u001b[1m\u001b[31merror:\u001b[m Argument 1 to \u001b[m\u001b[1m\"make_sound\"\u001b[m has incompatible type \u001b[m\u001b[1m\"Dog\"\u001b[m; expected \u001b[m\u001b[1m\"SoundMaker\"\u001b[m \u001b[m\u001b[33m[arg-type]\u001b[m\n",
238+
"\u001b[1m\u001b[31mFound 1 error in 1 file (checked 1 source file)\u001b[m\n"
239+
]
240+
}
241+
],
242+
"source": [
243+
"!mypy protocols_toremove.py"
244+
]
245+
},
246+
{
247+
"cell_type": "markdown",
248+
"metadata": {},
249+
"source": [
250+
"Indeed, running this script would result in a runtime error."
251+
]
252+
},
253+
{
254+
"cell_type": "code",
255+
"execution_count": 8,
256+
"metadata": {},
257+
"outputs": [
258+
{
259+
"name": "stdout",
260+
"output_type": "stream",
261+
"text": [
262+
"quack\n",
263+
"ring\n",
264+
"quack\n",
265+
"ring\n",
266+
"Traceback (most recent call last):\n",
267+
" File \"/home/gjb/Projects/Python-software-engineering/source-code/object-orientation/protocols_toremove.py\", line 54, in <module>\n",
268+
" make_sound(dog)\n",
269+
" File \"/home/gjb/Projects/Python-software-engineering/source-code/object-orientation/protocols_toremove.py\", line 38, in make_sound\n",
270+
" print(stuff.make_sound())\n",
271+
" ^^^^^^^^^^^^^^^^\n",
272+
"AttributeError: 'Dog' object has no attribute 'make_sound'\n"
273+
]
274+
}
275+
],
276+
"source": [
277+
"!python protocols_toremove.py"
278+
]
279+
},
280+
{
281+
"cell_type": "code",
282+
"execution_count": 9,
283+
"metadata": {},
284+
"outputs": [],
285+
"source": [
286+
"!rm protocols_toremove.py"
287+
]
288+
},
132289
{
133290
"cell_type": "markdown",
134291
"metadata": {},
@@ -145,7 +302,7 @@
145302
},
146303
{
147304
"cell_type": "code",
148-
"execution_count": 6,
305+
"execution_count": 10,
149306
"metadata": {},
150307
"outputs": [],
151308
"source": [
@@ -167,7 +324,7 @@
167324
},
168325
{
169326
"cell_type": "code",
170-
"execution_count": 7,
327+
"execution_count": 11,
171328
"metadata": {},
172329
"outputs": [],
173330
"source": [
@@ -182,7 +339,7 @@
182339
},
183340
{
184341
"cell_type": "code",
185-
"execution_count": 8,
342+
"execution_count": 12,
186343
"metadata": {},
187344
"outputs": [],
188345
"source": [
@@ -197,7 +354,7 @@
197354
},
198355
{
199356
"cell_type": "code",
200-
"execution_count": 9,
357+
"execution_count": 13,
201358
"metadata": {},
202359
"outputs": [],
203360
"source": [
@@ -211,7 +368,7 @@
211368
},
212369
{
213370
"cell_type": "code",
214-
"execution_count": 10,
371+
"execution_count": 14,
215372
"metadata": {},
216373
"outputs": [],
217374
"source": [
@@ -220,7 +377,7 @@
220377
},
221378
{
222379
"cell_type": "code",
223-
"execution_count": 11,
380+
"execution_count": 15,
224381
"metadata": {},
225382
"outputs": [
226383
{
@@ -246,17 +403,20 @@
246403
},
247404
{
248405
"cell_type": "code",
249-
"execution_count": 12,
250-
"metadata": {},
406+
"execution_count": 16,
407+
"metadata": {
408+
"tags": []
409+
},
251410
"outputs": [
252411
{
253412
"name": "stderr",
254413
"output_type": "stream",
255414
"text": [
256415
"Traceback (most recent call last):\n",
257-
" File \"<ipython-input-12-1c825f3b7991>\", line 3, in <module>\n",
416+
" File \"/tmp/ipykernel_3319/3334719555.py\", line 3, in <module>\n",
258417
" print(dog.make_sound())\n",
259-
" File \"<ipython-input-6-918cd6c589ba>\", line 7, in make_sound\n",
418+
" ^^^^^^^^^^^^^^^^\n",
419+
" File \"/tmp/ipykernel_3319/2135747159.py\", line 7, in make_sound\n",
260420
" raise ValueError(f'{type(self)} does not make sound')\n",
261421
"ValueError: <class '__main__.Dog'> does not make sound\n"
262422
]
@@ -269,11 +429,18 @@
269429
"except ValueError as error:\n",
270430
" print_exc()"
271431
]
432+
},
433+
{
434+
"cell_type": "markdown",
435+
"metadata": {},
436+
"source": [
437+
"However, note that this implies a class hierarchy."
438+
]
272439
}
273440
],
274441
"metadata": {
275442
"kernelspec": {
276-
"display_name": "Python 3",
443+
"display_name": "Python 3 (ipykernel)",
277444
"language": "python",
278445
"name": "python3"
279446
},
@@ -287,7 +454,7 @@
287454
"name": "python",
288455
"nbconvert_exporter": "python",
289456
"pygments_lexer": "ipython3",
290-
"version": "3.9.5"
457+
"version": "3.11.4"
291458
}
292459
},
293460
"nbformat": 4,

0 commit comments

Comments
 (0)