From 1396259a6c3fd3d108454c354bce6d65cf9375ab Mon Sep 17 00:00:00 2001 From: Geert Jan Bex Date: Sat, 29 May 2021 17:02:41 +0200 Subject: [PATCH 01/10] Move finite state parser to design pattern directory --- source-code/design-patters/README.md | 2 ++ .../finite-state-parser/.gitignore | 0 .../finite-state-parser/README.md | 0 .../finite-state-parser/bad_data.txt | 0 .../finite-state-parser/basic_fs_parser.py | 0 .../finite-state-parser/block.py | 0 .../finite-state-parser/block_generator.py | 0 .../finite-state-parser/data_non_typed.txt | 0 .../finite-state-parser/data_typed.txt | 0 .../finite-state-parser/fs_parser.py | 0 .../finite-state-parser/non_typed_fs_parser.py | 0 .../finite-state-parser/oo_fs_parser.py | 0 .../finite-state-parser/parser.py | 0 .../finite-state-parser/parser_errors.py | 0 .../finite-state-parser/pyparsing_block_parser.py | 0 .../finite-state-parser/pyparsing_block_parser_script.py | 0 .../finite-state-parser/simple_block.py | 0 .../finite-state-parser/simple_parser.py | 0 .../finite-state-parser/struct_fs_parser.py | 0 source-code/object-orientation/README.md | 2 -- 20 files changed, 2 insertions(+), 2 deletions(-) rename source-code/{object-orientation => design-patters}/finite-state-parser/.gitignore (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/README.md (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/bad_data.txt (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/basic_fs_parser.py (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/block.py (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/block_generator.py (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/data_non_typed.txt (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/data_typed.txt (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/fs_parser.py (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/non_typed_fs_parser.py (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/oo_fs_parser.py (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/parser.py (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/parser_errors.py (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/pyparsing_block_parser.py (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/pyparsing_block_parser_script.py (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/simple_block.py (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/simple_parser.py (100%) rename source-code/{object-orientation => design-patters}/finite-state-parser/struct_fs_parser.py (100%) diff --git a/source-code/design-patters/README.md b/source-code/design-patters/README.md index 8d6ed7b..810289f 100644 --- a/source-code/design-patters/README.md +++ b/source-code/design-patters/README.md @@ -8,3 +8,5 @@ Code to illustrate some design patterns in Python. can be used to change the behaviour of objects. 1. `factory_design_pattern.ipynb`: notebook illustrating how a factory class can be used to conveniently construct many objects with the same properties. + * `finite-state-parser`: illustration of object-oriented data + representation and the state pattern. diff --git a/source-code/object-orientation/finite-state-parser/.gitignore b/source-code/design-patters/finite-state-parser/.gitignore similarity index 100% rename from source-code/object-orientation/finite-state-parser/.gitignore rename to source-code/design-patters/finite-state-parser/.gitignore diff --git a/source-code/object-orientation/finite-state-parser/README.md b/source-code/design-patters/finite-state-parser/README.md similarity index 100% rename from source-code/object-orientation/finite-state-parser/README.md rename to source-code/design-patters/finite-state-parser/README.md diff --git a/source-code/object-orientation/finite-state-parser/bad_data.txt b/source-code/design-patters/finite-state-parser/bad_data.txt similarity index 100% rename from source-code/object-orientation/finite-state-parser/bad_data.txt rename to source-code/design-patters/finite-state-parser/bad_data.txt diff --git a/source-code/object-orientation/finite-state-parser/basic_fs_parser.py b/source-code/design-patters/finite-state-parser/basic_fs_parser.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/basic_fs_parser.py rename to source-code/design-patters/finite-state-parser/basic_fs_parser.py diff --git a/source-code/object-orientation/finite-state-parser/block.py b/source-code/design-patters/finite-state-parser/block.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/block.py rename to source-code/design-patters/finite-state-parser/block.py diff --git a/source-code/object-orientation/finite-state-parser/block_generator.py b/source-code/design-patters/finite-state-parser/block_generator.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/block_generator.py rename to source-code/design-patters/finite-state-parser/block_generator.py diff --git a/source-code/object-orientation/finite-state-parser/data_non_typed.txt b/source-code/design-patters/finite-state-parser/data_non_typed.txt similarity index 100% rename from source-code/object-orientation/finite-state-parser/data_non_typed.txt rename to source-code/design-patters/finite-state-parser/data_non_typed.txt diff --git a/source-code/object-orientation/finite-state-parser/data_typed.txt b/source-code/design-patters/finite-state-parser/data_typed.txt similarity index 100% rename from source-code/object-orientation/finite-state-parser/data_typed.txt rename to source-code/design-patters/finite-state-parser/data_typed.txt diff --git a/source-code/object-orientation/finite-state-parser/fs_parser.py b/source-code/design-patters/finite-state-parser/fs_parser.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/fs_parser.py rename to source-code/design-patters/finite-state-parser/fs_parser.py diff --git a/source-code/object-orientation/finite-state-parser/non_typed_fs_parser.py b/source-code/design-patters/finite-state-parser/non_typed_fs_parser.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/non_typed_fs_parser.py rename to source-code/design-patters/finite-state-parser/non_typed_fs_parser.py diff --git a/source-code/object-orientation/finite-state-parser/oo_fs_parser.py b/source-code/design-patters/finite-state-parser/oo_fs_parser.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/oo_fs_parser.py rename to source-code/design-patters/finite-state-parser/oo_fs_parser.py diff --git a/source-code/object-orientation/finite-state-parser/parser.py b/source-code/design-patters/finite-state-parser/parser.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/parser.py rename to source-code/design-patters/finite-state-parser/parser.py diff --git a/source-code/object-orientation/finite-state-parser/parser_errors.py b/source-code/design-patters/finite-state-parser/parser_errors.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/parser_errors.py rename to source-code/design-patters/finite-state-parser/parser_errors.py diff --git a/source-code/object-orientation/finite-state-parser/pyparsing_block_parser.py b/source-code/design-patters/finite-state-parser/pyparsing_block_parser.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/pyparsing_block_parser.py rename to source-code/design-patters/finite-state-parser/pyparsing_block_parser.py diff --git a/source-code/object-orientation/finite-state-parser/pyparsing_block_parser_script.py b/source-code/design-patters/finite-state-parser/pyparsing_block_parser_script.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/pyparsing_block_parser_script.py rename to source-code/design-patters/finite-state-parser/pyparsing_block_parser_script.py diff --git a/source-code/object-orientation/finite-state-parser/simple_block.py b/source-code/design-patters/finite-state-parser/simple_block.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/simple_block.py rename to source-code/design-patters/finite-state-parser/simple_block.py diff --git a/source-code/object-orientation/finite-state-parser/simple_parser.py b/source-code/design-patters/finite-state-parser/simple_parser.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/simple_parser.py rename to source-code/design-patters/finite-state-parser/simple_parser.py diff --git a/source-code/object-orientation/finite-state-parser/struct_fs_parser.py b/source-code/design-patters/finite-state-parser/struct_fs_parser.py similarity index 100% rename from source-code/object-orientation/finite-state-parser/struct_fs_parser.py rename to source-code/design-patters/finite-state-parser/struct_fs_parser.py diff --git a/source-code/object-orientation/README.md b/source-code/object-orientation/README.md index 0f89a86..5bbd3ec 100644 --- a/source-code/object-orientation/README.md +++ b/source-code/object-orientation/README.md @@ -19,6 +19,4 @@ Some examples of object-oriented programming in Python. * `multiple_inheritance.ipynb`: notebook illustrating some aspects of multiple inheritance in Python. * `mix-ins`: illustration of mix-ins. - * `finite-state-parser`: illustration of object-oriented data - representation. * `attr_intro.ipynb`: illustration of how to use the `attrs` package. From df468abc12711e8c98652562e930104f4581b561 Mon Sep 17 00:00:00 2001 From: Geert Jan Bex Date: Fri, 4 Jun 2021 18:51:53 +0200 Subject: [PATCH 02/10] Add truly functional implementation and operating overloading --- source-code/oo_vs_functional.ipynb | 487 +++++++++++++++++++++++++---- 1 file changed, 420 insertions(+), 67 deletions(-) diff --git a/source-code/oo_vs_functional.ipynb b/source-code/oo_vs_functional.ipynb index 3b3db32..ecf7e17 100644 --- a/source-code/oo_vs_functional.ipynb +++ b/source-code/oo_vs_functional.ipynb @@ -16,12 +16,13 @@ "outputs": [], "source": [ "from collections import Counter\n", - "from functools import wraps\n", + "from functools import wraps, reduce\n", "import heapq\n", "import math\n", "from math import exp, floor, log\n", "import matplotlib.pyplot as plt\n", "%matplotlib inline\n", + "import operator\n", "import random" ] }, @@ -156,7 +157,22 @@ " float\n", " current standard deviation, None if less than two data values'''\n", " if self.n > 1:\n", - " return math.sqrt((self._sum2 - self._sum**2/self.n)/(self.n - 1))" + " return math.sqrt((self._sum2 - self._sum**2/self.n)/(self.n - 1))\n", + " \n", + " def __repr__(self):\n", + " '''return a texutal representation of the object\n", + " \n", + " Returns\n", + " -------\n", + " str\n", + " string representation of the object\n", + " '''\n", + " if self.n == 0:\n", + " return f'mean = N/A, stddev = N/A, n = {self.n}'\n", + " elif self.n == 1:\n", + " return f'mean = {self.mean}, stddev = N/A, n = {self.n}'\n", + " else:\n", + " return f'mean = {self.mean}, stddev = {self.stddev}, n = {self.n}'" ] }, { @@ -178,11 +194,11 @@ "output_type": "stream", "text": [ "0 None None\n", - "1 1.0 None\n", - "2 1.5 0.7071067811865476\n", - "3 2.0 1.0\n", - "4 2.5 1.2909944487358056\n", - "5 3.0 1.5811388300841898\n" + "mean = 1.0, stddev = N/A, n = 1\n", + "mean = 1.5, stddev = 0.7071067811865476, n = 2\n", + "mean = 2.0, stddev = 1.0, n = 3\n", + "mean = 2.5, stddev = 1.2909944487358056, n = 4\n", + "mean = 3.0, stddev = 1.5811388300841898, n = 5\n" ] } ], @@ -191,7 +207,7 @@ "print(stats.n, stats.mean, stats.stddev)\n", "for value in range(1, 6):\n", " stats.add(value)\n", - " print(stats.n, stats.mean, stats.stddev)" + " print(stats)" ] }, { @@ -212,20 +228,20 @@ "name": "stdout", "output_type": "stream", "text": [ - "odd stats -> 1 1.0 None\n", - "even stats -> 0 None None\n", - "odd stats -> 1 1.0 None\n", - "even stats -> 1 2.0 None\n", - "odd stats -> 2 2.0 1.4142135623730951\n", - "even stats -> 1 2.0 None\n", - "odd stats -> 2 2.0 1.4142135623730951\n", - "even stats -> 2 3.0 1.4142135623730951\n", - "odd stats -> 3 3.0 2.0\n", - "even stats -> 2 3.0 1.4142135623730951\n", - "odd stats -> 3 3.0 2.0\n", - "even stats -> 3 4.0 2.0\n", - "odd stats -> 4 4.0 2.581988897471611\n", - "even stats -> 3 4.0 2.0\n" + "odd stats -> mean = 1.0, stddev = N/A, n = 1\n", + "even stats -> mean = N/A, stddev = N/A, n = 0\n", + "odd stats -> mean = 1.0, stddev = N/A, n = 1\n", + "even stats -> mean = 2.0, stddev = N/A, n = 1\n", + "odd stats -> mean = 2.0, stddev = 1.4142135623730951, n = 2\n", + "even stats -> mean = 2.0, stddev = N/A, n = 1\n", + "odd stats -> mean = 2.0, stddev = 1.4142135623730951, n = 2\n", + "even stats -> mean = 3.0, stddev = 1.4142135623730951, n = 2\n", + "odd stats -> mean = 3.0, stddev = 2.0, n = 3\n", + "even stats -> mean = 3.0, stddev = 1.4142135623730951, n = 2\n", + "odd stats -> mean = 3.0, stddev = 2.0, n = 3\n", + "even stats -> mean = 4.0, stddev = 2.0, n = 3\n", + "odd stats -> mean = 4.0, stddev = 2.581988897471611, n = 4\n", + "even stats -> mean = 4.0, stddev = 2.0, n = 3\n" ] } ], @@ -236,8 +252,8 @@ " even_stats.add(value)\n", " else:\n", " odd_stats.add(value)\n", - " print('odd stats -> ', odd_stats.n, odd_stats.mean, odd_stats.stddev)\n", - " print('even stats -> ', even_stats.n, even_stats.mean, even_stats.stddev)" + " print('odd stats -> ', odd_stats)\n", + " print('even stats -> ', even_stats)" ] }, { @@ -672,6 +688,267 @@ " print('odd stats -> ', odd_stats(value))" ] }, + { + "cell_type": "markdown", + "id": "0383b8ab-1820-4b47-a40b-3a26a42dbd67", + "metadata": {}, + "source": [ + "But is this really a functional approach? Strictly speaking, no. Functional programming is supposed to be side-effect free, but our function in fact has state that evolves over time.\n", + "\n", + "The following class forms the basis for a real functional implementation." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "5e97514c-d7a6-4b24-aec0-b78d261ad09b", + "metadata": {}, + "outputs": [], + "source": [ + "class FunctionalStats:\n", + " \n", + " def __init__(self, sum_value: float=0.0, sum2: float=0.0, n: int=0):\n", + " '''constructor for Stats objects\n", + " \n", + " Params\n", + " ------\n", + " Either none, which initializes an object that has seen no data yet, or\n", + " \n", + " sum_value: float\n", + " the sum of the values up to this point\n", + " sum2: float\n", + " the sum of the squares of the values up to this point\n", + " n: int\n", + " the number of values up to this point\n", + " '''\n", + " self._sum = sum_value\n", + " self._sum2 = sum2\n", + " self._n = n\n", + " \n", + " @property\n", + " def n(self):\n", + " '''Number of values seen by this object\n", + " \n", + " Returns\n", + " -------\n", + " int\n", + " number of data values seen so far\n", + " '''\n", + " return self._n\n", + " \n", + " @property\n", + " def mean(self):\n", + " '''Mean value of the data values seen so far\n", + " \n", + " Returns\n", + " -------\n", + " float\n", + " mean value\n", + " \n", + " Raises\n", + " ------\n", + " ValueError\n", + " Exception when the number of values is less than 1\n", + " '''\n", + " if self._n < 1:\n", + " raise ValueError('at least one data value required for mean')\n", + " return self._sum/self._n\n", + " \n", + " @property\n", + " def stddev(self):\n", + " '''Standard deviation of the data values seen so far\n", + " \n", + " Returns\n", + " -------\n", + " float\n", + " standard deviation\n", + "\n", + " Raises\n", + " ------\n", + " ValueError\n", + " Exception when the number of values is less than 1\n", + " \n", + " '''\n", + " if self._n < 2:\n", + " raise ValueError('at least two data values required for standard deviation')\n", + " return math.sqrt((self._sum2 - self._sum**2/self._n)/(self._n - 1))\n", + " \n", + " def __add__(self, value: float):\n", + " '''functional operator, creates a new Stats object when a value is added\n", + " called as stats + value\n", + " \n", + " Params\n", + " ------\n", + " value: float\n", + " new data value to be added to the statistics\n", + " \n", + " Returns\n", + " -------\n", + " Stats\n", + " a new Stats object that includes the value just added\n", + " '''\n", + " return FunctionalStats(sum_value=self._sum + value, sum2=self._sum2 + value**2, n=self._n + 1)\n", + " \n", + " def __iadd__(self, value: float):\n", + " '''non-functional operator, updates the Stats object when a value is added\n", + " \n", + " Params\n", + " ------\n", + " value: float\n", + " new data value to be added to the statistics\n", + " \n", + " Returns\n", + " -------\n", + " Stats\n", + " updated Stats object including the value just added, called as\n", + " stats += value\n", + " '''\n", + " self._sum += value\n", + " self._sum2 += value**2\n", + " self._n += 1\n", + " return self\n", + " \n", + " def __repr__(self):\n", + " '''return a texutal representation of the object\n", + " \n", + " Returns\n", + " -------\n", + " str\n", + " string representation of the object\n", + " '''\n", + " if self.n == 0:\n", + " return f'mean = N/A, stddev = N/A, n = {self.n}'\n", + " elif self.n == 1:\n", + " return f'mean = {self.mean}, stddev = N/A, n = {self.n}'\n", + " else:\n", + " return f'mean = {self.mean}, stddev = {self.stddev}, n = {self.n}'" + ] + }, + { + "cell_type": "markdown", + "id": "ca890ab0-ac7c-4bac-ad88-5bf35b3de271", + "metadata": {}, + "source": [ + "To illustrate that using the `+` operator is truly side-effect free, we can sotre all the intermediate objects n a list, and print them when done." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "6ec62835-ec1b-42e7-980b-dabc1efb16a4", + "metadata": {}, + "outputs": [], + "source": [ + "values = [3.1, 5.2, 7.3]" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "54875dc7-5af3-4346-89da-ccd797ca1b44", + "metadata": {}, + "outputs": [], + "source": [ + "all_stats = [FunctionalStats()]\n", + "for value in values:\n", + " all_stats.append(all_stats[-1] + value)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "e8adc830-7e6f-4521-af07-776e7a03f6c3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mean = N/A, stddev = N/A, n = 0\n", + "mean = 3.1, stddev = N/A, n = 1\n", + "mean = 4.15, stddev = 1.4849242404917493, n = 2\n", + "mean = 5.2, stddev = 2.0999999999999974, n = 3\n" + ] + } + ], + "source": [ + "for stats in all_stats:\n", + " print(stats)" + ] + }, + { + "cell_type": "markdown", + "id": "4aff29b4-fdbe-409d-b80e-f113caff6d45", + "metadata": {}, + "source": [ + "On the other hand, using the `+=` operator which is non-functional shows that indeed the object is updated." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "e5c939ec-5afa-47c4-bb6d-bd3fc218a061", + "metadata": {}, + "outputs": [], + "source": [ + "all_stats = [FunctionalStats()]\n", + "for value in values:\n", + " stats = all_stats[-1]\n", + " stats += value\n", + " all_stats.append(stats)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "ff7fa2db-e6a8-4fe0-b396-aa22e92dd721", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mean = 5.2, stddev = 2.0999999999999974, n = 3\n", + "mean = 5.2, stddev = 2.0999999999999974, n = 3\n", + "mean = 5.2, stddev = 2.0999999999999974, n = 3\n", + "mean = 5.2, stddev = 2.0999999999999974, n = 3\n" + ] + } + ], + "source": [ + "for stats in all_stats:\n", + " print(stats)" + ] + }, + { + "cell_type": "markdown", + "id": "18c8630e-9006-4c2b-aaa0-37468bfc57ae", + "metadata": {}, + "source": [ + "Note that is now trivial to use this `Stats` object in a reduction." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "dae60959-7900-49dd-81f3-e38f4f607a78", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "mean = 5.2, stddev = 2.0999999999999974, n = 3" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "reduce(operator.add, values, FunctionalStats())" + ] + }, { "cell_type": "markdown", "id": "chief-monkey", @@ -690,7 +967,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 21, "id": "packed-marijuana", "metadata": {}, "outputs": [], @@ -700,7 +977,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 22, "id": "ongoing-showcase", "metadata": {}, "outputs": [ @@ -708,7 +985,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "454 ms ± 1.62 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + "307 ms ± 2.92 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], @@ -721,7 +998,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 23, "id": "bridal-sender", "metadata": {}, "outputs": [ @@ -729,7 +1006,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "483 ms ± 12.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + "486 ms ± 6.41 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], @@ -744,7 +1021,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 24, "id": "fabulous-standard", "metadata": {}, "outputs": [ @@ -752,7 +1029,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "467 ms ± 3.95 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + "468 ms ± 6.78 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], @@ -764,29 +1041,105 @@ ] }, { - "cell_type": "markdown", - "id": "greatest-ambassador", + "cell_type": "code", + "execution_count": 25, + "id": "551b9dce-5f8b-41dc-9e1a-e4ac604d770a", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "587 ms ± 8.13 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], "source": [ - "## Conclusion" + "%%timeit\n", + "stats = FunctionalStats()\n", + "for value in values:\n", + " stats = stats + value" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "2511caf8-d7cb-4990-9cd9-03c360528644", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "591 ms ± 2.08 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%timeit reduce(operator.add, values, FunctionalStats())" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "c8edc1e2-cac5-4f15-a688-fd9cbff918f0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "319 ms ± 1.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit\n", + "stats = FunctionalStats()\n", + "for value in values:\n", + " stats += value" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "31ffb758-5e03-40d3-85e6-313e7ec8cc17", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "323 ms ± 984 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%timeit reduce(operator.iadd, values, FunctionalStats())" ] }, { "cell_type": "markdown", - "id": "normal-tucson", + "id": "greatest-ambassador", "metadata": {}, "source": [ - "It is immediately clear that hte funcitonal approach is ridiculously easy, once you wrap your head around it. Up to that time, the object oriented approach might be more intuitive.\n", - "\n", - "Coroutines seems the hardest to do, although they are intuitevely easy to understand, the mechanics to get them to work are somewhat cumbersome. However, they have nice applications when creating data pipelines." + "## Conclusion" ] }, { "cell_type": "markdown", - "id": "oriental-transportation", + "id": "182b3e62-9778-4efa-b870-a1f3a66196d8", "metadata": {}, "source": [ - "The object oriented approach has a better performance, but only by 5 %." + "It is immediately clear that the \"not really\" funcitonal approach is ridiculously easy, once you wrap your head around it. Up to that time, the object oriented approach might be more intuitive.\n", + "\n", + "The true functional approach is definitely the slowest verion by some margin, but it is of course the cleanest since it is side-effect free.\n", + "\n", + "Coroutines seems the hardest to do, although they are intuitevely easy to understand, the mechanics to get them to work are somewhat cumbersome. However, they have nice applications when creating data pipelines.\n", + "\n", + "The object oriented approach has a better performance, but only by 5 %.\n", + "\n", + "Overloading the `+=` operator gives the best performance by quite some margin." ] }, { @@ -831,7 +1184,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 29, "id": "annual-preference", "metadata": {}, "outputs": [ @@ -867,7 +1220,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 30, "id": "homeless-poland", "metadata": {}, "outputs": [], @@ -943,7 +1296,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 31, "id": "reverse-productivity", "metadata": {}, "outputs": [ @@ -951,16 +1304,16 @@ "name": "stdout", "output_type": "stream", "text": [ - "[3] -> 3\n", - "[3, 47] -> 25.0\n", - "[3, 30, 47] -> 30\n", - "[3, 30, 47, 48] -> 38.5\n", - "[2, 3, 30, 47, 48] -> 30\n", - "[2, 3, 9, 30, 47, 48] -> 19.5\n", - "[2, 3, 9, 30, 44, 47, 48] -> 30\n", - "[2, 3, 9, 30, 44, 47, 48, 49] -> 37.0\n", - "[2, 3, 7, 9, 30, 44, 47, 48, 49] -> 30\n", - "[2, 3, 7, 9, 18, 30, 44, 47, 48, 49] -> 24.0\n" + "[23] -> 23\n", + "[23, 29] -> 26.0\n", + "[23, 24, 29] -> 24\n", + "[23, 24, 29, 50] -> 26.5\n", + "[6, 23, 24, 29, 50] -> 24\n", + "[5, 6, 23, 24, 29, 50] -> 23.5\n", + "[5, 5, 6, 23, 24, 29, 50] -> 23\n", + "[5, 5, 6, 23, 24, 28, 29, 50] -> 23.5\n", + "[5, 5, 6, 23, 24, 28, 29, 42, 50] -> 24\n", + "[5, 5, 6, 23, 24, 28, 28, 29, 42, 50] -> 26.0\n" ] } ], @@ -1000,7 +1353,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 32, "id": "upper-diving", "metadata": {}, "outputs": [], @@ -1044,7 +1397,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 33, "id": "wrong-pavilion", "metadata": {}, "outputs": [], @@ -1071,7 +1424,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 34, "id": "approved-definition", "metadata": {}, "outputs": [ @@ -1081,7 +1434,7 @@ "252" ] }, - "execution_count": 23, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } @@ -1100,7 +1453,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 35, "id": "objective-scheduling", "metadata": {}, "outputs": [ @@ -1110,7 +1463,7 @@ "252" ] }, - "execution_count": 24, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } @@ -1121,7 +1474,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 36, "id": "convinced-visitor", "metadata": {}, "outputs": [ @@ -1131,7 +1484,7 @@ "252" ] }, - "execution_count": 25, + "execution_count": 36, "metadata": {}, "output_type": "execute_result" } @@ -1150,13 +1503,13 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 37, "id": "alike-killer", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -1173,17 +1526,17 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 38, "id": "posted-nebraska", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(331, 458)" + "(342, 458)" ] }, - "execution_count": 27, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } @@ -1217,7 +1570,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.2" + "version": "3.9.5" } }, "nbformat": 4, From dfdac1f1d87bffbc5ab7319ffd308890330926f6 Mon Sep 17 00:00:00 2001 From: Geert Jan Bex Date: Mon, 14 Jun 2021 16:40:15 +0200 Subject: [PATCH 03/10] Add illustration of metaclasses --- source-code/README.md | 1 + source-code/metaclasses/README.md | 15 ++ source-code/metaclasses/metaclasses.ipynb | 195 ++++++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 source-code/metaclasses/README.md create mode 100644 source-code/metaclasses/metaclasses.ipynb diff --git a/source-code/README.md b/source-code/README.md index eb6e98c..dfee04f 100644 --- a/source-code/README.md +++ b/source-code/README.md @@ -26,3 +26,4 @@ was used to develop it. `pytest`. 1. `oo_vs_functional.ipynb`: comparing object-oriented approach to functional approach, including coroutines. +1. `metaclasses`: illustration of the use of metaclasses in Python. diff --git a/source-code/metaclasses/README.md b/source-code/metaclasses/README.md new file mode 100644 index 0000000..9a9e57a --- /dev/null +++ b/source-code/metaclasses/README.md @@ -0,0 +1,15 @@ +# Metaclasses + +Python supports metaclasses, i.e., classes that control the creation of other +classes. + +Although this feature should not be overused, it can be useful in some +circumstances. + +See the following [article](https://medium.com/fintechexplained/advanced-python-metaprogramming-980da1be0c7d) for a discussion. + + +## What is it? + +1. `metaclasses.ipynb`: Jupyter notebook illustrating using a metaclass to + verify the correct implementation of a mix-in. diff --git a/source-code/metaclasses/metaclasses.ipynb b/source-code/metaclasses/metaclasses.ipynb new file mode 100644 index 0000000..e27077c --- /dev/null +++ b/source-code/metaclasses/metaclasses.ipynb @@ -0,0 +1,195 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "72694516-dbb4-47dc-bb49-cf0832d2eb2b", + "metadata": {}, + "source": [ + "Metaclasses can be quite useful in some circumstances. A metaclass serves as a constructor for classes and can be used for validation at defintion time.\n", + "\n", + "Consider the following metaclass: it will check whether the class has a `_sound` attribute, if not, it will fail upon defintion. It will also add a `make_sound` method, acting similar to a mix-in." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "a2c0fb42-c8b7-4e7c-9044-f0a543a11a6c", + "metadata": {}, + "outputs": [], + "source": [ + "class SoundMeta(type):\n", + " \n", + " def __new__(cls, what, bases=None, dict=None):\n", + " if '_sound' not in dict:\n", + " raise Exception('no _sound attribute defined')\n", + " new_dict = dict.copy()\n", + " new_dict['make_sound'] = lambda self: f'say {self._sound}'\n", + " return type.__new__(cls, what, bases, new_dict)" + ] + }, + { + "cell_type": "markdown", + "id": "356b35e2-b534-44d1-bea6-67675cc0bdbb", + "metadata": {}, + "source": [ + "Note that the `SoundMeta` class has `type` as a base class, to `SoundMeta` is a class. It defines its own `__new__` function to control object creation, which calls the parent's (i.e., `type`'s) `__new__` method with the modified dictionary." + ] + }, + { + "cell_type": "markdown", + "id": "64654746-fc9f-4448-b26d-1887b391fd71", + "metadata": {}, + "source": [ + "The `Dog` class has a `sound` attribute, and defines some attributes and methods of its own." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "73852058-22e2-4036-86ef-4e205a12bef7", + "metadata": {}, + "outputs": [], + "source": [ + "class Dog(metaclass=SoundMeta):\n", + " _sound: str = 'woof'\n", + " \n", + " def __init__(self, name):\n", + " self._name = name\n", + " \n", + " @property\n", + " def name(self):\n", + " return self.name" + ] + }, + { + "cell_type": "markdown", + "id": "696ff05b-273d-4dd3-ab2c-ac5a219cdee3", + "metadata": {}, + "source": [ + "We can instantiate a particular `Dog` and call its `make_sound()` method." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "678a6d7e-f165-4183-a183-001c0a333a54", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'say woof'" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dog = Dog('felix')\n", + "dog.make_sound()" + ] + }, + { + "cell_type": "markdown", + "id": "bfbfbd5f-b2d4-4aa2-acf4-b331ce86a768", + "metadata": {}, + "source": [ + "The `Bell` class also has `SoundMeta` as metaclass." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "862b88a9-c467-43d3-975f-70aac7cdc330", + "metadata": {}, + "outputs": [], + "source": [ + "class Bell(metaclass=SoundMeta):\n", + " _sound: str = 'dong'" + ] + }, + { + "cell_type": "markdown", + "id": "ab5b88a3-1374-4980-aa97-4a33dc867c75", + "metadata": {}, + "source": [ + "It too can be used to instantiate objects that make a sound." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "be700599-60bc-490a-8fbb-4bdae3cee06f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'say dong'" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "big_ben = Bell()\n", + "big_ben.make_sound()" + ] + }, + { + "cell_type": "markdown", + "id": "31cebd32-f4df-4a5b-9516-8b2a7a5ffaa3", + "metadata": {}, + "source": [ + "If you try to create a class that has no `_sound` attribute, the class definition will result in an error." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "0f537499-6a88-4ca6-a9d8-1fad1ef5272c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "no _sound attribute defined\n" + ] + } + ], + "source": [ + "try:\n", + " class Phone(metaclass=SoundMeta):\n", + " pass\n", + "except Exception as e:\n", + " print(e)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 88a077fdd4fe91c3a54d5cb58a382cfe6ccbcf56 Mon Sep 17 00:00:00 2001 From: Geert Jan Bex Date: Tue, 3 Aug 2021 17:20:20 +0200 Subject: [PATCH 04/10] Fix mistakes and typos --- README.md | 7 +++++++ python_software_engineering.pptx | Bin 551351 -> 551332 bytes 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index 21eb21f..742f65b 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,10 @@ For information on the training, see the website 1. [Contributing](CONTRIBUTING.md): information on how to contribute to this repository. 1. docs: directory containing the website for this repository. + +## Contributors + +* Geert Jan Bex ([geertjan.bex@uhasselt.be](mailto:geertjan.bex@uhasselt.be)), + Hasselt University/University of Leuven +* Mustafa Dikmen, University of Leuven + * correcting typos and pointing out various mistakes \ No newline at end of file diff --git a/python_software_engineering.pptx b/python_software_engineering.pptx index 556e20c7de70e3426ec03621746e0be6b63ff28a..0f6bcaf36a816a30f05c124ff48a801a9c60030d 100644 GIT binary patch delta 23809 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 delta 23545 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 d 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 Date: Wed, 25 Aug 2021 17:02:09 +0200 Subject: [PATCH 05/10] Add data structure section --- source-code/README.md | 2 + source-code/data-structures/README.md | 11 + source-code/data-structures/defaultdict.ipynb | 231 ++++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 source-code/data-structures/README.md create mode 100644 source-code/data-structures/defaultdict.ipynb diff --git a/source-code/README.md b/source-code/README.md index dfee04f..0991213 100644 --- a/source-code/README.md +++ b/source-code/README.md @@ -27,3 +27,5 @@ was used to develop it. 1. `oo_vs_functional.ipynb`: comparing object-oriented approach to functional approach, including coroutines. 1. `metaclasses`: illustration of the use of metaclasses in Python. +1. `data-structures`: illustration of Python and standard library data + structures. diff --git a/source-code/data-structures/README.md b/source-code/data-structures/README.md new file mode 100644 index 0000000..ba56bb2 --- /dev/null +++ b/source-code/data-structures/README.md @@ -0,0 +1,11 @@ +# Data structures + +Python as well as its standard library have many data structures that help you +write elegant and efficient code. Here you will soe examples. + + +## What is it? + +1. `defaultdict.ipynb`: Jupyter notebook that illustrates the use of + `collections.defaultdict` with a default factory for a Python `list` and + a `dataclasss`. diff --git a/source-code/data-structures/defaultdict.ipynb b/source-code/data-structures/defaultdict.ipynb new file mode 100644 index 0000000..0671f35 --- /dev/null +++ b/source-code/data-structures/defaultdict.ipynb @@ -0,0 +1,231 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2462a390-11a0-4d96-be42-a215460ee554", + "metadata": {}, + "source": [ + "# Requirements" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4012d44e-1e7f-488d-bb19-d40466a0b800", + "metadata": {}, + "outputs": [], + "source": [ + "from collections import defaultdict\n", + "from dataclasses import dataclass" + ] + }, + { + "cell_type": "markdown", + "id": "62c1100f-d14c-4773-a87d-9b7935d55dcd", + "metadata": {}, + "source": [ + "# defaultdict" + ] + }, + { + "cell_type": "markdown", + "id": "771384fa-293a-4f00-94f9-0c5d28f19ed5", + "metadata": {}, + "source": [ + "The `defaultdict` class is quite convenient since you don't have to test whether a key is already in a dictionary. If it is not, a factory is used to create an initial entry." + ] + }, + { + "cell_type": "markdown", + "id": "f3951a33-b671-4603-8349-61c1641ae405", + "metadata": {}, + "source": [ + "## Simple example" + ] + }, + { + "cell_type": "markdown", + "id": "a2630300-45fd-4d51-8a05-a05bad9ee9d6", + "metadata": {}, + "source": [ + "Consider a dictionary that has lists as values. If a key is not in the dictionary, it should be initialized with an empty list. The factory function for an empty list is the `list` function." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "67799112-3eb0-4410-b260-5c4646adffa6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "multiples = defaultdict(list)" + ] + }, + { + "cell_type": "markdown", + "id": "556ecf15-3003-49f8-aa9b-e9f3998f2855", + "metadata": {}, + "source": [ + "Now we can add data to the dictionary, for each integer between 0 and 20, we check whether the number is divisible by 2, 3, 4, or 5, and append it to the list that is the value for the divisor (which is the key)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "34147d51-a265-46b8-9e22-10b81460c006", + "metadata": {}, + "outputs": [], + "source": [ + "for number in range(20):\n", + " for divisor in range(2, 6):\n", + " if number % divisor == 0:\n", + " multiples[divisor].append(number)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d533abb9-7fcc-4ddf-895b-a13a71af138a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "defaultdict(list,\n", + " {2: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18],\n", + " 3: [0, 3, 6, 9, 12, 15, 18],\n", + " 4: [0, 4, 8, 12, 16],\n", + " 5: [0, 5, 10, 15]})" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "multiples" + ] + }, + { + "cell_type": "markdown", + "id": "4fb29e3d-4264-4030-b933-b101c115d923", + "metadata": {}, + "source": [ + "## Arbitrary classes" + ] + }, + { + "cell_type": "markdown", + "id": "667ee4ca-6b2f-4ec0-823e-beef80dbbe93", + "metadata": {}, + "source": [ + "`defaultdict` also supports arbitrary objects as default values. Consider the following dataclass that represents a person." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0d049fe8-5b12-4c35-a730-1ddb47db55e8", + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class Person:\n", + " first_name: str = None\n", + " last_name: str = None\n", + " age: int = None" + ] + }, + { + "cell_type": "markdown", + "id": "928778d2-6fd8-4662-bb31-857e3a87f2c5", + "metadata": {}, + "source": [ + "The constructor is the default factory in this case." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1eec5ce6-3d7e-4e64-abd1-86f5f92e1446", + "metadata": {}, + "outputs": [], + "source": [ + "people = defaultdict(Person)" + ] + }, + { + "cell_type": "markdown", + "id": "aa0d20b2-ef70-487f-8637-cc6060355c1f", + "metadata": {}, + "source": [ + "When a new person turns up, a default `Person` is constructed with `None` for each attribute, and one of the attributes, `first_name` is assigned to below." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6732bd91-c726-4603-b4c4-7c70fc5eae7d", + "metadata": {}, + "outputs": [], + "source": [ + "people['gjb'].first_name = 'Geert Jan'" + ] + }, + { + "cell_type": "markdown", + "id": "63b5c683-0a51-45e1-9d65-c9527e75136d", + "metadata": {}, + "source": [ + "The dictionary indeed contains a single entry, a `Person` with `first_name` initialized, and `liast_name` and `age` attributes still set to `None`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c510d857-6796-48c4-a181-9ce58828cc2f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "defaultdict(__main__.Person,\n", + " {'gjb': Person(first_name='Geert Jan', last_name=None, age=None)})" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "people" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.5" + }, + "toc-autonumbering": true + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 25ba45244d3648a3b54d29ba6bbc1b5ceb0ecb35 Mon Sep 17 00:00:00 2001 From: Geert Jan Bex Date: Mon, 21 Mar 2022 21:04:22 +0100 Subject: [PATCH 06/10] Add sieve of Eratosthenes implementation using generators --- source-code/functional-programming/README.md | 2 + .../sieve_of_eratosthenes.ipynb | 177 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 source-code/functional-programming/sieve_of_eratosthenes.ipynb diff --git a/source-code/functional-programming/README.md b/source-code/functional-programming/README.md index a281182..67930a2 100644 --- a/source-code/functional-programming/README.md +++ b/source-code/functional-programming/README.md @@ -4,3 +4,5 @@ Some examples of a functional programming style in Python. ## What is it? 1. `functional_programming_style.ipynb`: notebook explaining a functional programming style in Python with examples. +1. `sieve_of_eratosthenes.ipynb`: a lazy implementation of the sieve of + Eratosthenes using generators. diff --git a/source-code/functional-programming/sieve_of_eratosthenes.ipynb b/source-code/functional-programming/sieve_of_eratosthenes.ipynb new file mode 100644 index 0000000..5296f4d --- /dev/null +++ b/source-code/functional-programming/sieve_of_eratosthenes.ipynb @@ -0,0 +1,177 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1be55c27-90c2-42e9-ae9d-ace0c148d609", + "metadata": {}, + "source": [ + "# Requirements" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "2535d817-9d04-4830-a43c-f33412031e1f", + "metadata": {}, + "outputs": [], + "source": [ + "from itertools import count" + ] + }, + { + "cell_type": "markdown", + "id": "24c47c91-50e1-41c7-9be1-88df29f01aae", + "metadata": {}, + "source": [ + "# Sieve of Eratosthenes" + ] + }, + { + "cell_type": "markdown", + "id": "a6e0e405-e8c9-4579-a756-ba9bb938c9d3", + "metadata": {}, + "source": [ + "The sieve of Eratosthenes is an ancient algorithm to determine prime numbers. Consider an imaginary list of all prime numbers larger than 1. The first number is 2, and it is prime. Now strike all the other even numbers from the list, since they can't be prime. The next remaining number in the list is 3, so that is prime, but all numbers divisible by 3 can't be prime, strike them from the list. Repeat until you run of of time or get terminally bored." + ] + }, + { + "cell_type": "markdown", + "id": "11557a97-fab5-4a24-ae17-73d691e2e9a0", + "metadata": {}, + "source": [ + "Consider a generator that lazily produces numbers. Now we can construct another generator that uses that first generator, but filters out some numbers using a Boolean condition. If we start with a generator that produces all odd numbers larger than 3, the next iterator can filter out all the multiples of 3, the one based on that all the multiples of 5, and so on." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "e072d0e6-efb8-4f31-83ee-b08006b7294f", + "metadata": {}, + "outputs": [], + "source": [ + "def eratosthenes_sieve(numbers):\n", + " prime = next(numbers)\n", + " yield prime\n", + " yield from (number for number in numbers if number % prime != 0)" + ] + }, + { + "cell_type": "markdown", + "id": "11bdddec-a20f-4904-9134-5de74d2813aa", + "metadata": {}, + "source": [ + "The first number produced by the generator `numbers` is always a prime, so it is returned. Next a new iterator is constructed by yields all numbers, except those divisible by the prime number we've just returned." + ] + }, + { + "cell_type": "markdown", + "id": "27a70870-1f34-48cf-9b23-48b8afb93aa5", + "metadata": {}, + "source": [ + "As a last step, we write a function that yields 2, and then all the odd prime numbers." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "85eaccfd-c2d3-47c8-8241-9d7e7a810943", + "metadata": {}, + "outputs": [], + "source": [ + "def primes():\n", + " yield 2\n", + " yield from eratosthenes_sieve(count(start=3, step=2))" + ] + }, + { + "cell_type": "markdown", + "id": "74f29fa4-10c4-4bf5-bebb-8ddc16689fb2", + "metadata": {}, + "source": [ + "Use this to generator all prime numbers less than 100." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "d2b0242f-1233-451d-9248-5a65eea1e9e5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n", + "3\n", + "5\n", + "7\n", + "11\n", + "13\n", + "17\n", + "19\n", + "23\n", + "25\n", + "29\n", + "31\n", + "35\n", + "37\n", + "41\n", + "43\n", + "47\n", + "49\n", + "53\n", + "55\n", + "59\n", + "61\n", + "65\n", + "67\n", + "71\n", + "73\n", + "77\n", + "79\n", + "83\n", + "85\n", + "89\n", + "91\n", + "95\n", + "97\n" + ] + } + ], + "source": [ + "for prime in primes():\n", + " if prime >= 100: break\n", + " print(prime)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54735c93-da12-4d34-aa82-f448eb9586ad", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 08b54c60653870bdd91d20ecbed91cb87d1a287c Mon Sep 17 00:00:00 2001 From: Geert Jan Bex Date: Fri, 25 Mar 2022 08:46:49 +0100 Subject: [PATCH 07/10] Add notebook comparing classic and lazy prime number generation --- source-code/prime_time.ipynb | 271 +++++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 source-code/prime_time.ipynb diff --git a/source-code/prime_time.ipynb b/source-code/prime_time.ipynb new file mode 100644 index 0000000..a2b6776 --- /dev/null +++ b/source-code/prime_time.ipynb @@ -0,0 +1,271 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fc3b2761-8665-477a-8bfc-58bdf2ac4f7a", + "metadata": {}, + "source": [ + "# Requirements" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "47182c93-a8f7-4772-9a9c-e58a433d02ba", + "metadata": {}, + "outputs": [], + "source": [ + "import itertools\n", + "import math" + ] + }, + { + "cell_type": "markdown", + "id": "4e0adefe-f566-4a81-a0df-0b5d0c7604cf", + "metadata": {}, + "source": [ + "# Precomputed sieve" + ] + }, + { + "cell_type": "markdown", + "id": "0f0a2be6-515a-4d25-a5c8-67b9dfefe69a", + "metadata": {}, + "source": [ + "The sieve of Eratosthenes is computed and stored in a list. The overall implementation is a generator for convenience." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "b6c3ba8e-e5c5-4a3a-b059-02fa0283776a", + "metadata": {}, + "outputs": [], + "source": [ + "def prime_numbers_precomputed(max_n):\n", + " sieve = [True]*(max_n + 1)\n", + " sieve[0], sieve[1] = False, False\n", + " for n in range(2, math.isqrt(max_n) + 1):\n", + " if sieve[n]:\n", + " for non_prime in range(2*n, max_n + 1, n):\n", + " sieve[non_prime] = False\n", + " yield from (n for n, is_prime in enumerate(sieve) if is_prime)" + ] + }, + { + "cell_type": "markdown", + "id": "3ad4ec7f-303b-4980-8405-db68f8e86339", + "metadata": {}, + "source": [ + "Generate prime numbers upto 50." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a1ee6b85-5af0-4f4a-9a70-e1f08d177349", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n", + "3\n", + "5\n", + "7\n", + "11\n", + "13\n", + "17\n", + "19\n", + "23\n", + "29\n", + "31\n", + "37\n", + "41\n", + "43\n", + "47\n" + ] + } + ], + "source": [ + "for prime in prime_numbers_precomputed(50):\n", + " print(prime)" + ] + }, + { + "cell_type": "markdown", + "id": "6eef26fa-d00d-4fab-a8d7-08ffc52590bd", + "metadata": {}, + "source": [ + "# Sieve using generators" + ] + }, + { + "cell_type": "markdown", + "id": "54049286-713e-48fa-a1fb-44078942d348", + "metadata": {}, + "source": [ + "Implementation using generators, entirely lazy." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ab70534d-688f-46f4-8b30-44bf1e34ab5b", + "metadata": {}, + "outputs": [], + "source": [ + "def prime_numbers():\n", + " def prime_number_sieve(numbers):\n", + " prime = next(numbers)\n", + " yield prime\n", + " yield from (n for n in numbers if n % prime != 0)\n", + " yield 2\n", + " yield from prime_number_sieve(itertools.count(3, 2))" + ] + }, + { + "cell_type": "markdown", + "id": "b54d4fd3-4e5a-4556-9bac-85da2ceb9e19", + "metadata": {}, + "source": [ + "Again, generating the prime numbers up to 50." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "d67bb1fe-8e55-45b0-aace-36ab4397acb0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n", + "3\n", + "5\n", + "7\n", + "11\n", + "13\n", + "17\n", + "19\n", + "23\n", + "25\n", + "29\n", + "31\n", + "35\n", + "37\n", + "41\n", + "43\n", + "47\n", + "49\n" + ] + } + ], + "source": [ + "for prime in prime_numbers():\n", + " if prime <= 50:\n", + " print(prime)\n", + " else:\n", + " break" + ] + }, + { + "cell_type": "markdown", + "id": "edf1943a-31b2-4a4e-bf96-47ea2875b567", + "metadata": {}, + "source": [ + "# Comparing performance" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "83d6696e-b31c-4dbc-9b0c-702c9aedc4a9", + "metadata": {}, + "outputs": [], + "source": [ + "max_n = 1_000_000" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "bbea5e94-d82a-4be9-b4f4-152a8136c608", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "80.4 ms ± 2.83 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + } + ], + "source": [ + "%%timeit\n", + "numbers = []\n", + "for prime in prime_numbers_precomputed(max_n):\n", + " if prime <= max_n:\n", + " numbers.append(prime)\n", + " else:\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "351d9f27-6d04-4d70-b8e3-451d223f64a4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "46.9 ms ± 1.48 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + } + ], + "source": [ + "%%timeit\n", + "numbers = []\n", + "for prime in prime_numbers():\n", + " if prime <= max_n:\n", + " numbers.append(prime)\n", + " else:\n", + " break" + ] + }, + { + "cell_type": "markdown", + "id": "1111d621-2f13-456c-ba62-a493df7fdc64", + "metadata": {}, + "source": [ + "Interestingly, the lazy implementation is faster by a substantial factor, perhaps this could be improved by using a different data representation for the precomputed sieve." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From e40966b5e4bb6d17ac1575a62ee72a9470968830 Mon Sep 17 00:00:00 2001 From: Geert Jan Bex Date: Tue, 31 May 2022 15:31:45 +0200 Subject: [PATCH 08/10] Update to recent type syntax --- source-code/typing/incorrect_02.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source-code/typing/incorrect_02.py b/source-code/typing/incorrect_02.py index 8e9cfd9..46e20f4 100644 --- a/source-code/typing/incorrect_02.py +++ b/source-code/typing/incorrect_02.py @@ -10,5 +10,5 @@ def fib(n: int) -> int: return fib(n - 1) + fib(n - 2) if __name__ == '__main__': - n = int(sys.argv[1]) # type: str + n: str = int(sys.argv[1]) print(fib(n)) From 5f93037ced0d8d023031ff4932b5c7760f6674e7 Mon Sep 17 00:00:00 2001 From: Geert Jan Bex Date: Wed, 1 Jun 2022 12:37:24 +0200 Subject: [PATCH 09/10] Add type hints example for duck typing --- source-code/typing/README.md | 10 +++++ source-code/typing/duck_typing.py | 29 ++++++++++++++ source-code/typing/duck_typing_wrong.py | 29 ++++++++++++++ source-code/typing/typed_duck_typing.py | 40 +++++++++++++++++++ .../typed_duck_typing_false_positive.py | 36 +++++++++++++++++ source-code/typing/typed_duck_typing_wrong.py | 40 +++++++++++++++++++ 6 files changed, 184 insertions(+) create mode 100644 source-code/typing/duck_typing.py create mode 100644 source-code/typing/duck_typing_wrong.py create mode 100644 source-code/typing/typed_duck_typing.py create mode 100644 source-code/typing/typed_duck_typing_false_positive.py create mode 100644 source-code/typing/typed_duck_typing_wrong.py diff --git a/source-code/typing/README.md b/source-code/typing/README.md index ff6dd3d..ad7eaf0 100644 --- a/source-code/typing/README.md +++ b/source-code/typing/README.md @@ -23,3 +23,13 @@ Type checking can be done using [mypy](http://mypy-lang.org/index.html). which is a type error. 1. `people_incorrect.py`: code that defines a `People` class, stores some in a list with mistakes. +1. `duck_typing.py`: example code illustrating duck typing. +1. `duck_typing_wrong.py`: example code illustrating duck typing, but + with an error. +1. `typed_duck_typing.py`: example code illustrating duck typing + using type hints. +1. `typed_duck_typing_wrong.py`: example code illustrating duck typing + using type hints with an error. +1. `typed_duck_typing_false_positive.py`: example code illustrating + duck typing using type hints for which mypy 0.910 generates a + false positive. diff --git a/source-code/typing/duck_typing.py b/source-code/typing/duck_typing.py new file mode 100644 index 0000000..39d2b4a --- /dev/null +++ b/source-code/typing/duck_typing.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +import argparse + +class Duck: + + def make_sound(self): + print('quack') + + +class AlarmClock: + + def make_sound(self): + print('ring-ring') + + +def sound_repeater(sound_maker, nr_repeats): + for _ in range(nr_repeats): + sound_maker.make_sound() + + +if __name__ == '__main__': + arg_parser = argparse.ArgumentParser(description='sound maker') + arg_parser.add_argument('--type', choices=['duck', 'alarm'], + help='sound source') + arg_parser.add_argument('--n', type=int, default=1, + help='number of sounds to make') + options = arg_parser.parse_args() + sound_maker = Duck() if options.type == 'duck' else AlarmClock() + sound_repeater(sound_maker, options.n) diff --git a/source-code/typing/duck_typing_wrong.py b/source-code/typing/duck_typing_wrong.py new file mode 100644 index 0000000..76aaefa --- /dev/null +++ b/source-code/typing/duck_typing_wrong.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +import argparse + +class Duck: + + def quack(self): + print('quack') + + +class AlarmClock: + + def make_sound(self): + print('ring-ring') + + +def sound_repeater(sound_maker, nr_repeats): + for _ in range(nr_repeats): + sound_maker.make_sound() + + +if __name__ == '__main__': + arg_parser = argparse.ArgumentParser(description='sound maker') + arg_parser.add_argument('--type', choices=['duck', 'alarm'], + help='sound source') + arg_parser.add_argument('--n', type=int, default=1, + help='number of sounds to make') + options = arg_parser.parse_args() + sound_maker = Duck() if options.type == 'duck' else AlarmClock() + sound_repeater(sound_maker, options.n) diff --git a/source-code/typing/typed_duck_typing.py b/source-code/typing/typed_duck_typing.py new file mode 100644 index 0000000..0bc8e3b --- /dev/null +++ b/source-code/typing/typed_duck_typing.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +import argparse +from typing import Protocol + +class SoundMaker(Protocol): + + def make_sound(self) -> None: + pass + + +class Duck: + + def make_sound(self) -> None: + print('quack') + + +class AlarmClock: + + def make_sound(self) -> None: + print('ring-ring') + + +def sound_repeater(sound_maker: SoundMaker, nr_repeats: int): + for _ in range(nr_repeats): + sound_maker.make_sound() + + +if __name__ == '__main__': + arg_parser = argparse.ArgumentParser(description='sound maker') + arg_parser.add_argument('--type', choices=['duck', 'alarm'], + help='sound source') + arg_parser.add_argument('--n', type=int, default=1, + help='number of sounds to make') + options = arg_parser.parse_args() + sound_maker: SoundMaker + if options.type == 'duck': + sound_maker = Duck() + else: + sound_maker = AlarmClock() + sound_repeater(sound_maker, options.n) diff --git a/source-code/typing/typed_duck_typing_false_positive.py b/source-code/typing/typed_duck_typing_false_positive.py new file mode 100644 index 0000000..d799e80 --- /dev/null +++ b/source-code/typing/typed_duck_typing_false_positive.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +import argparse +from typing import Protocol + +class SoundMaker(Protocol): + + def make_sound(self) -> None: + pass + + +class Duck: + + def make_sound(self) -> None: + print('quack') + + +class AlarmClock: + + def make_sound(self) -> None: + print('ring-ring') + + +def sound_repeater(sound_maker: SoundMaker, nr_repeats: int): + for _ in range(nr_repeats): + sound_maker.make_sound() + + +if __name__ == '__main__': + arg_parser = argparse.ArgumentParser(description='sound maker') + arg_parser.add_argument('--type', choices=['duck', 'alarm'], + help='sound source') + arg_parser.add_argument('--n', type=int, default=1, + help='number of sounds to make') + options = arg_parser.parse_args() + sound_maker: SoundMaker = Duck() if options.type == 'duck' else AlarmClock() + sound_repeater(sound_maker, options.n) diff --git a/source-code/typing/typed_duck_typing_wrong.py b/source-code/typing/typed_duck_typing_wrong.py new file mode 100644 index 0000000..6dfa123 --- /dev/null +++ b/source-code/typing/typed_duck_typing_wrong.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +import argparse +from typing import Protocol + +class SoundMaker(Protocol): + + def make_sound(self) -> None: + pass + + +class Duck: + + def quack(self) -> None: + print('quack') + + +class AlarmClock: + + def make_sound(self) -> None: + print('ring-ring') + + +def sound_repeater(sound_maker: SoundMaker, nr_repeats: int): + for _ in range(nr_repeats): + sound_maker.make_sound() + + +if __name__ == '__main__': + arg_parser = argparse.ArgumentParser(description='sound maker') + arg_parser.add_argument('--type', choices=['duck', 'alarm'], + help='sound source') + arg_parser.add_argument('--n', type=int, default=1, + help='number of sounds to make') + options = arg_parser.parse_args() + sound_maker: SoundMaker + if options.type == 'duck': + sound_maker = Duck() + else: + sound_maker = AlarmClock() + sound_repeater(sound_maker, options.n) From 671bdb83474b02f21a27646ef8504df13eb20d67 Mon Sep 17 00:00:00 2001 From: Geert Jan Bex Date: Wed, 1 Jun 2022 21:52:01 +0200 Subject: [PATCH 10/10] Update Python version --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index ad8da8c..8ed3e97 100644 --- a/environment.yml +++ b/environment.yml @@ -6,7 +6,7 @@ dependencies: - jupyterlab - flake8 - pytest - - python=3.9 + - python=3.10 - mypy - pylint - matplotlib