|
51 | 51 | "outputs": [],
|
52 | 52 | "source": [
|
53 | 53 | "class Duck:\n",
|
54 |
| - " \n", |
55 |
| - " _sound: str = 'quack'\n", |
56 | 54 | " species: str\n",
|
57 | 55 | " \n",
|
58 | 56 | " def make_sound(self):\n",
|
59 |
| - " return self._sound\n", |
| 57 | + " return 'quack'\n", |
60 | 58 | " \n",
|
61 | 59 | " def __init__(self, species):\n",
|
62 | 60 | " self.species = species"
|
|
69 | 67 | "outputs": [],
|
70 | 68 | "source": [
|
71 | 69 | "class Timer:\n",
|
72 |
| - " \n", |
73 |
| - " _sound: str = 'beep'\n", |
74 | 70 | " time: int\n",
|
75 | 71 | " \n",
|
76 | 72 | " def make_sound(self):\n",
|
77 |
| - " return self._sound\n", |
| 73 | + " return 'ring'\n", |
78 | 74 | " \n",
|
79 | 75 | " def __init__(self, time):\n",
|
80 | 76 | " self.time = time"
|
|
113 | 109 | "output_type": "stream",
|
114 | 110 | "text": [
|
115 | 111 | "<class '__main__.Duck'> says quack\n",
|
116 |
| - "<class '__main__.Timer'> says beep\n" |
| 112 | + "<class '__main__.Timer'> says ring\n" |
117 | 113 | ]
|
118 | 114 | }
|
119 | 115 | ],
|
|
129 | 125 | "Note that the sound faculty of these classes is not derived from a common ancestor class by inheritance."
|
130 | 126 | ]
|
131 | 127 | },
|
| 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 | + }, |
132 | 289 | {
|
133 | 290 | "cell_type": "markdown",
|
134 | 291 | "metadata": {},
|
|
145 | 302 | },
|
146 | 303 | {
|
147 | 304 | "cell_type": "code",
|
148 |
| - "execution_count": 6, |
| 305 | + "execution_count": 10, |
149 | 306 | "metadata": {},
|
150 | 307 | "outputs": [],
|
151 | 308 | "source": [
|
|
167 | 324 | },
|
168 | 325 | {
|
169 | 326 | "cell_type": "code",
|
170 |
| - "execution_count": 7, |
| 327 | + "execution_count": 11, |
171 | 328 | "metadata": {},
|
172 | 329 | "outputs": [],
|
173 | 330 | "source": [
|
|
182 | 339 | },
|
183 | 340 | {
|
184 | 341 | "cell_type": "code",
|
185 |
| - "execution_count": 8, |
| 342 | + "execution_count": 12, |
186 | 343 | "metadata": {},
|
187 | 344 | "outputs": [],
|
188 | 345 | "source": [
|
|
197 | 354 | },
|
198 | 355 | {
|
199 | 356 | "cell_type": "code",
|
200 |
| - "execution_count": 9, |
| 357 | + "execution_count": 13, |
201 | 358 | "metadata": {},
|
202 | 359 | "outputs": [],
|
203 | 360 | "source": [
|
|
211 | 368 | },
|
212 | 369 | {
|
213 | 370 | "cell_type": "code",
|
214 |
| - "execution_count": 10, |
| 371 | + "execution_count": 14, |
215 | 372 | "metadata": {},
|
216 | 373 | "outputs": [],
|
217 | 374 | "source": [
|
|
220 | 377 | },
|
221 | 378 | {
|
222 | 379 | "cell_type": "code",
|
223 |
| - "execution_count": 11, |
| 380 | + "execution_count": 15, |
224 | 381 | "metadata": {},
|
225 | 382 | "outputs": [
|
226 | 383 | {
|
|
246 | 403 | },
|
247 | 404 | {
|
248 | 405 | "cell_type": "code",
|
249 |
| - "execution_count": 12, |
250 |
| - "metadata": {}, |
| 406 | + "execution_count": 16, |
| 407 | + "metadata": { |
| 408 | + "tags": [] |
| 409 | + }, |
251 | 410 | "outputs": [
|
252 | 411 | {
|
253 | 412 | "name": "stderr",
|
254 | 413 | "output_type": "stream",
|
255 | 414 | "text": [
|
256 | 415 | "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", |
258 | 417 | " 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", |
260 | 420 | " raise ValueError(f'{type(self)} does not make sound')\n",
|
261 | 421 | "ValueError: <class '__main__.Dog'> does not make sound\n"
|
262 | 422 | ]
|
|
269 | 429 | "except ValueError as error:\n",
|
270 | 430 | " print_exc()"
|
271 | 431 | ]
|
| 432 | + }, |
| 433 | + { |
| 434 | + "cell_type": "markdown", |
| 435 | + "metadata": {}, |
| 436 | + "source": [ |
| 437 | + "However, note that this implies a class hierarchy." |
| 438 | + ] |
272 | 439 | }
|
273 | 440 | ],
|
274 | 441 | "metadata": {
|
275 | 442 | "kernelspec": {
|
276 |
| - "display_name": "Python 3", |
| 443 | + "display_name": "Python 3 (ipykernel)", |
277 | 444 | "language": "python",
|
278 | 445 | "name": "python3"
|
279 | 446 | },
|
|
287 | 454 | "name": "python",
|
288 | 455 | "nbconvert_exporter": "python",
|
289 | 456 | "pygments_lexer": "ipython3",
|
290 |
| - "version": "3.9.5" |
| 457 | + "version": "3.11.4" |
291 | 458 | }
|
292 | 459 | },
|
293 | 460 | "nbformat": 4,
|
|
0 commit comments