diff --git a/01-data-model/data-model.ipynb b/01-data-model/data-model.ipynb index f525899..5b30397 100644 --- a/01-data-model/data-model.ipynb +++ b/01-data-model/data-model.ipynb @@ -482,7 +482,7 @@ " self.y = y\n", "\n", " def __repr__(self):\n", - " return 'Vector(%r, %r)' % (self.x, self.y)\n", + " return f'Vector({self.x!r}, {self.y!r})'\n", "\n", " def __abs__(self):\n", " return math.hypot(self.x, self.y)\n", diff --git a/02-array-seq/array-seq.ipynb b/02-array-seq/array-seq.ipynb index 032d811..afdbf6b 100644 --- a/02-array-seq/array-seq.ipynb +++ b/02-array-seq/array-seq.ipynb @@ -9,12 +9,14 @@ "**Sections with code snippets in this chapter:**\n", "\n", "* [List Comprehensions and Generator Expressions](#List-Comprehensions-and-Generator-Expressions)\n", + "* [Tuples Are Not Just Immutable Lists](#Tuples-Are-Not-Just-Immutable-Lists)\n", + "* [Unpacking sequences and iterables](#Unpacking-sequences-and-iterables)\n", + "* [Pattern Matching with Sequences](#Pattern-Matching-with-Sequences)\n", "* [Slicing](#Slicing)\n", - "* [Building Lists of Lists](#Building-Lists-of-Lists)\n", + "* [Using + and * with Sequences](#Using-+-and-*-with-Sequences)\n", "* [Augmented Assignment with Sequences](#Augmented-Assignment-with-Sequences)\n", "* [list.sort and the sorted Built-In Function](#list.sort-and-the-sorted-Built-In-Function)\n", - "* [Managing Ordered Sequences with bisect](#Managing-Ordered-Sequences-with-bisect)\n", - "* [Arrays](#Arrays)\n", + "* [When a List Is Not the Answer](#When-a-List-Is-Not-the-Answer)\n", "* [Memory Views](#Memory-Views)\n", "* [NumPy and SciPy](#NumPy-and-SciPy)\n", "* [Deques and Other Queues](#Deques-and-Other-Queues)\n", @@ -42,9 +44,7 @@ "outputs": [ { "data": { - "text/plain": [ - "[36, 162, 163, 165, 8364, 164]" - ] + "text/plain": "[36, 162, 163, 165, 8364, 164]" }, "execution_count": 1, "metadata": {}, @@ -65,7 +65,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2-2. Build a list of Unicode codepoints from a string, take 2" + "#### Example 2-2. Build a list of Unicode codepoints from a string, using a listcomp" ] }, { @@ -75,9 +75,7 @@ "outputs": [ { "data": { - "text/plain": [ - "[36, 162, 163, 165, 8364, 164]" - ] + "text/plain": "[36, 162, 163, 165, 8364, 164]" }, "execution_count": 2, "metadata": {}, @@ -106,9 +104,7 @@ "outputs": [ { "data": { - "text/plain": [ - "'ABC'" - ] + "text/plain": "'ABC'" }, "execution_count": 3, "metadata": {}, @@ -128,9 +124,7 @@ "outputs": [ { "data": { - "text/plain": [ - "[65, 66, 67]" - ] + "text/plain": "[65, 66, 67]" }, "execution_count": 4, "metadata": {}, @@ -141,6 +135,30 @@ "codes" ] }, + { + "cell_type": "code", + "execution_count": 5, + "outputs": [ + { + "data": { + "text/plain": "67" + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "codes = [last := ord(c) for c in x]\n", + "last" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, { "cell_type": "markdown", "metadata": {}, @@ -150,214 +168,958 @@ }, { "cell_type": "code", - "execution_count": 5, - "metadata": {}, + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": "[162, 163, 165, 8364, 164]" + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "symbols = '$¢£¥€¤'\n", + "beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]\n", + "beyond_ascii" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": "[162, 163, 165, 8364, 164]" + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))\n", + "beyond_ascii" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 2-4. Cartesian product using a list comprehension" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": "[('black', 'S'),\n ('black', 'M'),\n ('black', 'L'),\n ('white', 'S'),\n ('white', 'M'),\n ('white', 'L')]" + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "colors = ['black', 'white']\n", + "sizes = ['S', 'M', 'L']\n", + "tshirts = [(color, size) for color in colors for size in sizes]\n", + "tshirts" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('black', 'S')\n", + "('black', 'M')\n", + "('black', 'L')\n", + "('white', 'S')\n", + "('white', 'M')\n", + "('white', 'L')\n" + ] + } + ], + "source": [ + "for color in colors:\n", + " for size in sizes:\n", + " print((color, size))" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": "[('black', 'S'),\n ('black', 'M'),\n ('black', 'L'),\n ('white', 'S'),\n ('white', 'M'),\n ('white', 'L')]" + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "shirts = [(color, size) for size in sizes\n", + " for color in colors]\n", + "tshirts" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 2-5. Initializing a tuple and an array from a generator expression" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": "(36, 162, 163, 165, 8364, 164)" + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "symbols = '$¢£¥€¤'\n", + "tuple(ord(symbol) for symbol in symbols)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": "array('I', [36, 162, 163, 165, 8364, 164])" + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import array\n", + "\n", + "array.array('I', (ord(symbol) for symbol in symbols))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 2-6. Cartesian product in a generator expression" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "black S\n", + "black M\n", + "black L\n", + "white S\n", + "white M\n", + "white L\n" + ] + } + ], + "source": [ + "colors = ['black', 'white']\n", + "sizes = ['S', 'M', 'L']\n", + "\n", + "for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes):\n", + " print(tshirt)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Tuples Are Not Just Immutable Lists" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, + "execution_count": 73 + }, + { + "cell_type": "markdown", + "source": [ + "#### Example 2-7. Tuples used as records" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 14, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BRA/CE342567\n", + "ESP/XDA205856\n", + "USA/31195855\n" + ] + } + ], + "source": [ + "lax_coordinates = (33.9425, -118.408056)\n", + "city, year, pop, chg, area = ('Tokyo', 2003, 32_450, 0.66, 8014)\n", + "traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')]\n", + "\n", + "for passport in sorted(traveler_ids):\n", + " print('%s/%s' % passport)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 15, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "USA\n", + "BRA\n", + "ESP\n" + ] + } + ], + "source": [ + "for country, _ in traveler_ids:\n", + " print(country)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "### Tuples as Immutable Lists" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 16, + "outputs": [ + { + "data": { + "text/plain": "True" + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a = (10, 'alpha', [1, 2])\n", + "b = (10, 'alpha', [1, 2])\n", + "a == b" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 17, + "outputs": [ + { + "data": { + "text/plain": "False" + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b[-1].append(99)\n", + "a == b" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 18, + "outputs": [ + { + "data": { + "text/plain": "(10, 'alpha', [1, 2, 99])" + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 19, + "outputs": [ + { + "data": { + "text/plain": "True" + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def fixed(o):\n", + " try:\n", + " hash(o)\n", + " except TypeError:\n", + " return False\n", + " return True\n", + "\n", + "\n", + "tf = (10, 'alpha', (1, 2)) # Contains no mutable items\n", + "tm = (10, 'alpha', [1, 2]) # Contains a mutable item (list)\n", + "fixed(tf)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 20, + "outputs": [ + { + "data": { + "text/plain": "False" + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fixed(tm)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## Unpacking sequences and iterables" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 21, + "outputs": [ + { + "data": { + "text/plain": "33.9425" + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lax_coordinates = (33.9425, -118.408056)\n", + "latitude, longitude = lax_coordinates # unpacking\n", + "latitude" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 22, + "outputs": [ + { + "data": { + "text/plain": "-118.408056" + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "longitude" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 23, + "outputs": [ + { + "data": { + "text/plain": "(2, 4)" + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "divmod(20, 8)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 24, + "outputs": [ + { + "data": { + "text/plain": "(2, 4)" + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "t = (20, 8)\n", + "divmod(*t)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 25, + "outputs": [ + { + "data": { + "text/plain": "(2, 4)" + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "quotient, remainder = divmod(*t)\n", + "quotient, remainder" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 26, + "outputs": [ + { + "data": { + "text/plain": "'id_rsa.pub'" + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import os\n", + "\n", + "_, filename = os.path.split('/home/luciano/.ssh/id_rsa.pub')\n", + "filename" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "### Using * to grab excess items" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 27, + "outputs": [ + { + "data": { + "text/plain": "(0, 1, [2, 3, 4])" + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a, b, *rest = range(5)\n", + "a, b, rest" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 28, + "outputs": [ + { + "data": { + "text/plain": "(0, 1, [2])" + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a, b, *rest = range(3)\n", + "a, b, rest" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 29, "outputs": [ { "data": { - "text/plain": [ - "[162, 163, 165, 8364, 164]" - ] + "text/plain": "(0, 1, [])" }, - "execution_count": 5, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "symbols = '$¢£¥€¤'\n", - "beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]\n", - "beyond_ascii" - ] + "a, b, *rest = range(2)\n", + "a, b, rest" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, + "execution_count": 30, "outputs": [ { "data": { - "text/plain": [ - "[162, 163, 165, 8364, 164]" - ] + "text/plain": "(0, [1, 2], 3, 4)" }, - "execution_count": 6, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))\n", - "beyond_ascii" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Example 2-4. Cartesian product using a list comprehension" - ] + "a, *body, c, d = range(5)\n", + "a, body, c, d" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, + "execution_count": 31, "outputs": [ { "data": { - "text/plain": [ - "[('black', 'S'),\n", - " ('black', 'M'),\n", - " ('black', 'L'),\n", - " ('white', 'S'),\n", - " ('white', 'M'),\n", - " ('white', 'L')]" - ] + "text/plain": "([0, 1], 2, 3, 4)" }, - "execution_count": 7, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "colors = ['black', 'white']\n", - "sizes = ['S', 'M', 'L']\n", - "tshirts = [(color, size) for color in colors for size in sizes]\n", - "tshirts" - ] + "*head, b, c, d = range(5)\n", + "head, b, c, d" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "### Unpacking with * in function calls and sequence literals" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, + "execution_count": 32, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "('black', 'S')\n", - "('black', 'M')\n", - "('black', 'L')\n", - "('white', 'S')\n", - "('white', 'M')\n", - "('white', 'L')\n" - ] + "data": { + "text/plain": "(1, 2, 3, 4, (5, 6))" + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "for color in colors:\n", - " for size in sizes:\n", - " print((color, size))" - ] + "def fun(a, b, c, d, *rest):\n", + " return a, b, c, d, rest\n", + "\n", + "\n", + "fun(*[1, 2], 3, *range(4, 7))" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } }, { "cell_type": "code", - "execution_count": 9, - "metadata": {}, + "execution_count": 33, "outputs": [ { "data": { - "text/plain": [ - "[('black', 'S'),\n", - " ('black', 'M'),\n", - " ('black', 'L'),\n", - " ('white', 'S'),\n", - " ('white', 'M'),\n", - " ('white', 'L')]" - ] + "text/plain": "(0, 1, 2, 3, 4)" }, - "execution_count": 9, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "shirts = [(color, size) for size in sizes\n", - " for color in colors]\n", - "tshirts" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Example 2-5. Initializing a tuple and an array from a generator expression" - ] + "*range(4), 4" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } }, { "cell_type": "code", - "execution_count": 10, - "metadata": {}, + "execution_count": 34, "outputs": [ { "data": { - "text/plain": [ - "(36, 162, 163, 165, 8364, 164)" - ] + "text/plain": "[0, 1, 2, 3, 4]" }, - "execution_count": 10, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "symbols = '$¢£¥€¤'\n", - "tuple(ord(symbol) for symbol in symbols)" - ] + "[*range(4), 4]" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } }, { "cell_type": "code", - "execution_count": 11, - "metadata": {}, + "execution_count": 35, "outputs": [ { "data": { - "text/plain": [ - "array('I', [36, 162, 163, 165, 8364, 164])" - ] + "text/plain": "{0, 1, 2, 3, 4, 5, 6, 7}" }, - "execution_count": 11, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import array\n", - "array.array('I', (ord(symbol) for symbol in symbols))" - ] + "{*range(4), 4, *(5, 6, 7)}" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ - "#### Example 2-6. Cartesian product in a generator expression" - ] + "### Nested unpacking\n", + "#### Example 2-8. Unpacking nested tuples to access the longitude\n", + "\n", + "[02-array-seq/metro_lat_lon.py](02-array-seq/metro_lat_lon.py)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## Pattern Matching with Sequences\n", + "#### Example 2-9. Method from an imaginary Robot class" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } }, { "cell_type": "code", - "execution_count": 12, - "metadata": {}, + "execution_count": 36, + "outputs": [], + "source": [ + "# def handle_command(self, message):\n", + "# match message:\n", + "# case ['BEEPER', frequency, times]:\n", + "# self.beep(times, frequency)\n", + "# case ['NECK', angle]:\n", + "# self.rotate_neck(angle)\n", + "# case ['LED', ident, intensity]:\n", + "# self.leds[ident].set_brightness(ident, intensity)\n", + "# case ['LED', ident, red, green, blue]:\n", + "# self.leds[ident].set_color(ident, red, green, blue)\n", + "# case _:\n", + "# raise InvalidCommand(message)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "#### Example 2-10. Destructuring nested tuples—requires Python ≥ 3.10.\n", + "[02-array-seq/match_lat_lon.py](02-array-seq/match_lat_lon.py)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 37, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "black S\n", - "black M\n", - "black L\n", - "white S\n", - "white M\n", - "white L\n" + " | latitude | longitude\n", + "Mexico City | 19.4333 | -99.1333\n", + "New York-Newark | 40.8086 | -74.0204\n", + "São Paulo | -23.5478 | -46.6358\n" ] } ], "source": [ - "colors = ['black', 'white']\n", - "sizes = ['S', 'M', 'L']\n", + "metro_areas = [\n", + " ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),\n", + " ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),\n", + " ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),\n", + " ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),\n", + " ('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),\n", + "]\n", "\n", - "for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes):\n", - " print(tshirt)" - ] + "def main():\n", + " print(f'{\"\":15} | {\"latitude\":>9} | {\"longitude\":>9}')\n", + " for record in metro_areas:\n", + " match record:\n", + " case [name, _, _, (lat, lon)] if lon <= 0:\n", + " print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')\n", + "main()" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "### Pattern Matching Sequences in an Interpreter\n", + "#### Example 2-11. Matching patterns without match/case.\n", + "[02-array-seq/lispy/py3.9/lis.py](02-array-seq/lispy/py3.9/lis.py)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "#### Example 2-12. Pattern matching with match/case—requires Python ≥ 3.10.\n", + "[02-array-seq/lispy/py3.10/lis.py](02-array-seq/lispy/py3.10/lis.py)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "markdown", + "source": [], + "metadata": { + "collapsed": false + } }, { "cell_type": "markdown", @@ -375,16 +1137,14 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 38, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[10, 20]" - ] + "text/plain": "[10, 20]" }, - "execution_count": 13, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } @@ -397,16 +1157,14 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 39, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[30, 40, 50, 60]" - ] + "text/plain": "[30, 40, 50, 60]" }, - "execution_count": 14, + "execution_count": 39, "metadata": {}, "output_type": "execute_result" } @@ -417,16 +1175,14 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 40, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[10, 20, 30]" - ] + "text/plain": "[10, 20, 30]" }, - "execution_count": 15, + "execution_count": 40, "metadata": {}, "output_type": "execute_result" } @@ -437,16 +1193,14 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 41, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[40, 50, 60]" - ] + "text/plain": "[40, 50, 60]" }, - "execution_count": 16, + "execution_count": 41, "metadata": {}, "output_type": "execute_result" } @@ -464,16 +1218,14 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 42, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "'bye'" - ] + "text/plain": "'bye'" }, - "execution_count": 17, + "execution_count": 42, "metadata": {}, "output_type": "execute_result" } @@ -485,16 +1237,14 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 43, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "'elcycib'" - ] + "text/plain": "'elcycib'" }, - "execution_count": 18, + "execution_count": 43, "metadata": {}, "output_type": "execute_result" } @@ -505,16 +1255,14 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 44, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "'eccb'" - ] + "text/plain": "'eccb'" }, - "execution_count": 19, + "execution_count": 44, "metadata": {}, "output_type": "execute_result" } @@ -527,12 +1275,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2-9. Line items from a flat-file invoice" + "#### Example 2-13. Line items from a flat-file invoice" ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 45, "metadata": {}, "outputs": [ { @@ -577,16 +1325,14 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 46, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" - ] + "text/plain": "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" }, - "execution_count": 21, + "execution_count": 46, "metadata": {}, "output_type": "execute_result" } @@ -598,16 +1344,14 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 47, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[0, 1, 20, 30, 5, 6, 7, 8, 9]" - ] + "text/plain": "[0, 1, 20, 30, 5, 6, 7, 8, 9]" }, - "execution_count": 22, + "execution_count": 47, "metadata": {}, "output_type": "execute_result" } @@ -619,16 +1363,14 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 48, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[0, 1, 20, 30, 5, 8, 9]" - ] + "text/plain": "[0, 1, 20, 30, 5, 8, 9]" }, - "execution_count": 23, + "execution_count": 48, "metadata": {}, "output_type": "execute_result" } @@ -640,16 +1382,14 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 49, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[0, 1, 20, 11, 5, 22, 9]" - ] + "text/plain": "[0, 1, 20, 11, 5, 22, 9]" }, - "execution_count": 24, + "execution_count": 49, "metadata": {}, "output_type": "execute_result" } @@ -668,7 +1408,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 50, "metadata": {}, "outputs": [ { @@ -688,16 +1428,14 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 51, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[0, 1, 100, 22, 9]" - ] + "text/plain": "[0, 1, 100, 22, 9]" }, - "execution_count": 26, + "execution_count": 51, "metadata": {}, "output_type": "execute_result" } @@ -711,21 +1449,19 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Using + and * with Sequences" + "## Using + and * with Sequences" ] }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 52, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]" - ] + "text/plain": "[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]" }, - "execution_count": 27, + "execution_count": 52, "metadata": {}, "output_type": "execute_result" } @@ -737,16 +1473,14 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 53, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "'abcdabcdabcdabcdabcd'" - ] + "text/plain": "'abcdabcdabcdabcdabcd'" }, - "execution_count": 28, + "execution_count": 53, "metadata": {}, "output_type": "execute_result" } @@ -766,21 +1500,19 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2-10. A list with three lists of length 3 can represent a tic-tac-toe board" + "#### Example 2-14. A list with three lists of length 3 can represent a tic-tac-toe board" ] }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 54, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]" - ] + "text/plain": "[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]" }, - "execution_count": 29, + "execution_count": 54, "metadata": {}, "output_type": "execute_result" } @@ -792,16 +1524,14 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 55, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]" - ] + "text/plain": "[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]" }, - "execution_count": 30, + "execution_count": 55, "metadata": {}, "output_type": "execute_result" } @@ -815,21 +1545,19 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2-11. A list with three references to the same list is useless" + "#### Example 2-15. A list with three references to the same list is useless" ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 56, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]" - ] + "text/plain": "[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]" }, - "execution_count": 31, + "execution_count": 56, "metadata": {}, "output_type": "execute_result" } @@ -841,16 +1569,14 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 57, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]" - ] + "text/plain": "[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]" }, - "execution_count": 32, + "execution_count": 57, "metadata": {}, "output_type": "execute_result" } @@ -869,16 +1595,14 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 58, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]" - ] + "text/plain": "[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]" }, - "execution_count": 33, + "execution_count": 58, "metadata": {}, "output_type": "execute_result" } @@ -893,16 +1617,14 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 59, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']]" - ] + "text/plain": "[['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']]" }, - "execution_count": 34, + "execution_count": 59, "metadata": {}, "output_type": "execute_result" } @@ -921,7 +1643,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 60, "metadata": {}, "outputs": [], "source": [ @@ -931,16 +1653,14 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 61, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "4414271936" - ] + "text/plain": "140694277263808" }, - "execution_count": 36, + "execution_count": 61, "metadata": {}, "output_type": "execute_result" } @@ -952,16 +1672,14 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 62, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[1, 2, 3, 1, 2, 3]" - ] + "text/plain": "[1, 2, 3, 1, 2, 3]" }, - "execution_count": 37, + "execution_count": 62, "metadata": {}, "output_type": "execute_result" } @@ -973,16 +1691,14 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 63, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "True" - ] + "text/plain": "True" }, - "execution_count": 38, + "execution_count": 63, "metadata": {}, "output_type": "execute_result" } @@ -993,7 +1709,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 64, "metadata": {}, "outputs": [], "source": [ @@ -1003,16 +1719,14 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 65, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "4414275328" - ] + "text/plain": "140694329335488" }, - "execution_count": 40, + "execution_count": 65, "metadata": {}, "output_type": "execute_result" } @@ -1024,35 +1738,34 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 66, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "False" - ] + "text/plain": "False" }, - "execution_count": 41, + "execution_count": 66, "metadata": {}, "output_type": "execute_result" } ], "source": [ "t *= 2\n", - "id(t) == idt # new tuple" + "id(t) == idt # new tuple" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### A += Assignment Puzzler" + "### A += Assignment Puzzler\n", + "#### Example 2-16. A riddle" ] }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 67, "metadata": {}, "outputs": [ { @@ -1071,18 +1784,28 @@ " print(repr(e))" ] }, + { + "cell_type": "markdown", + "source": [ + "#### Example 2-17. The unexpected result: item t2 is changed and an exception is raised" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 68, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "(1, 2, [30, 40, 50, 60])" - ] + "text/plain": "(1, 2, [30, 40, 50, 60])" }, - "execution_count": 43, + "execution_count": 68, "metadata": {}, "output_type": "execute_result" } @@ -1095,12 +1818,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2-14. Bytecode for the expression s[a] += b" + "#### Example 2-18. Bytecode for the expression s[a] += b" ] }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 69, "metadata": {}, "outputs": [ { @@ -1135,16 +1858,14 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 70, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "['apple', 'banana', 'grape', 'raspberry']" - ] + "text/plain": "['apple', 'banana', 'grape', 'raspberry']" }, - "execution_count": 45, + "execution_count": 70, "metadata": {}, "output_type": "execute_result" } @@ -1156,16 +1877,14 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 71, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "['grape', 'raspberry', 'apple', 'banana']" - ] + "text/plain": "['grape', 'raspberry', 'apple', 'banana']" }, - "execution_count": 46, + "execution_count": 71, "metadata": {}, "output_type": "execute_result" } @@ -1176,16 +1895,14 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 72, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "['raspberry', 'grape', 'banana', 'apple']" - ] + "text/plain": "['raspberry', 'grape', 'banana', 'apple']" }, - "execution_count": 47, + "execution_count": 72, "metadata": {}, "output_type": "execute_result" } @@ -1196,16 +1913,14 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 73, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "['grape', 'apple', 'banana', 'raspberry']" - ] + "text/plain": "['grape', 'apple', 'banana', 'raspberry']" }, - "execution_count": 48, + "execution_count": 73, "metadata": {}, "output_type": "execute_result" } @@ -1216,16 +1931,14 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 74, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "['raspberry', 'banana', 'grape', 'apple']" - ] + "text/plain": "['raspberry', 'banana', 'grape', 'apple']" }, - "execution_count": 49, + "execution_count": 74, "metadata": {}, "output_type": "execute_result" } @@ -1236,16 +1949,14 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 75, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "['grape', 'raspberry', 'apple', 'banana']" - ] + "text/plain": "['grape', 'raspberry', 'apple', 'banana']" }, - "execution_count": 50, + "execution_count": 75, "metadata": {}, "output_type": "execute_result" } @@ -1256,16 +1967,14 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 76, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "['apple', 'banana', 'grape', 'raspberry']" - ] + "text/plain": "['apple', 'banana', 'grape', 'raspberry']" }, - "execution_count": 51, + "execution_count": 76, "metadata": {}, "output_type": "execute_result" } @@ -1279,322 +1988,231 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Managing Ordered Sequences with bisect" + "## When a List Is Not the Answer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2-15. bisect finds insertion points for items in a sorted sequence" - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "DEMO: bisect_right\n", - "haystack -> 1 4 5 6 8 12 15 20 21 23 23 26 29 30\n", - "31 @ 14 | | | | | | | | | | | | | |31\n", - "30 @ 14 | | | | | | | | | | | | | |30\n", - "29 @ 13 | | | | | | | | | | | | |29\n", - "23 @ 11 | | | | | | | | | | |23\n", - "22 @ 9 | | | | | | | | |22\n", - "10 @ 5 | | | | |10\n", - " 8 @ 5 | | | | |8 \n", - " 5 @ 3 | | |5 \n", - " 2 @ 1 |2 \n", - " 1 @ 1 |1 \n", - " 0 @ 0 0 \n" - ] - } - ], - "source": [ - "# BEGIN BISECT_DEMO\n", - "import bisect\n", - "import sys\n", - "\n", - "HAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30]\n", - "NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]\n", - "\n", - "ROW_FMT = '{0:2d} @ {1:2d} {2}{0:<2d}'\n", - "\n", - "def demo(haystack, needles, bisect_fn):\n", - " print('DEMO:', bisect_fn.__name__) # <1>\n", - " print('haystack ->', ' '.join('%2d' % n for n in haystack))\n", - " for needle in reversed(needles):\n", - " position = bisect_fn(haystack, needle) # <2>\n", - " offset = position * ' |' # <3>\n", - " print(ROW_FMT.format(needle, position, offset)) # <4>\n", - "\n", - "demo(HAYSTACK, NEEDLES, bisect.bisect) # <5>\n", - "# END BISECT_DEMO" - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "DEMO: bisect_left\n", - "haystack -> 1 4 5 6 8 12 15 20 21 23 23 26 29 30\n", - "31 @ 14 | | | | | | | | | | | | | |31\n", - "30 @ 13 | | | | | | | | | | | | |30\n", - "29 @ 12 | | | | | | | | | | | |29\n", - "23 @ 9 | | | | | | | | |23\n", - "22 @ 9 | | | | | | | | |22\n", - "10 @ 5 | | | | |10\n", - " 8 @ 4 | | | |8 \n", - " 5 @ 2 | |5 \n", - " 2 @ 1 |2 \n", - " 1 @ 0 1 \n", - " 0 @ 0 0 \n" - ] - } - ], - "source": [ - "demo(HAYSTACK, NEEDLES, bisect.bisect_left)" + "### Arrays" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2-16. Given a test score, grade returns the corresponding letter grade" + "#### Example 2-19. Creating, saving, and loading a large array of floats" ] }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 77, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "['F', 'D', 'D', 'C', 'C', 'B', 'B', 'A', 'A']" - ] + "text/plain": "0.8190492979077034" }, - "execution_count": 54, + "execution_count": 77, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):\n", - " i = bisect.bisect(breakpoints, score)\n", - " return grades[i]\n", + "from array import array\n", + "from random import random, seed\n", + "seed(10) # Use seed to make the output consistent\n", "\n", - "[grade(score) for score in [55, 60, 65, 70, 75, 80, 85, 90, 95]]" + "floats = array('d', (random() for i in range(10 ** 7)))\n", + "floats[-1]" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 78, "metadata": {}, + "outputs": [], "source": [ - "#### Example 2-17. bisect_left maps a score of 60 to grade F, not D as in Example 2-16." + "with open('floats.bin', 'wb') as fp:\n", + " floats.tofile(fp)" ] }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 79, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "['F', 'F', 'D', 'D', 'C', 'C', 'B', 'B', 'A']" - ] + "text/plain": "0.8190492979077034" }, - "execution_count": 55, + "execution_count": 79, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):\n", - " i = bisect.bisect_left(breakpoints, score)\n", - " return grades[i]\n", + "floats2 = array('d')\n", "\n", - "[grade(score) for score in [55, 60, 65, 70, 75, 80, 85, 90, 95]]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Example 2-18. Insort keeps a sorted sequence always sorted" + "with open('floats.bin', 'rb') as fp:\n", + " floats2.fromfile(fp, 10 ** 7)\n", + "\n", + "floats2[-1]" ] }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 80, "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "insert 10 -> [10]\n", - "insert 0 -> [0, 10]\n", - "insert 6 -> [0, 6, 10]\n", - "insert 8 -> [0, 6, 8, 10]\n", - "insert 7 -> [0, 6, 7, 8, 10]\n", - "insert 2 -> [0, 2, 6, 7, 8, 10]\n", - "insert 10 -> [0, 2, 6, 7, 8, 10, 10]\n" - ] + "data": { + "text/plain": "True" + }, + "execution_count": 80, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "import bisect\n", - "import random\n", - "\n", - "SIZE = 7\n", - "\n", - "random.seed(1729)\n", - "\n", - "my_list = []\n", - "\n", - "for i in range(SIZE):\n", - " new_item = random.randrange(SIZE*2)\n", - " bisect.insort(my_list, new_item)\n", - " print(f'insert {new_item:2d} -> {my_list}')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## When a List Is Not the Answer" + "floats2 == floats" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Arrays" + "### Memory Views" ] }, { "cell_type": "markdown", - "metadata": {}, "source": [ - "#### Example 2-19. Creating, saving, and loading a large array of floats" - ] + "#### Example 2-20. Handling 6 bytes memory of as 1×6, 2×3, and 3×2 views" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } }, { "cell_type": "code", - "execution_count": 57, - "metadata": {}, + "execution_count": 81, "outputs": [ { "data": { - "text/plain": [ - "0.5963321947530882" - ] + "text/plain": "[0, 1, 2, 3, 4, 5]" }, - "execution_count": 57, + "execution_count": 81, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "from array import array\n", - "from random import random\n", - "\n", - "floats = array('d', (random() for i in range(10**7)))\n", - "floats[-1]" - ] + "octets = array('B', range(6))\n", + "m1 = memoryview(octets)\n", + "m1.tolist()" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } }, { "cell_type": "code", - "execution_count": 58, - "metadata": {}, - "outputs": [], + "execution_count": 82, + "outputs": [ + { + "data": { + "text/plain": "[[0, 1, 2], [3, 4, 5]]" + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "with open('floats.bin', 'wb') as fp:\n", - " floats.tofile(fp)" - ] + "m2 = m1.cast('B', [2, 3])\n", + "m2.tolist()" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } }, { "cell_type": "code", - "execution_count": 59, - "metadata": {}, + "execution_count": 83, "outputs": [ { "data": { - "text/plain": [ - "0.5963321947530882" - ] + "text/plain": "[[0, 1], [2, 3], [4, 5]]" }, - "execution_count": 59, + "execution_count": 83, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "floats2 = array('d')\n", - "\n", - "with open('floats.bin', 'rb') as fp:\n", - " floats2.fromfile(fp, 10**7)\n", - "\n", - "floats2[-1]" - ] + "m3 = m1.cast('B', [3, 2])\n", + "m3.tolist()" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } }, { "cell_type": "code", - "execution_count": 60, - "metadata": {}, + "execution_count": 84, "outputs": [ { "data": { - "text/plain": [ - "True" - ] + "text/plain": "array('B', [0, 1, 2, 33, 22, 5])" }, - "execution_count": 60, + "execution_count": 84, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "floats2 == floats" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Memory Views" - ] + "m2[1,1] = 22\n", + "m3[1,1] = 33\n", + "octets" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2-20. Changing the value of an array item by poking one of its bytes" + "#### Example 2-21. Changing the value of an 16-bit integer array item by poking one of its bytes" ] }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 85, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "5" - ] + "text/plain": "5" }, - "execution_count": 61, + "execution_count": 85, "metadata": {}, "output_type": "execute_result" } @@ -1607,16 +2225,14 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 86, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "-2" - ] + "text/plain": "-2" }, - "execution_count": 62, + "execution_count": 86, "metadata": {}, "output_type": "execute_result" } @@ -1627,16 +2243,14 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 87, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]" - ] + "text/plain": "[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]" }, - "execution_count": 63, + "execution_count": 87, "metadata": {}, "output_type": "execute_result" } @@ -1648,16 +2262,14 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 88, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "array('h', [-2, -1, 1024, 1, 2])" - ] + "text/plain": "array('h', [-2, -1, 1024, 1, 2])" }, - "execution_count": 64, + "execution_count": 88, "metadata": {}, "output_type": "execute_result" } @@ -1671,50 +2283,47 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### NumPy and SciPy" + "### NumPy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2-21. Basic operations with rows and columns in a numpy.ndarray" + "#### Example 2-22. Basic operations with rows and columns in a numpy.ndarray" ] }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 89, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])" - ] + "text/plain": "array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])" }, - "execution_count": 65, + "execution_count": 89, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import numpy as np\n", + "\n", "a = np.arange(12)\n", "a" ] }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 90, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "numpy.ndarray" - ] + "text/plain": "numpy.ndarray" }, - "execution_count": 66, + "execution_count": 90, "metadata": {}, "output_type": "execute_result" } @@ -1725,16 +2334,14 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 91, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "(12,)" - ] + "text/plain": "(12,)" }, - "execution_count": 67, + "execution_count": 91, "metadata": {}, "output_type": "execute_result" } @@ -1745,18 +2352,14 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 92, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "array([[ 0, 1, 2, 3],\n", - " [ 4, 5, 6, 7],\n", - " [ 8, 9, 10, 11]])" - ] + "text/plain": "array([[ 0, 1, 2, 3],\n [ 4, 5, 6, 7],\n [ 8, 9, 10, 11]])" }, - "execution_count": 68, + "execution_count": 92, "metadata": {}, "output_type": "execute_result" } @@ -1768,16 +2371,14 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 93, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "array([ 8, 9, 10, 11])" - ] + "text/plain": "array([ 8, 9, 10, 11])" }, - "execution_count": 69, + "execution_count": 93, "metadata": {}, "output_type": "execute_result" } @@ -1788,16 +2389,14 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 94, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "9" - ] + "text/plain": "9" }, - "execution_count": 70, + "execution_count": 94, "metadata": {}, "output_type": "execute_result" } @@ -1808,16 +2407,14 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": 95, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "array([1, 5, 9])" - ] + "text/plain": "array([1, 5, 9])" }, - "execution_count": 71, + "execution_count": 95, "metadata": {}, "output_type": "execute_result" } @@ -1828,25 +2425,20 @@ }, { "cell_type": "code", - "execution_count": 72, + "execution_count": 96, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "array([[ 0, 4, 8],\n", - " [ 1, 5, 9],\n", - " [ 2, 6, 10],\n", - " [ 3, 7, 11]])" - ] + "text/plain": "array([[ 0, 4, 8],\n [ 1, 5, 9],\n [ 2, 6, 10],\n [ 3, 7, 11]])" }, - "execution_count": 72, + "execution_count": 96, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "a.transpose()\n" + "a.transpose()" ] }, { @@ -1858,7 +2450,7 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": 97, "metadata": {}, "outputs": [], "source": [ @@ -1869,7 +2461,7 @@ }, { "cell_type": "code", - "execution_count": 74, + "execution_count": 98, "metadata": {}, "outputs": [], "source": [ @@ -1878,16 +2470,14 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": 99, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "array([0.29150425, 0.33893554, 0.08112756])" - ] + "text/plain": "array([0.06078257, 0.61741189, 0.84349987])" }, - "execution_count": 75, + "execution_count": 99, "metadata": {}, "output_type": "execute_result" } @@ -1898,16 +2488,14 @@ }, { "cell_type": "code", - "execution_count": 76, + "execution_count": 100, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "array([0.14575213, 0.16946777, 0.04056378])" - ] + "text/plain": "array([0.03039128, 0.30870594, 0.42174994])" }, - "execution_count": 76, + "execution_count": 100, "metadata": {}, "output_type": "execute_result" } @@ -1919,16 +2507,14 @@ }, { "cell_type": "code", - "execution_count": 77, + "execution_count": 101, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "True" - ] + "text/plain": "True" }, - "execution_count": 77, + "execution_count": 101, "metadata": {}, "output_type": "execute_result" } @@ -1943,7 +2529,7 @@ }, { "cell_type": "code", - "execution_count": 78, + "execution_count": 102, "metadata": {}, "outputs": [], "source": [ @@ -1954,16 +2540,14 @@ }, { "cell_type": "code", - "execution_count": 79, + "execution_count": 103, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "memmap([0.29150425, 0.33893554, 0.08112756])" - ] + "text/plain": "memmap([0.06078257, 0.61741189, 0.84349987])" }, - "execution_count": 79, + "execution_count": 103, "metadata": {}, "output_type": "execute_result" } @@ -1983,21 +2567,19 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2-22. Working with a deque" + "#### Example 2-23. Working with a deque" ] }, { "cell_type": "code", - "execution_count": 80, + "execution_count": 104, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])" - ] + "text/plain": "deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])" }, - "execution_count": 80, + "execution_count": 104, "metadata": {}, "output_type": "execute_result" } @@ -2011,16 +2593,14 @@ }, { "cell_type": "code", - "execution_count": 81, + "execution_count": 105, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6])" - ] + "text/plain": "deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6])" }, - "execution_count": 81, + "execution_count": 105, "metadata": {}, "output_type": "execute_result" } @@ -2032,16 +2612,14 @@ }, { "cell_type": "code", - "execution_count": 82, + "execution_count": 106, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])" - ] + "text/plain": "deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])" }, - "execution_count": 82, + "execution_count": 106, "metadata": {}, "output_type": "execute_result" } @@ -2053,16 +2631,14 @@ }, { "cell_type": "code", - "execution_count": 83, + "execution_count": 107, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9])" - ] + "text/plain": "deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9])" }, - "execution_count": 83, + "execution_count": 107, "metadata": {}, "output_type": "execute_result" } @@ -2074,16 +2650,14 @@ }, { "cell_type": "code", - "execution_count": 84, + "execution_count": 108, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33])" - ] + "text/plain": "deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33])" }, - "execution_count": 84, + "execution_count": 108, "metadata": {}, "output_type": "execute_result" } @@ -2095,16 +2669,14 @@ }, { "cell_type": "code", - "execution_count": 85, + "execution_count": 109, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8])" - ] + "text/plain": "deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8])" }, - "execution_count": 85, + "execution_count": 109, "metadata": {}, "output_type": "execute_result" } @@ -2130,7 +2702,7 @@ }, { "cell_type": "code", - "execution_count": 86, + "execution_count": 110, "metadata": {}, "outputs": [], "source": [ @@ -2139,7 +2711,7 @@ }, { "cell_type": "code", - "execution_count": 87, + "execution_count": 111, "metadata": {}, "outputs": [ { @@ -2166,16 +2738,14 @@ }, { "cell_type": "code", - "execution_count": 88, + "execution_count": 112, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[0, '1', 5, 6, '9', 14, 19, '23', 28, '28']" - ] + "text/plain": "[0, '1', 5, 6, '9', 14, 19, '23', 28, '28']" }, - "execution_count": 88, + "execution_count": 112, "metadata": {}, "output_type": "execute_result" } @@ -2188,16 +2758,14 @@ }, { "cell_type": "code", - "execution_count": 89, + "execution_count": 113, "metadata": {}, "outputs": [ { "data": { - "text/plain": [ - "[0, '1', 14, 19, '23', 28, '28', 5, 6, '9']" - ] + "text/plain": "[0, '1', 14, 19, '23', 28, '28', 5, 6, '9']" }, - "execution_count": 89, + "execution_count": 113, "metadata": {}, "output_type": "execute_result" } @@ -2208,7 +2776,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 113, "metadata": {}, "outputs": [], "source": [] @@ -2235,4 +2803,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/02-array-seq/lispy/py3.10/examples_test.py b/02-array-seq/lispy/py3.10/examples_test.py index 38510a3..603ccd3 100644 --- a/02-array-seq/lispy/py3.10/examples_test.py +++ b/02-array-seq/lispy/py3.10/examples_test.py @@ -1,19 +1,127 @@ """ -Doctests for `parse`: +Doctests for `parse` +-------------------- +# tag::PARSE[] >>> from lis import parse +>>> parse('1.5') +1.5 +>>> parse('ni!') +'ni!' +>>> parse('(gcd 18 45)') +['gcd', 18, 45] +>>> parse(''' +... (define double +... (lambda (n) +... (* n 2))) +... ''') +['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]] + +# end::PARSE[] + +Doctest for `Environment` +------------------------- + +# tag::ENVIRONMENT[] +>>> from lis import Environment +>>> inner_env = {'a': 2} +>>> outer_env = {'a': 0, 'b': 1} +>>> env = Environment(inner_env, outer_env) +>>> env['a'] = 111 # <1> +>>> env['c'] = 222 +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1}) +>>> env.change('b', 333) # <2> +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333}) + +# end::ENVIRONMENT[] -# tag::PARSE_DEMO[] ->>> parse('1.5') # <1> +Doctests for `evaluate` +----------------------- + +# tag::EVAL_NUMBER[] +>>> from lis import parse, evaluate, standard_env +>>> evaluate(parse('1.5'), {}) 1.5 ->>> parse('set!') # <2> -'set!' ->>> parse('(gcd 18 44)') # <3> -['gcd', 18, 44] ->>> parse('(- m (* n (// m n)))') # <4> -['-', 'm', ['*', 'n', ['//', 'm', 'n']]] -# end::PARSE_DEMO[] +# end::EVAL_NUMBER[] + +# tag::EVAL_SYMBOL[] +>>> from lis import standard_env +>>> evaluate(parse('+'), standard_env()) + +>>> evaluate(parse('ni!'), standard_env()) +Traceback (most recent call last): + ... +KeyError: 'ni!' + +# end::EVAL_SYMBOL[] + + +# tag::EVAL_QUOTE[] +>>> evaluate(parse('(quote no-such-name)'), standard_env()) +'no-such-name' +>>> evaluate(parse('(quote (99 bottles of beer))'), standard_env()) +[99, 'bottles', 'of', 'beer'] +>>> evaluate(parse('(quote (/ 10 0))'), standard_env()) +['/', 10, 0] + +# end::EVAL_QUOTE[] + +# tag::EVAL_IF[] +>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env()) +1 +>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env()) +0 + +# end::EVAL_IF[] + + +# tag::EVAL_LAMBDA[] +>>> expr = '(lambda (a b) (* (/ a b) 100))' +>>> f = evaluate(parse(expr), standard_env()) +>>> f # doctest: +ELLIPSIS + +>>> f(15, 20) +75.0 + +# end::EVAL_LAMBDA[] + +# tag::EVAL_DEFINE[] +>>> global_env = standard_env() +>>> evaluate(parse('(define answer (* 7 6))'), global_env) +>>> global_env['answer'] +42 + +# end::EVAL_DEFINE[] + +# tag::EVAL_DEFUN[] +>>> global_env = standard_env() +>>> percent = '(define (% a b) (* (/ a b) 100))' +>>> evaluate(parse(percent), global_env) +>>> global_env['%'] # doctest: +ELLIPSIS + +>>> global_env['%'](170, 200) +85.0 + +# end::EVAL_DEFUN[] + +function call: + +# tag::EVAL_CALL[] +>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env) +42.0 + +# end::EVAL_CALL[] + +# tag::EVAL_SYNTAX_ERROR[] +>>> evaluate(parse('(lambda is not like this)'), standard_env()) +Traceback (most recent call last): + ... +SyntaxError: (lambda is not like this) + +# end::EVAL_SYNTAX_ERROR[] """ @@ -39,16 +147,17 @@ def test_factorial(): gcd_src = """ (define (mod m n) - (- m (* n (// m n)))) + (- m (* n (quotient m n)))) (define (gcd m n) (if (= n 0) m (gcd n (mod m n)))) -(gcd 18 45) +(display (gcd 18 45)) """ -def test_gcd(): - got = run(gcd_src) - assert got == 9 +def test_gcd(capsys): + run(gcd_src) + captured = capsys.readouterr() + assert captured.out == '9\n' quicksort_src = """ @@ -104,6 +213,46 @@ def test_newton(): (define inc (make-adder 1)) (inc 99) """ -def test_newton(): +def test_closure(): got = run(closure_src) assert got == 100 + +closure_with_change_src = """ +(define (make-counter) + (define n 0) + (lambda () + (set! n (+ n 1)) + n) +) +(define counter (make-counter)) +(display (counter)) +(display (counter)) +(display (counter)) +""" +def test_closure_with_change(capsys): + run(closure_with_change_src) + captured = capsys.readouterr() + assert captured.out == '1\n2\n3\n' + + + +# tag::RUN_AVERAGER[] +closure_averager_src = """ +(define (make-averager) + (define count 0) + (define total 0) + (lambda (new-value) + (set! count (+ count 1)) + (set! total (+ total new-value)) + (/ total count) + ) +) +(define avg (make-averager)) +(avg 10) +(avg 11) +(avg 15) +""" +def test_closure_averager(): + got = run(closure_averager_src) + assert got == 12.0 +# end::RUN_AVERAGER[] diff --git a/02-array-seq/lispy/py3.10/lis.py b/02-array-seq/lispy/py3.10/lis.py old mode 100644 new mode 100755 index 035481f..88d9345 --- a/02-array-seq/lispy/py3.10/lis.py +++ b/02-array-seq/lispy/py3.10/lis.py @@ -1,53 +1,90 @@ +#!/usr/bin/env python + ################ Lispy: Scheme Interpreter in Python 3.10 ## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html ## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021) ## by Luciano Ramalho, adding type hints and pattern matching. + ################ Imports and Types +# tag::IMPORTS[] import math import operator as op from collections import ChainMap -from collections.abc import MutableMapping, Iterator from itertools import chain -from typing import Any, TypeAlias +from typing import Any, TypeAlias, NoReturn Symbol: TypeAlias = str Atom: TypeAlias = float | int | Symbol Expression: TypeAlias = Atom | list +# end::IMPORTS[] -Environment: TypeAlias = MutableMapping[Symbol, object] +################ Parsing: parse, tokenize, and read_from_tokens -class Procedure: - "A user-defined Scheme procedure." +def parse(program: str) -> Expression: + "Read a Scheme expression from a string." + return read_from_tokens(tokenize(program)) - def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment): - self.parms = parms - self.body = body - self.env = env +def tokenize(s: str) -> list[str]: + "Convert a string into a list of tokens." + return s.replace('(', ' ( ').replace(')', ' ) ').split() - def __call__(self, *args: Expression) -> Any: - local_env = dict(zip(self.parms, args)) - env: Environment = ChainMap(local_env, self.env) - for exp in self.body: - result = evaluate(exp, env) - return result +def read_from_tokens(tokens: list[str]) -> Expression: + "Read an expression from a sequence of tokens." + if len(tokens) == 0: + raise SyntaxError('unexpected EOF while reading') + token = tokens.pop(0) + if '(' == token: + exp = [] + while tokens[0] != ')': + exp.append(read_from_tokens(tokens)) + tokens.pop(0) # discard ')' + return exp + elif ')' == token: + raise SyntaxError('unexpected )') + else: + return parse_atom(token) + +def parse_atom(token: str) -> Atom: + "Numbers become numbers; every other token is a symbol." + try: + return int(token) + except ValueError: + try: + return float(token) + except ValueError: + return Symbol(token) ################ Global Environment +# tag::ENV_CLASS[] +class Environment(ChainMap[Symbol, Any]): + "A ChainMap that allows changing an item in-place." + + def change(self, key: Symbol, value: object) -> None: + "Find where key is defined and change the value there." + for map in self.maps: + if key in map: + map[key] = value + return + raise KeyError(key) +# end::ENV_CLASS[] + + def standard_env() -> Environment: "An environment with some Scheme standard procedures." - env: Environment = {} + env = Environment() env.update(vars(math)) # sin, cos, sqrt, pi, ... env.update({ '+': op.add, '-': op.sub, '*': op.mul, '/': op.truediv, - '//': op.floordiv, + 'quotient': op.floordiv, '>': op.gt, '<': op.lt, '>=': op.ge, @@ -60,6 +97,7 @@ def standard_env() -> Environment: 'car': lambda x: x[0], 'cdr': lambda x: x[1:], 'cons': lambda x, y: [x] + y, + 'display': lambda x: print(lispstr(x)), 'eq?': op.is_, 'equal?': op.eq, 'filter': lambda *args: list(filter(*args)), @@ -78,111 +116,104 @@ def standard_env() -> Environment: }) return env -################ Parsing: parse, tokenize, and read_from_tokens - -def parse(program: str) -> Expression: - "Read a Scheme expression from a string." - return read_from_tokens(tokenize(program)) - - -def tokenize(s: str) -> list[str]: - "Convert a string into a list of tokens." - return s.replace('(', ' ( ').replace(')', ' ) ').split() - - -def read_from_tokens(tokens: list[str]) -> Expression: - "Read an expression from a sequence of tokens." - if len(tokens) == 0: - raise SyntaxError('unexpected EOF while reading') - token = tokens.pop(0) - if '(' == token: - exp = [] - while tokens[0] != ')': - exp.append(read_from_tokens(tokens)) - tokens.pop(0) # discard ')' - return exp - elif ')' == token: - raise SyntaxError('unexpected )') - else: - return parse_atom(token) - - -def parse_atom(token: str) -> Atom: - "Numbers become numbers; every other token is a symbol." - try: - return int(token) - except ValueError: - try: - return float(token) - except ValueError: - return Symbol(token) - ################ Interaction: A REPL -def repl(prompt: str = 'lis.py> ') -> None: - "A prompt-read-evaluate-print loop." - global_env = standard_env() +# tag::REPL[] +def repl(prompt: str = 'lis.py> ') -> NoReturn: + "A prompt-read-eval-print loop." + global_env = Environment({}, standard_env()) while True: - val = evaluate(parse(input(prompt)), global_env) + ast = parse(input(prompt)) + val = evaluate(ast, global_env) if val is not None: print(lispstr(val)) - def lispstr(exp: object) -> str: "Convert a Python object back into a Lisp-readable string." if isinstance(exp, list): return '(' + ' '.join(map(lispstr, exp)) + ')' else: return str(exp) +# end::REPL[] + +################ Evaluator -################ eval +KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!'] -# tag::EVALUATE[] +# tag::EVAL_MATCH_TOP[] def evaluate(exp: Expression, env: Environment) -> Any: "Evaluate an expression in an environment." match exp: +# end::EVAL_MATCH_TOP[] case int(x) | float(x): return x - case Symbol(var): - return env[var] - case []: - return [] - case ['quote', exp]: - return exp - case ['if', test, consequence, alternative]: + case Symbol() as name: + return env[name] +# tag::EVAL_MATCH_MIDDLE[] + case ['quote', x]: # <1> + return x + case ['if', test, consequence, alternative]: # <2> if evaluate(test, env): return evaluate(consequence, env) else: return evaluate(alternative, env) - case ['define', Symbol(var), value_exp]: - env[var] = evaluate(value_exp, env) - case ['define', [Symbol(name), *parms], *body]: - env[name] = Procedure(parms, body, env) - case ['lambda', [*parms], *body]: + case ['lambda', [*parms], *body] if body: # <3> return Procedure(parms, body, env) - case [op, *args]: - proc = evaluate(op, env) + case ['define', Symbol() as name, value_exp]: # <4> + env[name] = evaluate(value_exp, env) +# end::EVAL_MATCH_MIDDLE[] + case ['define', [Symbol() as name, *parms], *body] if body: + env[name] = Procedure(parms, body, env) + case ['set!', Symbol() as name, value_exp]: + env.change(name, evaluate(value_exp, env)) + case [func_exp, *args] if func_exp not in KEYWORDS: + proc = evaluate(func_exp, env) values = [evaluate(arg, env) for arg in args] return proc(*values) - case _: - raise SyntaxError(repr(exp)) -# end::EVALUATE[] +# tag::EVAL_MATCH_BOTTOM[] + case _: # <5> + raise SyntaxError(lispstr(exp)) +# end::EVAL_MATCH_BOTTOM[] +# tag::PROCEDURE[] +class Procedure: + "A user-defined Scheme procedure." + + def __init__( # <1> + self, parms: list[Symbol], body: list[Expression], env: Environment + ): + self.parms = parms # <2> + self.body = body + self.env = env + + def __call__(self, *args: Expression) -> Any: # <3> + local_env = dict(zip(self.parms, args)) # <4> + env = Environment(local_env, self.env) # <5> + for exp in self.body: # <6> + result = evaluate(exp, env) + return result # <7> +# end::PROCEDURE[] -################ non-interactive execution +################ command-line interface -def run_lines(source: str) -> Iterator[Any]: - global_env: Environment = standard_env() +def run(source: str) -> Any: + global_env = Environment({}, standard_env()) tokens = tokenize(source) while tokens: exp = read_from_tokens(tokens) - yield evaluate(exp, global_env) + result = evaluate(exp, global_env) + return result +def main(args: list[str]) -> None: + if len(args) == 1: + with open(args[0]) as fp: + run(fp.read()) + else: + repl() -def run(source: str) -> Any: - for result in run_lines(source): - pass - return result +if __name__ == '__main__': + import sys + main(sys.argv[1:]) diff --git a/02-array-seq/lispy/py3.10/lis_test.py b/02-array-seq/lispy/py3.10/lis_test.py index 3688888..03dc15c 100644 --- a/02-array-seq/lispy/py3.10/lis_test.py +++ b/02-array-seq/lispy/py3.10/lis_test.py @@ -73,10 +73,10 @@ def test_evaluate(source: str, expected: Optional[Expression]) -> None: def std_env() -> Environment: return standard_env() -# tests for each of the cases in evaluate +# tests for cases in evaluate def test_evaluate_variable() -> None: - env: Environment = dict(x=10) + env = Environment({'x': 10}) source = 'x' expected = 10 got = evaluate(parse(source), env) @@ -168,8 +168,6 @@ def test_invocation_user_procedure(std_env: Environment) -> None: assert got == 22 -###################################### for py3.10/lis.py only - def test_define_function(std_env: Environment) -> None: source = '(define (max a b) (if (>= a b) a b))' got = evaluate(parse(source), std_env) diff --git a/02-array-seq/lispy/py3.10/meta_test.py b/02-array-seq/lispy/py3.10/meta_test.py deleted file mode 100644 index 3ddc3f3..0000000 --- a/02-array-seq/lispy/py3.10/meta_test.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Tests for developing a meta-circular interpreter, step-by-step. -""" - - -import operator as op - -from lis import run - -env_scm = """ -(define standard-env (list - (list (quote +) +) - (list (quote -) -) -)) -standard-env -""" - -def test_env_build(): - got = run(env_scm) - assert got == [['+', op.add], ['-', op.sub]] - -scan_scm = """ -(define l (quote (a b c))) -(define (scan what where) - (if (null? where) - () - (if (eq? what (car where)) - what - (scan what (cdr where)))) -) -""" - -def test_scan(): - source = scan_scm + '(scan (quote a) l )' - got = run(source) - assert got == 'a' - - -def test_scan_not_found(): - source = scan_scm + '(scan (quote z) l )' - got = run(source) - assert got == [] - - -lookup_scm = """ -(define env (list - (list (quote +) +) - (list (quote -) -) -)) -(define (lookup what where) - (if (null? where) - () - (if (eq? what (car (car where))) - (car (cdr (car where))) - (lookup what (cdr where)))) -) -""" - -def test_lookup(): - source = lookup_scm + '(lookup (quote +) env)' - got = run(source) - assert got == op.add - - -def test_lookup_not_found(): - source = lookup_scm + '(lookup (quote z) env )' - got = run(source) - assert got == [] - diff --git a/02-array-seq/lispy/py3.10/quicksort.scm b/02-array-seq/lispy/py3.10/quicksort.scm new file mode 100644 index 0000000..08dd596 --- /dev/null +++ b/02-array-seq/lispy/py3.10/quicksort.scm @@ -0,0 +1,17 @@ +(define (quicksort lst) + (if (null? lst) + lst + (begin + (define pivot (car lst)) + (define rest (cdr lst)) + (append + (quicksort + (filter (lambda (x) (< x pivot)) rest)) + (list pivot) + (quicksort + (filter (lambda (x) (>= x pivot)) rest))) + ) + ) +) +(display + (quicksort (list 2 1 6 3 4 0 8 9 7 5))) diff --git a/02-array-seq/lispy/py3.9-no-hints/lis.py b/02-array-seq/lispy/py3.9-no-hints/lis.py deleted file mode 100644 index c74c3ae..0000000 --- a/02-array-seq/lispy/py3.9-no-hints/lis.py +++ /dev/null @@ -1,142 +0,0 @@ -################ Lispy: Scheme Interpreter in Python 3.9 - -## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html -## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021) -## by Luciano Ramalho, adding type hints and pattern matching. - -################ Imports and Types - -import math -import operator as op -from collections import ChainMap -from typing import Any - -Symbol = str # A Lisp Symbol is implemented as a Python str -Number = (int, float) # A Lisp Number is implemented as a Python int or float - -class Procedure: - "A user-defined Scheme procedure." - def __init__(self, parms, body, env): - self.parms, self.body, self.env = parms, body, env - def __call__(self, *args): - env = ChainMap(dict(zip(self.parms, args)), self.env) - for exp in self.body: - result = evaluate(exp, env) - return result - - -################ Global Environment - -def standard_env(): - "An environment with some Scheme standard procedures." - env = {} - env.update(vars(math)) # sin, cos, sqrt, pi, ... - env.update({ - '+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv, - '>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq, - 'abs': abs, - 'append': op.add, - 'apply': lambda proc, args: proc(*args), - 'begin': lambda *x: x[-1], - 'car': lambda x: x[0], - 'cdr': lambda x: x[1:], - 'cons': lambda x,y: [x] + y, - 'eq?': op.is_, - 'equal?': op.eq, - 'length': len, - 'list': lambda *x: list(x), - 'list?': lambda x: isinstance(x,list), - 'map': lambda *args: list(map(*args)), - 'max': max, - 'min': min, - 'not': op.not_, - 'null?': lambda x: x == [], - 'number?': lambda x: isinstance(x, Number), - 'procedure?': callable, - 'round': round, - 'symbol?': lambda x: isinstance(x, Symbol), - }) - return env - -################ Parsing: parse, tokenize, and read_from_tokens - -def parse(program): - "Read a Scheme expression from a string." - return read_from_tokens(tokenize(program)) - -def tokenize(s): - "Convert a string into a list of tokens." - return s.replace('(',' ( ').replace(')',' ) ').split() - -def read_from_tokens(tokens): - "Read an expression from a sequence of tokens." - if len(tokens) == 0: - raise SyntaxError('unexpected EOF while reading') - token = tokens.pop(0) - if '(' == token: - exp = [] - while tokens[0] != ')': - exp.append(read_from_tokens(tokens)) - tokens.pop(0) # discard ')' - return exp - elif ')' == token: - raise SyntaxError('unexpected )') - else: - return parse_atom(token) - - -def parse_atom(token: str): - "Numbers become numbers; every other token is a symbol." - try: - return int(token) - except ValueError: - try: - return float(token) - except ValueError: - return Symbol(token) - - -################ Interaction: A REPL - -def repl(prompt: str = 'lis.py> ') -> None: - "A prompt-read-eval-print loop." - global_env = standard_env() - while True: - val = evaluate(parse(input(prompt)), global_env) - if val is not None: - print(lispstr(val)) - - -def lispstr(exp): - "Convert a Python object back into a Lisp-readable string." - if isinstance(exp, list): - return '(' + ' '.join(map(lispstr, exp)) + ')' - else: - return str(exp) - - -################ eval - -def evaluate(x, env): - "Evaluate an expression in an environment." - if isinstance(x, Symbol): # variable reference - return env[x] - elif not isinstance(x, list): # constant literal - return x - elif x[0] == 'quote': # (quote exp) - (_, exp) = x - return exp - elif x[0] == 'if': # (if test conseq alt) - (_, test, conseq, alt) = x - exp = (conseq if evaluate(test, env) else alt) - return evaluate(exp, env) - elif x[0] == 'define': # (define var exp) - (_, var, exp) = x - env[var] = evaluate(exp, env) - elif x[0] == 'lambda': # (lambda (var...) body) - (_, parms, *body) = x - return Procedure(parms, body, env) - else: # (proc arg...) - proc = evaluate(x[0], env) - args = [evaluate(exp, env) for exp in x[1:]] - return proc(*args) diff --git a/02-array-seq/lispy/py3.9/examples_test.py b/02-array-seq/lispy/py3.9/examples_test.py new file mode 100644 index 0000000..bdcda48 --- /dev/null +++ b/02-array-seq/lispy/py3.9/examples_test.py @@ -0,0 +1,196 @@ +""" +Doctests for `parse` +-------------------- + +# tag::PARSE[] +>>> from lis import parse +>>> parse('1.5') +1.5 +>>> parse('ni!') +'ni!' +>>> parse('(gcd 18 45)') +['gcd', 18, 45] +>>> parse(''' +... (define double +... (lambda (n) +... (* n 2))) +... ''') +['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]] + +# end::PARSE[] + +Doctest for `Environment` +------------------------- + +# tag::ENVIRONMENT[] +>>> from lis import Environment +>>> inner_env = {'a': 2} +>>> outer_env = {'a': 0, 'b': 1} +>>> env = Environment(inner_env, outer_env) +>>> env['a'] = 111 # <1> +>>> env['c'] = 222 +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1}) +>>> env.change('b', 333) # <2> +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333}) + +# end::ENVIRONMENT[] + +Doctests for `evaluate` +----------------------- + +# tag::EVAL_NUMBER[] +>>> from lis import parse, evaluate, standard_env +>>> evaluate(parse('1.5'), {}) +1.5 + +# end::EVAL_NUMBER[] + +# tag::EVAL_SYMBOL[] +>>> from lis import standard_env +>>> evaluate(parse('+'), standard_env()) + +>>> evaluate(parse('ni!'), standard_env()) +Traceback (most recent call last): + ... +KeyError: 'ni!' + +# end::EVAL_SYMBOL[] + + +# tag::EVAL_QUOTE[] +>>> evaluate(parse('(quote no-such-name)'), standard_env()) +'no-such-name' +>>> evaluate(parse('(quote (99 bottles of beer))'), standard_env()) +[99, 'bottles', 'of', 'beer'] +>>> evaluate(parse('(quote (/ 10 0))'), standard_env()) +['/', 10, 0] + +# end::EVAL_QUOTE[] + +# tag::EVAL_IF[] +>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env()) +1 +>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env()) +0 + +# end::EVAL_IF[] + + +# tag::EVAL_LAMBDA[] +>>> expr = '(lambda (a b) (* (/ a b) 100))' +>>> f = evaluate(parse(expr), standard_env()) +>>> f # doctest: +ELLIPSIS + +>>> f(15, 20) +75.0 + +# end::EVAL_LAMBDA[] + +# tag::EVAL_DEFINE[] +>>> global_env = standard_env() +>>> evaluate(parse('(define answer (* 7 6))'), global_env) +>>> global_env['answer'] +42 + +# end::EVAL_DEFINE[] + +# tag::EVAL_DEFUN[] +>>> global_env = standard_env() +>>> percent = '(define % (lambda (a b) (* (/ a b) 100)))' +>>> evaluate(parse(percent), global_env) +>>> global_env['%'] # doctest: +ELLIPSIS + +>>> global_env['%'](170, 200) +85.0 + +# end::EVAL_DEFUN[] + +function call: + +# tag::EVAL_CALL[] +>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env) +42.0 + +# end::EVAL_CALL[] + +""" + +import math + +from lis import run + + +fact_src = """ +(define ! + (lambda (n) + (if (< n 2) + 1 + (* n (! (- n 1))) + ) + ) +) +(! 42) +""" +def test_factorial(): + got = run(fact_src) + assert got == 1405006117752879898543142606244511569936384000000000 + assert got == math.factorial(42) + +closure_src = """ +(define make-adder + (lambda (increment) + (lambda (x) (+ increment x)) + ) +) +(define inc (make-adder 1)) +(inc 99) +""" +def test_closure(): + got = run(closure_src) + assert got == 100 + +closure_with_change_src = """ +(define make-counter + (lambda () + (define n 0) + (lambda () + (set! n (+ n 1)) + n) + ) +) +(define counter (make-counter)) +(display (counter)) +(display (counter)) +(display (counter)) +""" +def test_closure_with_change(capsys): + run(closure_with_change_src) + captured = capsys.readouterr() + assert captured.out == '1\n2\n3\n' + + + +# tag::RUN_AVERAGER[] +closure_averager_src = """ +(define make-averager + (lambda () + (define count 0) + (define total 0) + (lambda (new-value) + (set! count (+ count 1)) + (set! total (+ total new-value)) + (/ total count) + ) + ) +) +(define avg (make-averager)) +(avg 10) +(avg 11) +(avg 15) +""" +def test_closure_averager(): + got = run(closure_averager_src) + assert got == 12.0 +# end::RUN_AVERAGER[] \ No newline at end of file diff --git a/02-array-seq/lispy/py3.9/lis.py b/02-array-seq/lispy/py3.9/lis.py index 11f4402..dd80096 100644 --- a/02-array-seq/lispy/py3.9/lis.py +++ b/02-array-seq/lispy/py3.9/lis.py @@ -1,73 +1,24 @@ +#!/usr/bin/env python + ################ Lispy: Scheme Interpreter in Python 3.9 ## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html ## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021) ## by Luciano Ramalho, adding type hints and pattern matching. + ################ Imports and Types import math import operator as op from collections import ChainMap -from collections.abc import MutableMapping, Iterator from itertools import chain -from typing import Any, Union +from typing import Any, Union, NoReturn Symbol = str Atom = Union[float, int, Symbol] Expression = Union[Atom, list] -Environment = MutableMapping[Symbol, object] - - -class Procedure: - "A user-defined Scheme procedure." - - def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment): - self.parms = parms - self.body = body - self.env = env - - def __call__(self, *args: Expression) -> Any: - local_env = dict(zip(self.parms, args)) - env: Environment = ChainMap(local_env, self.env) - for exp in self.body: - result = evaluate(exp, env) - return result - - -################ Global Environment - -def standard_env() -> Environment: - "An environment with some Scheme standard procedures." - env: Environment = {} - env.update(vars(math)) # sin, cos, sqrt, pi, ... - env.update({ - '+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv, - '>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq, - 'abs': abs, - 'append': op.add, - 'apply': lambda proc, args: proc(*args), - 'begin': lambda *x: x[-1], - 'car': lambda x: x[0], - 'cdr': lambda x: x[1:], - 'cons': lambda x,y: [x] + y, - 'eq?': op.is_, - 'equal?': op.eq, - 'length': len, - 'list': lambda *x: list(x), - 'list?': lambda x: isinstance(x,list), - 'map': lambda *args: list(map(*args)), - 'max': max, - 'min': min, - 'not': op.not_, - 'null?': lambda x: x == [], - 'number?': lambda x: isinstance(x, (int, float)), - 'procedure?': callable, - 'round': round, - 'symbol?': lambda x: isinstance(x, Symbol), - }) - return env ################ Parsing: parse, tokenize, and read_from_tokens @@ -75,12 +26,10 @@ def parse(program: str) -> Expression: "Read a Scheme expression from a string." return read_from_tokens(tokenize(program)) - def tokenize(s: str) -> list[str]: "Convert a string into a list of tokens." return s.replace('(', ' ( ').replace(')', ' ) ').split() - def read_from_tokens(tokens: list[str]) -> Expression: "Read an expression from a sequence of tokens." if len(tokens) == 0: @@ -97,7 +46,6 @@ def read_from_tokens(tokens: list[str]) -> Expression: else: return parse_atom(token) - def parse_atom(token: str) -> Atom: "Numbers become numbers; every other token is a symbol." try: @@ -109,17 +57,73 @@ def parse_atom(token: str) -> Atom: return Symbol(token) +################ Global Environment + +class Environment(ChainMap[Symbol, Any]): + "A ChainMap that allows changing an item in-place." + + def change(self, key: Symbol, value: object) -> None: + "Find where key is defined and change the value there." + for map in self.maps: + if key in map: + map[key] = value # type: ignore[index] + return + raise KeyError(key) + + +def standard_env() -> Environment: + "An environment with some Scheme standard procedures." + env = Environment() + env.update(vars(math)) # sin, cos, sqrt, pi, ... + env.update({ + '+': op.add, + '-': op.sub, + '*': op.mul, + '/': op.truediv, + 'quotient': op.floordiv, + '>': op.gt, + '<': op.lt, + '>=': op.ge, + '<=': op.le, + '=': op.eq, + 'abs': abs, + 'append': lambda *args: list(chain(*args)), + 'apply': lambda proc, args: proc(*args), + 'begin': lambda *x: x[-1], + 'car': lambda x: x[0], + 'cdr': lambda x: x[1:], + 'cons': lambda x, y: [x] + y, + 'display': lambda x: print(lispstr(x)), + 'eq?': op.is_, + 'equal?': op.eq, + 'filter': lambda *args: list(filter(*args)), + 'length': len, + 'list': lambda *x: list(x), + 'list?': lambda x: isinstance(x, list), + 'map': lambda *args: list(map(*args)), + 'max': max, + 'min': min, + 'not': op.not_, + 'null?': lambda x: x == [], + 'number?': lambda x: isinstance(x, (int, float)), + 'procedure?': callable, + 'round': round, + 'symbol?': lambda x: isinstance(x, Symbol), + }) + return env + + ################ Interaction: A REPL -def repl(prompt: str = 'lis.py> ') -> None: +def repl(prompt: str = 'lis.py> ') -> NoReturn: "A prompt-read-eval-print loop." - global_env = standard_env() + global_env = Environment({}, standard_env()) while True: - val = evaluate(parse(input(prompt)), global_env) + ast = parse(input(prompt)) + val = evaluate(ast, global_env) if val is not None: print(lispstr(val)) - def lispstr(exp: object) -> str: "Convert a Python object back into a Lisp-readable string." if isinstance(exp, list): @@ -128,28 +132,78 @@ def lispstr(exp: object) -> str: return str(exp) -################ eval +################ Evaluator -def evaluate(x: Expression, env: Environment) -> Any: +# tag::EVAL_IF_TOP[] +def evaluate(exp: Expression, env: Environment) -> Any: "Evaluate an expression in an environment." - if isinstance(x, Symbol): # variable reference - return env[x] - elif not isinstance(x, list): # constant literal - return x - elif x[0] == 'quote': # (quote exp) - (_, exp) = x + if isinstance(exp, Symbol): # variable reference + return env[exp] +# end::EVAL_IF_TOP[] + elif not isinstance(exp, list): # constant literal return exp - elif x[0] == 'if': # (if test conseq alt) - (_, test, conseq, alt) = x - exp = (conseq if evaluate(test, env) else alt) - return evaluate(exp, env) - elif x[0] == 'define': # (define var exp) - (_, var, exp) = x - env[var] = evaluate(exp, env) - elif x[0] == 'lambda': # (lambda (var...) body) - (_, parms, *body) = x +# tag::EVAL_IF_MIDDLE[] + elif exp[0] == 'quote': # (quote exp) + (_, x) = exp + return x + elif exp[0] == 'if': # (if test conseq alt) + (_, test, consequence, alternative) = exp + if evaluate(test, env): + return evaluate(consequence, env) + else: + return evaluate(alternative, env) + elif exp[0] == 'lambda': # (lambda (parm…) body…) + (_, parms, *body) = exp return Procedure(parms, body, env) - else: # (proc arg...) - proc = evaluate(x[0], env) - args = [evaluate(exp, env) for exp in x[1:]] + elif exp[0] == 'define': + (_, name, value_exp) = exp + env[name] = evaluate(value_exp, env) +# end::EVAL_IF_MIDDLE[] + elif exp[0] == 'set!': + (_, name, value_exp) = exp + env.change(name, evaluate(value_exp, env)) + else: # (proc arg…) + (func_exp, *args) = exp + proc = evaluate(func_exp, env) + args = [evaluate(arg, env) for arg in args] return proc(*args) + + +class Procedure: + "A user-defined Scheme procedure." + + def __init__( + self, parms: list[Symbol], body: list[Expression], env: Environment + ): + self.parms = parms + self.body = body + self.env = env + + def __call__(self, *args: Expression) -> Any: + local_env = dict(zip(self.parms, args)) + env = Environment(local_env, self.env) + for exp in self.body: + result = evaluate(exp, env) + return result + + +################ command-line interface + +def run(source: str) -> Any: + global_env = Environment({}, standard_env()) + tokens = tokenize(source) + while tokens: + exp = read_from_tokens(tokens) + result = evaluate(exp, global_env) + return result + +def main(args: list[str]) -> None: + if len(args) == 1: + with open(args[0]) as fp: + run(fp.read()) + else: + repl() + +if __name__ == '__main__': + import sys + main(sys.argv[1:]) diff --git a/02-array-seq/lispy/py3.9/lis_test.py b/02-array-seq/lispy/py3.9/lis_test.py index 106ac24..64bafb4 100644 --- a/02-array-seq/lispy/py3.9/lis_test.py +++ b/02-array-seq/lispy/py3.9/lis_test.py @@ -1,8 +1,8 @@ -from typing import Any, Optional +from typing import Optional from pytest import mark, fixture -from lis import parse, evaluate, standard_env, Symbol, Environment, Expression +from lis import parse, evaluate, Expression, Environment, standard_env ############################################################# tests for parse @@ -73,10 +73,10 @@ def test_evaluate(source: str, expected: Optional[Expression]) -> None: def std_env() -> Environment: return standard_env() -# tests for each of the cases in evaluate +# tests for cases in evaluate def test_evaluate_variable() -> None: - env: Environment = dict(x=10) + env = Environment({'x': 10}) source = 'x' expected = 10 got = evaluate(parse(source), env) @@ -166,3 +166,15 @@ def test_invocation_user_procedure(std_env: Environment) -> None: """ got = evaluate(parse(source), std_env) assert got == 22 + + +def test_define_function(std_env: Environment) -> None: + source = '(define max (lambda (a b) (if (>= a b) a b)))' + got = evaluate(parse(source), std_env) + assert got is None + max_fn = std_env['max'] + assert max_fn.parms == ['a', 'b'] + assert max_fn.body == [['if', ['>=', 'a', 'b'], 'a', 'b']] + assert max_fn.env is std_env + assert max_fn(1, 2) == 2 + assert max_fn(3, 2) == 3 diff --git a/02-array-seq/test.sh b/02-array-seq/test.sh index 4747320..906717d 100755 --- a/02-array-seq/test.sh +++ b/02-array-seq/test.sh @@ -1,4 +1,4 @@ #!/bin/bash python3 -m doctest bisect_demo.py -python3 -m doctest metro_lat_long.py +python3 -m doctest metro_lat_lon.py pytest -q --nbval diff --git a/04-text-byte/charfinder/README.rst b/04-text-byte/charfinder/README.rst index 46a5d70..15a613d 100644 --- a/04-text-byte/charfinder/README.rst +++ b/04-text-byte/charfinder/README.rst @@ -58,7 +58,7 @@ Test ``find`` with single result:: Test ``find`` with two results:: - >>> find('chess', 'queen', last=0xFFFF) # doctest:+NORMALIZE_WHITESPACE + >>> find('chess', 'queen', end=0xFFFF) # doctest:+NORMALIZE_WHITESPACE U+2655 ♕ WHITE CHESS QUEEN U+265B ♛ BLACK CHESS QUEEN diff --git a/04-text-byte/charfinder/cf.py b/04-text-byte/charfinder/cf.py index 28f20d0..db982bc 100755 --- a/04-text-byte/charfinder/cf.py +++ b/04-text-byte/charfinder/cf.py @@ -2,11 +2,11 @@ import sys import unicodedata -FIRST, LAST = ord(' '), sys.maxunicode # <1> +START, END = ord(' '), sys.maxunicode + 1 # <1> -def find(*query_words, first=FIRST, last=LAST): # <2> +def find(*query_words, start=START, end=END): # <2> query = {w.upper() for w in query_words} # <3> - for code in range(first, last + 1): + for code in range(start, end): char = chr(code) # <4> name = unicodedata.name(char, None) # <5> if name and query.issubset(name.split()): # <6> diff --git a/05-data-classes/dataclass/club.py b/05-data-classes/dataclass/club.py index cd8ff46..7af49c8 100644 --- a/05-data-classes/dataclass/club.py +++ b/05-data-classes/dataclass/club.py @@ -1,9 +1,7 @@ from dataclasses import dataclass, field - @dataclass class ClubMember: - name: str guests: list = field(default_factory=list) diff --git a/05-data-classes/dataclass/club_wrong.py b/05-data-classes/dataclass/club_wrong.py index 3d73d6a..8521a9d 100644 --- a/05-data-classes/dataclass/club_wrong.py +++ b/05-data-classes/dataclass/club_wrong.py @@ -3,7 +3,6 @@ # tag::CLUBMEMBER[] @dataclass class ClubMember: - name: str guests: list = [] # end::CLUBMEMBER[] diff --git a/05-data-classes/dataclass/hackerclub.py b/05-data-classes/dataclass/hackerclub.py index 4d9112e..60f8234 100644 --- a/05-data-classes/dataclass/hackerclub.py +++ b/05-data-classes/dataclass/hackerclub.py @@ -6,7 +6,7 @@ >>> anna HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven') -If ``handle`` is ommitted, it's set to the first part of the member's name:: +If ``handle`` is omitted, it's set to the first part of the member's name:: >>> leo = HackerClubMember('Leo Rochael') >>> leo @@ -34,9 +34,7 @@ @dataclass class HackerClubMember(ClubMember): # <1> - all_handles = set() # <2> - handle: str = '' # <3> def __post_init__(self): diff --git a/05-data-classes/dataclass/hackerclub_annotated.py b/05-data-classes/dataclass/hackerclub_annotated.py index 5cf90fc..0b87c50 100644 --- a/05-data-classes/dataclass/hackerclub_annotated.py +++ b/05-data-classes/dataclass/hackerclub_annotated.py @@ -6,7 +6,7 @@ >>> anna HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven') -If ``handle`` is ommitted, it's set to the first part of the member's name:: +If ``handle`` is omitted, it's set to the first part of the member's name:: >>> leo = HackerClubMember('Leo Rochael') >>> leo @@ -35,9 +35,7 @@ @dataclass class HackerClubMember(ClubMember): - all_handles: ClassVar[set[str]] = set() - handle: str = '' def __post_init__(self): diff --git a/05-data-classes/dataclass/resource.py b/05-data-classes/dataclass/resource.py index 5190055..f332a11 100644 --- a/05-data-classes/dataclass/resource.py +++ b/05-data-classes/dataclass/resource.py @@ -32,7 +32,7 @@ from datetime import date -class ResourceType(Enum): # <1> +class ResourceType(Enum): # <1> BOOK = auto() EBOOK = auto() VIDEO = auto() diff --git a/05-data-classes/meaning/demo_dc.py b/05-data-classes/meaning/demo_dc.py index 3cc45ce..fa45bb8 100644 --- a/05-data-classes/meaning/demo_dc.py +++ b/05-data-classes/meaning/demo_dc.py @@ -2,7 +2,6 @@ @dataclass class DemoDataClass: - a: int # <1> b: float = 1.1 # <2> c = 'spam' # <3> diff --git a/05-data-classes/meaning/demo_nt.py b/05-data-classes/meaning/demo_nt.py index 317fb82..8f52354 100644 --- a/05-data-classes/meaning/demo_nt.py +++ b/05-data-classes/meaning/demo_nt.py @@ -1,7 +1,6 @@ import typing class DemoNTClass(typing.NamedTuple): - a: int # <1> b: float = 1.1 # <2> c = 'spam' # <3> diff --git a/05-data-classes/meaning/demo_plain.py b/05-data-classes/meaning/demo_plain.py index 6376959..98c3e40 100644 --- a/05-data-classes/meaning/demo_plain.py +++ b/05-data-classes/meaning/demo_plain.py @@ -1,5 +1,4 @@ class DemoPlainClass: - a: int # <1> b: float = 1.1 # <2> c = 'spam' # <3> diff --git a/05-data-classes/typing_namedtuple/coordinates.py b/05-data-classes/typing_namedtuple/coordinates.py index 378a430..5e4d879 100644 --- a/05-data-classes/typing_namedtuple/coordinates.py +++ b/05-data-classes/typing_namedtuple/coordinates.py @@ -11,7 +11,6 @@ from typing import NamedTuple class Coordinate(NamedTuple): - lat: float lon: float diff --git a/05-data-classes/typing_namedtuple/coordinates2.py b/05-data-classes/typing_namedtuple/coordinates2.py index 2032311..efcd6be 100644 --- a/05-data-classes/typing_namedtuple/coordinates2.py +++ b/05-data-classes/typing_namedtuple/coordinates2.py @@ -13,8 +13,7 @@ from typing import NamedTuple class Coordinate(NamedTuple): - lat: float # <1> lon: float reference: str = 'WGS84' # <2> -# end::COORDINATE[] \ No newline at end of file +# end::COORDINATE[] diff --git a/05-data-classes/typing_namedtuple/nocheck_demo.py b/05-data-classes/typing_namedtuple/nocheck_demo.py index 8ca5dc1..43c1b96 100644 --- a/05-data-classes/typing_namedtuple/nocheck_demo.py +++ b/05-data-classes/typing_namedtuple/nocheck_demo.py @@ -1,7 +1,6 @@ import typing class Coordinate(typing.NamedTuple): - lat: float lon: float diff --git a/06-obj-ref/README.rst b/06-obj-ref/README.rst index deac2fa..0d705e6 100644 --- a/06-obj-ref/README.rst +++ b/06-obj-ref/README.rst @@ -1,4 +1,4 @@ -Sample code for Chapter 8 - "Object references, mutability and recycling" +Sample code for Chapter 6 - "Object references, mutability and recycling" From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015) http://shop.oreilly.com/product/0636920032519.do diff --git a/07-1class-func/clip.py b/07-1class-func/clip.py deleted file mode 100644 index 2f97c66..0000000 --- a/07-1class-func/clip.py +++ /dev/null @@ -1,48 +0,0 @@ -""" - >>> clip('banana split', 5) - 'banana' - >>> clip('banana split', 6) - 'banana' - >>> clip('banana split', 7) - 'banana' - >>> clip('banana split', 8) - 'banana' - >>> clip('banana split', 11) - 'banana' - >>> clip('banana split', 12) - 'banana split' - >>> clip('banana-split', 3) - 'banana-split' - -Jess' tests: - - >>> text = 'The quick brown fox jumps over the lazy dog.' - >>> clip14 = clip(text, max_len=14) - >>> clip14 - 'The quick' - >>> len(clip14) - 9 - >>> clip15 = clip(text, max_len=15) - >>> clip15 - 'The quick brown' - >>> len(clip15) - 15 - -""" - -# tag::CLIP[] -def clip(text, max_len=80): - """Return max_len characters clipped at space if possible""" - text = text.rstrip() - if len(text) <= max_len or ' ' not in text: - return text - end = len(text) - space_at = text.rfind(' ', 0, max_len + 1) - if space_at >= 0: - end = space_at - else: - space_at = text.find(' ', max_len) - if space_at >= 0: - end = space_at - return text[:end].rstrip() -# end::CLIP[] diff --git a/07-1class-func/clip_introspection.rst b/07-1class-func/clip_introspection.rst deleted file mode 100644 index 0b4334d..0000000 --- a/07-1class-func/clip_introspection.rst +++ /dev/null @@ -1,9 +0,0 @@ ->>> from clip import clip ->>> clip.__defaults__ -(80,) ->>> clip.__code__ # doctest: +ELLIPSIS - ->>> clip.__code__.co_varnames -('text', 'max_len', 'end', 'space_at') ->>> clip.__code__.co_argcount -2 diff --git a/07-1class-func/clip_signature.rst b/07-1class-func/clip_signature.rst deleted file mode 100644 index 9bc2dee..0000000 --- a/07-1class-func/clip_signature.rst +++ /dev/null @@ -1,12 +0,0 @@ ->>> from clip import clip ->>> from inspect import signature ->>> sig = signature(clip) ->>> sig - ->>> str(sig) -'(text, max_len=80)' ->>> for name, param in sig.parameters.items(): -... print(param.kind, ':', name, '=', param.default) -... -POSITIONAL_OR_KEYWORD : text = -POSITIONAL_OR_KEYWORD : max_len = 80 diff --git a/08-def-type-hints/RPN_calc/calc.py b/08-def-type-hints/RPN_calc/calc.py deleted file mode 100755 index 967b094..0000000 --- a/08-def-type-hints/RPN_calc/calc.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 - -import sys -from array import array -from typing import Mapping, MutableSequence, Callable, Iterable, Union, Any - - -OPERATORS: Mapping[str, Callable[[float, float], float]] = { - '+': lambda a, b: a + b, - '-': lambda a, b: a - b, - '*': lambda a, b: a * b, - '/': lambda a, b: a / b, - '^': lambda a, b: a ** b, -} - - -Stack = MutableSequence[float] - - -def parse_token(token: str) -> Union[str, float]: - try: - return float(token) - except ValueError: - return token - - -def evaluate(tokens: Iterable[str], stack: Stack) -> None: - for token in tokens: - atom = parse_token(token) - if isinstance(atom, float): - stack.append(atom) - else: # not float, must be operator - op = OPERATORS[atom] - x, y = stack.pop(), stack.pop() - result = op(y, x) - stack.append(result) - - -def display(s: Stack) -> str: - items = (repr(n) for n in s) - return ' │ '.join(items) + ' →' - - -def repl(input_fn: Callable[[Any], str] = input) -> None: - """Read-Eval-Print-Loop""" - - print('Use CTRL+C to quit.', file=sys.stderr) - stack: Stack = array('d') - - while True: - try: - line = input_fn('> ') # Read - except (EOFError, KeyboardInterrupt): - break - try: - evaluate(line.split(), stack) # Eval - except IndexError: - print('*** Not enough arguments.', file=sys.stderr) - except KeyError as exc: - print('*** Unknown operator:', exc.args[0], file=sys.stderr) - print(display(stack)) # Print - - print() - - -if __name__ == '__main__': - repl() diff --git a/08-def-type-hints/RPN_calc/calc_test.py b/08-def-type-hints/RPN_calc/calc_test.py deleted file mode 100644 index c2d144c..0000000 --- a/08-def-type-hints/RPN_calc/calc_test.py +++ /dev/null @@ -1,51 +0,0 @@ -from pytest import mark, approx # type: ignore - -from dialogue import Dialogue # type: ignore - -from calc import evaluate, repl, display, Stack - -TOLERANCE = .0001 - -@mark.parametrize("source, want", [ - ('2', 2), - ('2 3 +', 5), - ('5 3 -', 2), - ('3 5 * 2 +', 17), - ('2 3 4 5 * * *', 120), - ('1.1 1.1 1.1 + +', approx(3.3, TOLERANCE)), - ('100 32 - 5 * 9 /', approx(37.78, TOLERANCE)), -]) -def test_evaluate(source, want) -> None: - stack: Stack = [] - evaluate(source.split(), stack) - assert want == stack[-1] - - -@mark.parametrize("value, want", [ - ([], ' →'), - ([3.], '3.0 →'), - ([3., 4., 5.], '3.0 │ 4.0 │ 5.0 →'), -]) -def test_display(value, want) -> None: - assert want == display(value) - - -@mark.parametrize("session", [ - """ - > 3 - 3.0 → - """, - """ - > 3 5 6 - 3.0 │ 5.0 │ 6.0 → - > * - 3.0 │ 30.0 → - > - - -27.0 → - """, -]) -def test_repl(capsys, session) -> None: - dlg = Dialogue(session) - repl(dlg.fake_input) - captured = capsys.readouterr() - assert dlg.session.strip() == captured.out.strip() diff --git a/08-def-type-hints/callable/variance.py b/08-def-type-hints/callable/variance.py new file mode 100644 index 0000000..fcddbde --- /dev/null +++ b/08-def-type-hints/callable/variance.py @@ -0,0 +1,22 @@ +from collections.abc import Callable + +def update( # <1> + probe: Callable[[], float], # <2> + display: Callable[[float], None] # <3> + ) -> None: + temperature = probe() + # imagine lots of control code here + display(temperature) + +def probe_ok() -> int: # <4> + return 42 + +def display_wrong(temperature: int) -> None: # <5> + print(hex(temperature)) + +update(probe_ok, display_wrong) # type error # <6> + +def display_ok(temperature: complex) -> None: # <7> + print(temperature) + +update(probe_ok, display_ok) # OK # <8> diff --git a/08-def-type-hints/charindex.py b/08-def-type-hints/charindex.py index 36d20e3..c368c37 100644 --- a/08-def-type-hints/charindex.py +++ b/08-def-type-hints/charindex.py @@ -15,7 +15,7 @@ import sys import re import unicodedata -from typing import Dict, Set, Iterator +from collections.abc import Iterator RE_WORD = re.compile(r'\w+') STOP_CODE = sys.maxunicode + 1 @@ -25,8 +25,8 @@ def tokenize(text: str) -> Iterator[str]: # <1> for match in RE_WORD.finditer(text): yield match.group().upper() -def name_index(start: int = 32, end: int = STOP_CODE) -> Dict[str, Set[str]]: - index: Dict[str, Set[str]] = {} # <2> +def name_index(start: int = 32, end: int = STOP_CODE) -> dict[str, set[str]]: + index: dict[str, set[str]] = {} # <2> for char in (chr(i) for i in range(start, end)): if name := unicodedata.name(char, ''): # <3> for word in tokenize(name): diff --git a/08-def-type-hints/colors.py b/08-def-type-hints/colors.py index b57413c..c0d0665 100644 --- a/08-def-type-hints/colors.py +++ b/08-def-type-hints/colors.py @@ -1,4 +1,4 @@ -from typing import Tuple, Mapping +from collections.abc import Mapping NAMES = { 'aqua': 65535, @@ -19,7 +19,7 @@ 'yellow': 16776960, } -def rgb2hex(color=Tuple[int, int, int]) -> str: +def rgb2hex(color: tuple[int, int, int]) -> str: if any(c not in range(256) for c in color): raise ValueError('Color components must be in range(256)') values = (f'{n % 256:02x}' for n in color) @@ -27,7 +27,7 @@ def rgb2hex(color=Tuple[int, int, int]) -> str: HEX_ERROR = "Color must use format '#0099ff', got: {!r}" -def hex2rgb(color=str) -> Tuple[int, int, int]: +def hex2rgb(color: str) -> tuple[int, int, int]: if len(color) != 7 or color[0] != '#': raise ValueError(HEX_ERROR.format(color)) try: diff --git a/08-def-type-hints/columnize.py b/08-def-type-hints/columnize.py index ef995ae..8f4e92b 100644 --- a/08-def-type-hints/columnize.py +++ b/08-def-type-hints/columnize.py @@ -1,9 +1,11 @@ # tag::COLUMNIZE[] -from typing import Sequence, List, Tuple +from collections.abc import Sequence -def columnize(sequence: Sequence[str], num_columns: int = 0) -> List[Tuple[str, ...]]: +def columnize( + sequence: Sequence[str], num_columns: int = 0 +) -> list[tuple[str, ...]]: if num_columns == 0: - num_columns = round(len(sequence) ** .5) + num_columns = round(len(sequence) ** 0.5) num_rows, reminder = divmod(len(sequence), num_columns) num_rows += bool(reminder) return [tuple(sequence[i::num_rows]) for i in range(num_rows)] @@ -11,10 +13,11 @@ def columnize(sequence: Sequence[str], num_columns: int = 0) -> List[Tuple[str, def demo() -> None: - nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India' - ' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo' - ' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu' - ).split() + nato = ( + 'Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India' + ' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo' + ' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu' + ).split() for row in columnize(nato, 4): for word in row: diff --git a/08-def-type-hints/columnize2.py b/08-def-type-hints/columnize2.py deleted file mode 100644 index 2524fd3..0000000 --- a/08-def-type-hints/columnize2.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Sequence, List, Tuple, TypeVar - -T = TypeVar('T') - -def columnize(sequence: Sequence[T], num_columns: int = 0) -> List[Tuple[T, ...]]: - if num_columns == 0: - num_columns = round(len(sequence) ** .5) - num_rows, reminder = divmod(len(sequence), num_columns) - num_rows += bool(reminder) - return [tuple(sequence[i::num_rows]) for i in range(num_rows)] - - -def demo() -> None: - nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India' - ' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo' - ' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu' - ).split() - - for line in columnize(nato): - for word in line: - print(f'{word:15}', end='') - print() - - print() - for length in range(2, 21, 6): - values = list(range(1, length + 1)) - for row in columnize(values): - for cell in row: - print(f'{cell:5}', end='') - print() - print() - - -if __name__ == '__main__': - demo() diff --git a/08-def-type-hints/columnize_alias.py b/08-def-type-hints/columnize_alias.py deleted file mode 100644 index b783469..0000000 --- a/08-def-type-hints/columnize_alias.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Sequence, List, Tuple - -Row = Tuple[str, ...] - -def columnize(sequence: Sequence[str], num_columns: int) -> List[Row]: - if num_columns == 0: - num_columns = round(len(sequence) ** .5) - num_rows, reminder = divmod(len(sequence), num_columns) - num_rows += bool(reminder) - return [tuple(sequence[i::num_rows]) for i in range(num_rows)] - - -def demo() -> None: - nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India' - ' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo' - ' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu' - ).split() - - for row in columnize(nato, 4): - for word in row: - print(f'{word:15}', end='') - print() - - -if __name__ == '__main__': - demo() diff --git a/08-def-type-hints/comparable/top.py b/08-def-type-hints/comparable/top.py index c552890..2851f62 100644 --- a/08-def-type-hints/comparable/top.py +++ b/08-def-type-hints/comparable/top.py @@ -20,11 +20,14 @@ """ # tag::TOP[] -from typing import TypeVar, Iterable, List +from collections.abc import Iterable +from typing import TypeVar + from comparable import SupportsLessThan LT = TypeVar('LT', bound=SupportsLessThan) -def top(series: Iterable[LT], length: int) -> List[LT]: - return sorted(series, reverse=True)[:length] +def top(series: Iterable[LT], length: int) -> list[LT]: + ordered = sorted(series, reverse=True) + return ordered[:length] # end::TOP[] diff --git a/08-def-type-hints/comparable/top_test.py b/08-def-type-hints/comparable/top_test.py index baff36b..c8c39ac 100644 --- a/08-def-type-hints/comparable/top_test.py +++ b/08-def-type-hints/comparable/top_test.py @@ -1,6 +1,11 @@ -from typing import Tuple, List, Iterator, TYPE_CHECKING -import pytest # type: ignore +# tag::TOP_IMPORT[] +from collections.abc import Iterator +from typing import TYPE_CHECKING # <1> + +import pytest + from top import top +# end::TOP_IMPORT[] @pytest.mark.parametrize('series, length, expected', [ ((1, 2, 3), 2, [3, 2]), @@ -8,9 +13,9 @@ ((3, 3, 3), 1, [3]), ]) def test_top( - series: Tuple[float, ...], + series: tuple[float, ...], length: int, - expected: List[float], + expected: list[float], ) -> None: result = top(series, length) assert expected == result @@ -18,13 +23,13 @@ def test_top( # tag::TOP_TEST[] def test_top_tuples() -> None: fruit = 'mango pear apple kiwi banana'.split() - series: Iterator[Tuple[int, str]] = ( + series: Iterator[tuple[int, str]] = ( # <2> (len(s), s) for s in fruit) length = 3 expected = [(6, 'banana'), (5, 'mango'), (5, 'apple')] result = top(series, length) - if TYPE_CHECKING: - reveal_type(series) + if TYPE_CHECKING: # <3> + reveal_type(series) # <4> reveal_type(expected) reveal_type(result) assert result == expected @@ -34,7 +39,7 @@ def test_top_objects_error() -> None: series = [object() for _ in range(4)] if TYPE_CHECKING: reveal_type(series) - with pytest.raises(TypeError) as exc: - top(series, 3) - assert "'<' not supported" in str(exc) + with pytest.raises(TypeError) as excinfo: + top(series, 3) # <5> + assert "'<' not supported" in str(excinfo.value) # end::TOP_TEST[] diff --git a/08-def-type-hints/coordinates/coordinates.py b/08-def-type-hints/coordinates/coordinates.py index d48b390..fb594eb 100644 --- a/08-def-type-hints/coordinates/coordinates.py +++ b/08-def-type-hints/coordinates/coordinates.py @@ -8,10 +8,10 @@ """ # tag::GEOHASH[] -from geolib import geohash as gh # type: ignore +from geolib import geohash as gh # type: ignore # <1> PRECISION = 9 -def geohash(lat_lon: tuple[float, float]) -> str: +def geohash(lat_lon: tuple[float, float]) -> str: # <2> return gh.encode(*lat_lon, PRECISION) # end::GEOHASH[] diff --git a/08-def-type-hints/coordinates/coordinates_named.py b/08-def-type-hints/coordinates/coordinates_named.py index 87cac72..76bd9bb 100644 --- a/08-def-type-hints/coordinates/coordinates_named.py +++ b/08-def-type-hints/coordinates/coordinates_named.py @@ -9,7 +9,7 @@ """ # tag::GEOHASH[] -from typing import Tuple, NamedTuple +from typing import NamedTuple from geolib import geohash as gh # type: ignore @@ -21,16 +21,21 @@ class Coordinate(NamedTuple): def geohash(lat_lon: Coordinate) -> str: return gh.encode(*lat_lon, PRECISION) +# end::GEOHASH[] -def display(lat_lon: Tuple[float, float]) -> str: +# tag::DISPLAY[] +def display(lat_lon: tuple[float, float]) -> str: lat, lon = lat_lon ns = 'N' if lat >= 0 else 'S' ew = 'E' if lon >= 0 else 'W' return f'{abs(lat):0.1f}°{ns}, {abs(lon):0.1f}°{ew}' - -# end::GEOHASH[] +# end::DISPLAY[] def demo(): shanghai = 31.2304, 121.4737 + print(display(shanghai)) s = geohash(shanghai) print(s) + +if __name__ == '__main__': + demo() diff --git a/08-def-type-hints/coordinates/requirements.txt b/08-def-type-hints/coordinates/requirements.txt index fb11094..7b7b424 100644 --- a/08-def-type-hints/coordinates/requirements.txt +++ b/08-def-type-hints/coordinates/requirements.txt @@ -1,2 +1,2 @@ geolib==1.0.7 -future==0.18.2 +future==0.18.3 diff --git a/08-def-type-hints/list.py b/08-def-type-hints/list.py deleted file mode 100644 index 6d122e7..0000000 --- a/08-def-type-hints/list.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import List, Tuple - -def tokenize(text: str) -> List[str]: - return text.upper().split() - -l: List[str] = [] -l.append(1) -print(l) - -t: Tuple[str, float] = ('São Paulo', 12_176_866) diff --git a/08-def-type-hints/messages/hints_1/messages.py b/08-def-type-hints/messages/hints_1/messages.py index c00e706..59b432e 100644 --- a/08-def-type-hints/messages/hints_1/messages.py +++ b/08-def-type-hints/messages/hints_1/messages.py @@ -5,16 +5,15 @@ >>> show_count(1, 'bird') '1 bird' >>> show_count(0, 'bird') -'no bird' +'no birds' # end::SHOW_COUNT_DOCTEST[] """ # tag::SHOW_COUNT[] def show_count(count: int, word: str) -> str: - if count == 0: - return f'no {word}' - elif count == 1: - return f'{count} {word}' - return f'{count} {word}s' + if count == 1: + return f'1 {word}' + count_str = str(count) if count else 'no' + return f'{count_str} {word}s' # end::SHOW_COUNT[] diff --git a/08-def-type-hints/messages/hints_1/messages_test.py b/08-def-type-hints/messages/hints_1/messages_test.py index 2a16f25..3688da1 100644 --- a/08-def-type-hints/messages/hints_1/messages_test.py +++ b/08-def-type-hints/messages/hints_1/messages_test.py @@ -7,11 +7,11 @@ (1, '1 part'), (2, '2 parts'), ]) -def test_show_count(qty, expected): +def test_show_count(qty: int, expected: str) -> None: got = show_count(qty, 'part') assert got == expected def test_show_count_zero(): got = show_count(0, 'part') - assert got == 'no part' + assert got == 'no parts' diff --git a/08-def-type-hints/messages/hints_2/messages.py b/08-def-type-hints/messages/hints_2/messages.py index c43d85f..fd2a331 100644 --- a/08-def-type-hints/messages/hints_2/messages.py +++ b/08-def-type-hints/messages/hints_2/messages.py @@ -4,21 +4,22 @@ >>> show_count(1, 'bird') '1 bird' >>> show_count(0, 'bird') -'no bird' +'no birds' >>> show_count(3, 'virus', 'viruses') '3 viruses' +>>> show_count(1, 'virus', 'viruses') +'1 virus' +>>> show_count(0, 'virus', 'viruses') +'no viruses' """ # tag::SHOW_COUNT[] def show_count(count: int, singular: str, plural: str = '') -> str: - if count == 0: - return f'no {singular}' - elif count == 1: + if count == 1: return f'1 {singular}' - else: - if plural: - return f'{count} {plural}' - else: - return f'{count} {singular}s' + count_str = str(count) if count else 'no' + if not plural: + plural = singular + 's' + return f'{count_str} {plural}' # end::SHOW_COUNT[] diff --git a/08-def-type-hints/messages/hints_2/messages_test.py b/08-def-type-hints/messages/hints_2/messages_test.py index f7b2fe1..2678d9b 100644 --- a/08-def-type-hints/messages/hints_2/messages_test.py +++ b/08-def-type-hints/messages/hints_2/messages_test.py @@ -1,4 +1,4 @@ -from pytest import mark # type: ignore +from pytest import mark from messages import show_count @@ -6,9 +6,9 @@ @mark.parametrize('qty, expected', [ (1, '1 part'), (2, '2 parts'), - (0, 'no part'), + (0, 'no parts'), ]) -def test_show_count(qty, expected): +def test_show_count(qty: int, expected: str) -> None: got = show_count(qty, 'part') assert got == expected @@ -17,9 +17,9 @@ def test_show_count(qty, expected): @mark.parametrize('qty, expected', [ (1, '1 child'), (2, '2 children'), - (0, 'no child'), + (0, 'no children'), ]) -def test_irregular(qty, expected) -> None: +def test_irregular(qty: int, expected: str) -> None: got = show_count(qty, 'child', 'children') assert got == expected # end::TEST_IRREGULAR[] diff --git a/08-def-type-hints/messages/no_hints/messages.py b/08-def-type-hints/messages/no_hints/messages.py index d7898c5..df037e7 100644 --- a/08-def-type-hints/messages/no_hints/messages.py +++ b/08-def-type-hints/messages/no_hints/messages.py @@ -5,16 +5,15 @@ >>> show_count(1, 'bird') '1 bird' >>> show_count(0, 'bird') -'no bird' +'no birds' # end::SHOW_COUNT_DOCTEST[] """ # tag::SHOW_COUNT[] def show_count(count, word): - if count == 0: - return f'no {word}' - elif count == 1: - return f'{count} {word}' - return f'{count} {word}s' + if count == 1: + return f'1 {word}' + count_str = str(count) if count else 'no' + return f'{count_str} {word}s' # end::SHOW_COUNT[] diff --git a/08-def-type-hints/messages/no_hints/messages_test.py b/08-def-type-hints/messages/no_hints/messages_test.py index a8ec100..09532d3 100644 --- a/08-def-type-hints/messages/no_hints/messages_test.py +++ b/08-def-type-hints/messages/no_hints/messages_test.py @@ -12,4 +12,4 @@ def test_show_count(qty, expected): def test_show_count_zero(): got = show_count(0, 'part') - assert got == 'no part' + assert got == 'no parts' diff --git a/08-def-type-hints/mode/mode_T.py b/08-def-type-hints/mode/mode_T.py deleted file mode 100644 index cf88bbb..0000000 --- a/08-def-type-hints/mode/mode_T.py +++ /dev/null @@ -1,26 +0,0 @@ -from collections import Counter -from typing import Iterable, TypeVar - -T = TypeVar('T') - -def mode(data: Iterable[T]) -> T: - data = iter(data) - pairs = Counter(data).most_common(1) - if len(pairs) == 0: - raise ValueError('no mode for empty data') - return pairs[0][0] - - -def demo() -> None: - from typing import TYPE_CHECKING - pop: list[set] = [set(), set()] - m = mode(pop) - if TYPE_CHECKING: - reveal_type(pop) - reveal_type(m) - print(pop) - print(repr(m), type(m)) - - -if __name__ == '__main__': - demo() diff --git a/08-def-type-hints/mode/mode_float.py b/08-def-type-hints/mode/mode_float.py index 0684202..79308be 100644 --- a/08-def-type-hints/mode/mode_float.py +++ b/08-def-type-hints/mode/mode_float.py @@ -1,6 +1,6 @@ # tag::MODE_FLOAT[] from collections import Counter -from typing import Iterable +from collections.abc import Iterable def mode(data: Iterable[float]) -> float: pairs = Counter(data).most_common(1) @@ -20,4 +20,4 @@ def demo() -> None: print(repr(m), type(m)) if __name__ == '__main__': - demo() \ No newline at end of file + demo() diff --git a/08-def-type-hints/mode/mode_hashable.py b/08-def-type-hints/mode/mode_hashable.py index aa7f313..57c3a30 100644 --- a/08-def-type-hints/mode/mode_hashable.py +++ b/08-def-type-hints/mode/mode_hashable.py @@ -1,6 +1,7 @@ # tag::MODE_HASHABLE_T[] from collections import Counter -from typing import Iterable, Hashable, TypeVar +from collections.abc import Iterable, Hashable +from typing import TypeVar HashableT = TypeVar('HashableT', bound=Hashable) diff --git a/08-def-type-hints/mode/mode_hashable_wrong.py b/08-def-type-hints/mode/mode_hashable_wrong.py deleted file mode 100644 index ff770a7..0000000 --- a/08-def-type-hints/mode/mode_hashable_wrong.py +++ /dev/null @@ -1,24 +0,0 @@ -# tag::MODE_FLOAT[] -from collections import Counter -from typing import Iterable, Hashable - -def mode(data: Iterable[Hashable]) -> Hashable: - data = iter(data) - pairs = Counter(data).most_common(1) - if len(pairs) == 0: - raise ValueError('no mode for empty data') - return pairs[0][0] -# end::MODE_FLOAT[] - -def demo() -> None: - import typing - pop = 'abracadabra' - m = mode(pop) - if typing.TYPE_CHECKING: - reveal_type(pop) - reveal_type(m) - print(pop) - print(m.upper(), type(m)) - -if __name__ == '__main__': - demo() \ No newline at end of file diff --git a/08-def-type-hints/mode/mode_number.py b/08-def-type-hints/mode/mode_number.py deleted file mode 100644 index 999387f..0000000 --- a/08-def-type-hints/mode/mode_number.py +++ /dev/null @@ -1,26 +0,0 @@ -from collections import Counter -from typing import Iterable, TypeVar -from decimal import Decimal -from fractions import Fraction - -NumberT = TypeVar('NumberT', float, Decimal, Fraction) - -def mode(data: Iterable[NumberT]) -> NumberT: - pairs = Counter(data).most_common(1) - if len(pairs) == 0: - raise ValueError('no mode for empty data') - return pairs[0][0] - - -def demo() -> None: - from typing import TYPE_CHECKING - pop = [Fraction(1, 2), Fraction(1, 3), Fraction(1, 4), Fraction(1, 2)] - m = mode(pop) - if TYPE_CHECKING: - reveal_type(pop) - reveal_type(m) - print(pop) - print(repr(m), type(m)) - -if __name__ == '__main__': - demo() \ No newline at end of file diff --git a/08-def-type-hints/messages/hints_1/mypy.ini b/08-def-type-hints/mypy.ini similarity index 50% rename from 08-def-type-hints/messages/hints_1/mypy.ini rename to 08-def-type-hints/mypy.ini index f658867..ab42819 100644 --- a/08-def-type-hints/messages/hints_1/mypy.ini +++ b/08-def-type-hints/mypy.ini @@ -1,6 +1,4 @@ [mypy] -python_version = 3.8 +python_version = 3.9 warn_unused_configs = True disallow_incomplete_defs = True -[mypy-pytest] -ignore_missing_imports = True diff --git a/08-def-type-hints/passdrill.py b/08-def-type-hints/passdrill.py deleted file mode 100755 index 83b1be1..0000000 --- a/08-def-type-hints/passdrill.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 - -"""passdrill: typing drills for practicing passphrases -""" - -import os -import sys -from base64 import b64encode, b64decode -from getpass import getpass -from hashlib import scrypt -from typing import Sequence, Tuple - -HASH_FILENAME = 'passdrill.hash' -HELP = 'Use -s to save passphrase hash for practice.' - - -def prompt() -> str: - print('WARNING: the passphrase WILL BE SHOWN so that you can check it!') - confirmed = '' - while confirmed != 'y': - passphrase = input('Type passphrase to hash (it will be echoed): ') - if passphrase in ('', 'q'): - print('ERROR: the passphrase cannot be empty or "q".') - continue - print(f'Passphrase to be hashed -> {passphrase}') - confirmed = input('Confirm (y/n): ').lower() - return passphrase - - -def crypto_hash(salt: bytes, passphrase: str) -> bytes: - octets = passphrase.encode('utf-8') - # Recommended parameters for interactive logins as of 2017: - # N=32768, r=8 and p=1 (https://godoc.org/golang.org/x/crypto/scrypt) - return scrypt(octets, salt=salt, n=32768, r=8, p=1, maxmem=2 ** 26) - - -def build_hash(passphrase: str) -> bytes: - salt = os.urandom(32) - payload = crypto_hash(salt, passphrase) - return b64encode(salt) + b':' + b64encode(payload) - - -def save_hash() -> None: - salted_hash = build_hash(prompt()) - with open(HASH_FILENAME, 'wb') as fp: - fp.write(salted_hash) - print(f'Passphrase hash saved to {HASH_FILENAME}') - - -def load_hash() -> Tuple[bytes, bytes]: - try: - with open(HASH_FILENAME, 'rb') as fp: - salted_hash = fp.read() - except FileNotFoundError: - print('ERROR: passphrase hash file not found.', HELP) - # "standard" exit status codes: - # https://stackoverflow.com/questions/1101957/are-there-any-standard-exit-status-codes-in-linux/40484670#40484670 - sys.exit(74) # input/output error - - salt, stored_hash = salted_hash.split(b':') - return b64decode(salt), b64decode(stored_hash) - - -def practice() -> None: - salt, stored_hash = load_hash() - print('Type q to end practice.') - turn = 0 - correct = 0 - while True: - turn += 1 - response = getpass(f'{turn}:') - if response == '': - print('Type q to quit.') - turn -= 1 # don't count this response - continue - elif response == 'q': - turn -= 1 # don't count this response - break - if crypto_hash(salt, response) == stored_hash: - correct += 1 - answer = 'OK' - else: - answer = 'wrong' - print(f' {answer}\thits={correct}\tmisses={turn-correct}') - - if turn: - print(f'\n{turn} turns. {correct / turn:.1%} correct.') - - -def main(argv: Sequence[str]) -> None: - if len(argv) < 2: - practice() - elif len(argv) == 2 and argv[1] == '-s': - save_hash() - else: - print('ERROR: invalid argument.', HELP) - sys.exit(2) # command line usage error - - -if __name__ == '__main__': - main(sys.argv) diff --git a/08-def-type-hints/replacer.py b/08-def-type-hints/replacer.py index 73ec77d..553a927 100644 --- a/08-def-type-hints/replacer.py +++ b/08-def-type-hints/replacer.py @@ -13,9 +13,9 @@ """ # tag::ZIP_REPLACE[] -from typing import Iterable, Tuple +from collections.abc import Iterable -FromTo = Tuple[str, str] # <1> +FromTo = tuple[str, str] # <1> def zip_replace(text: str, changes: Iterable[FromTo]) -> str: # <2> for from_, to in changes: diff --git a/08-def-type-hints/replacer2.py b/08-def-type-hints/replacer2.py deleted file mode 100644 index 33cdaeb..0000000 --- a/08-def-type-hints/replacer2.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -``zip_replace`` replaces multiple calls to ``str.replace``:: - - >>> changes = [ - ... ('(', ' ( '), - ... (')', ' ) '), - ... (' ', ' '), - ... ] - >>> expr = '(+ 2 (* 3 7))' - >>> zip_replace(expr, changes) - ' ( + 2 ( * 3 7 ) ) ' - -""" - -from typing import Iterable, NamedTuple - - -class FromTo(NamedTuple): - from_: str - to: str - - -def zip_replace(text: str, changes: Iterable[FromTo], count: int = -1) -> str: - for from_, to in changes: - text = text.replace(from_, to, count) - return text - - -def demo() -> None: - import doctest - failed, count = doctest.testmod() - print(f'{count-failed} of {count} doctests OK') - l33t = [FromTo(*p) for p in 'a4 e3 i1 o0'.split()] - text = 'mad skilled noob powned leet' - print(zip_replace(text, l33t)) - - -if __name__ == '__main__': - demo() diff --git a/08-def-type-hints/reveal_array.py b/08-def-type-hints/reveal_array.py deleted file mode 100644 index 149c85a..0000000 --- a/08-def-type-hints/reveal_array.py +++ /dev/null @@ -1,8 +0,0 @@ -from array import array -from typing import MutableSequence - -a = array('d') -reveal_type(a) -b: MutableSequence[float] = array('b') -reveal_type(b) - diff --git a/08-def-type-hints/sample.py b/08-def-type-hints/sample.py index 0e7e2e9..04b0319 100644 --- a/08-def-type-hints/sample.py +++ b/08-def-type-hints/sample.py @@ -1,10 +1,11 @@ # tag::SAMPLE[] +from collections.abc import Sequence from random import shuffle -from typing import Sequence, List, TypeVar +from typing import TypeVar T = TypeVar('T') -def sample(population: Sequence[T], size: int) -> List[T]: +def sample(population: Sequence[T], size: int) -> list[T]: if size < 1: raise ValueError('size must be >= 1') result = list(population) diff --git a/08-def-type-hints/typevar_bounded.py b/08-def-type-hints/typevar_bounded.py new file mode 100644 index 0000000..5c2adf0 --- /dev/null +++ b/08-def-type-hints/typevar_bounded.py @@ -0,0 +1,11 @@ +from typing import TypeVar, TYPE_CHECKING + +BT = TypeVar('BT', bound=float) + +def triple2(a: BT) -> BT: + return a * 3 + +res2 = triple2(2) + +if TYPE_CHECKING: + reveal_type(res2) diff --git a/08-def-type-hints/typevars_constrained.py b/08-def-type-hints/typevars_constrained.py index 2bfea13..8fb8d8e 100644 --- a/08-def-type-hints/typevars_constrained.py +++ b/08-def-type-hints/typevars_constrained.py @@ -7,7 +7,7 @@ def triple1(a: RT) -> RT: return a * 3 -res1 = triple1(1, 2) +res1 = triple1(2) if TYPE_CHECKING: reveal_type(res1) @@ -19,7 +19,7 @@ def triple1(a: RT) -> RT: def triple2(a: BT) -> BT: return a * 3 -res2 = triple2(1, 2) +res2 = triple2(2) if TYPE_CHECKING: reveal_type(res2) diff --git a/09-closure-deco/clockdeco.py b/09-closure-deco/clock/clockdeco.py similarity index 100% rename from 09-closure-deco/clockdeco.py rename to 09-closure-deco/clock/clockdeco.py diff --git a/09-closure-deco/clockdeco0.py b/09-closure-deco/clock/clockdeco0.py similarity index 100% rename from 09-closure-deco/clockdeco0.py rename to 09-closure-deco/clock/clockdeco0.py diff --git a/09-closure-deco/clockdeco_cls.py b/09-closure-deco/clock/clockdeco_cls.py similarity index 100% rename from 09-closure-deco/clockdeco_cls.py rename to 09-closure-deco/clock/clockdeco_cls.py diff --git a/09-closure-deco/clockdeco_demo.py b/09-closure-deco/clock/clockdeco_demo.py similarity index 90% rename from 09-closure-deco/clockdeco_demo.py rename to 09-closure-deco/clock/clockdeco_demo.py index 121b52f..bd41b5c 100644 --- a/09-closure-deco/clockdeco_demo.py +++ b/09-closure-deco/clock/clockdeco_demo.py @@ -1,17 +1,14 @@ import time -from clockdeco import clock - +from clockdeco0 import clock @clock def snooze(seconds): time.sleep(seconds) - @clock def factorial(n): return 1 if n < 2 else n*factorial(n-1) - if __name__ == '__main__': print('*' * 40, 'Calling snooze(.123)') snooze(.123) diff --git a/09-closure-deco/clockdeco_param.py b/09-closure-deco/clock/clockdeco_param.py similarity index 100% rename from 09-closure-deco/clockdeco_param.py rename to 09-closure-deco/clock/clockdeco_param.py diff --git a/09-closure-deco/clockdeco_param_demo1.py b/09-closure-deco/clock/clockdeco_param_demo1.py similarity index 100% rename from 09-closure-deco/clockdeco_param_demo1.py rename to 09-closure-deco/clock/clockdeco_param_demo1.py diff --git a/09-closure-deco/clockdeco_param_demo2.py b/09-closure-deco/clock/clockdeco_param_demo2.py similarity index 100% rename from 09-closure-deco/clockdeco_param_demo2.py rename to 09-closure-deco/clock/clockdeco_param_demo2.py diff --git a/10-dp-1class-func/classic_strategy.py b/10-dp-1class-func/classic_strategy.py index dc72e19..d796a79 100644 --- a/10-dp-1class-func/classic_strategy.py +++ b/10-dp-1class-func/classic_strategy.py @@ -6,20 +6,20 @@ >>> joe = Customer('John Doe', 0) # <1> >>> ann = Customer('Ann Smith', 1100) - >>> cart = [LineItem('banana', 4, .5), # <2> - ... LineItem('apple', 10, 1.5), - ... LineItem('watermelon', 5, 5.0)] + >>> cart = (LineItem('banana', 4, Decimal('.5')), # <2> + ... LineItem('apple', 10, Decimal('1.5')), + ... LineItem('watermelon', 5, Decimal(5))) >>> Order(joe, cart, FidelityPromo()) # <3> >>> Order(ann, cart, FidelityPromo()) # <4> - >>> banana_cart = [LineItem('banana', 30, .5), # <5> - ... LineItem('apple', 10, 1.5)] + >>> banana_cart = (LineItem('banana', 30, Decimal('.5')), # <5> + ... LineItem('apple', 10, Decimal('1.5'))) >>> Order(joe, banana_cart, BulkItemPromo()) # <6> - >>> big_cart = [LineItem(str(item_code), 1, 1.0) # <7> - ... for item_code in range(10)] - >>> Order(joe, big_cart, LargeOrderPromo()) # <8> + >>> long_cart = tuple(LineItem(str(sku), 1, Decimal(1)) # <7> + ... for sku in range(10)) + >>> Order(joe, long_cart, LargeOrderPromo()) # <8> >>> Order(joe, cart, LargeOrderPromo()) @@ -29,44 +29,37 @@ # tag::CLASSIC_STRATEGY[] from abc import ABC, abstractmethod -import typing -from typing import Sequence, Optional +from collections.abc import Sequence +from decimal import Decimal +from typing import NamedTuple, Optional -class Customer(typing.NamedTuple): +class Customer(NamedTuple): name: str fidelity: int -class LineItem: - def __init__(self, product: str, quantity: int, price: float): - self.product = product - self.quantity = quantity - self.price = price +class LineItem(NamedTuple): + product: str + quantity: int + price: Decimal - def total(self): + def total(self) -> Decimal: return self.price * self.quantity -class Order: # the Context - def __init__( - self, - customer: Customer, - cart: Sequence[LineItem], - promotion: Optional['Promotion'] = None, - ): - self.customer = customer - self.cart = list(cart) - self.promotion = promotion +class Order(NamedTuple): # the Context + customer: Customer + cart: Sequence[LineItem] + promotion: Optional['Promotion'] = None - def total(self) -> float: - if not hasattr(self, '__total'): - self.__total = sum(item.total() for item in self.cart) - return self.__total + def total(self) -> Decimal: + totals = (item.total() for item in self.cart) + return sum(totals, start=Decimal(0)) - def due(self) -> float: + def due(self) -> Decimal: if self.promotion is None: - discount = 0.0 + discount = Decimal(0) else: discount = self.promotion.discount(self) return self.total() - discount @@ -77,36 +70,37 @@ def __repr__(self): class Promotion(ABC): # the Strategy: an abstract base class @abstractmethod - def discount(self, order: Order) -> float: + def discount(self, order: Order) -> Decimal: """Return discount as a positive dollar amount""" class FidelityPromo(Promotion): # first Concrete Strategy """5% discount for customers with 1000 or more fidelity points""" - def discount(self, order: Order) -> float: - return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0 + def discount(self, order: Order) -> Decimal: + rate = Decimal('0.05') + if order.customer.fidelity >= 1000: + return order.total() * rate + return Decimal(0) class BulkItemPromo(Promotion): # second Concrete Strategy """10% discount for each LineItem with 20 or more units""" - def discount(self, order: Order) -> float: - discount = 0 + def discount(self, order: Order) -> Decimal: + discount = Decimal(0) for item in order.cart: if item.quantity >= 20: - discount += item.total() * 0.1 + discount += item.total() * Decimal('0.1') return discount class LargeOrderPromo(Promotion): # third Concrete Strategy """7% discount for orders with 10 or more distinct items""" - def discount(self, order: Order) -> float: + def discount(self, order: Order) -> Decimal: distinct_items = {item.product for item in order.cart} if len(distinct_items) >= 10: - return order.total() * 0.07 - return 0 - - + return order.total() * Decimal('0.07') + return Decimal(0) # end::CLASSIC_STRATEGY[] diff --git a/10-dp-1class-func/classic_strategy_test.py b/10-dp-1class-func/classic_strategy_test.py index 8735811..143cd0c 100644 --- a/10-dp-1class-func/classic_strategy_test.py +++ b/10-dp-1class-func/classic_strategy_test.py @@ -1,4 +1,4 @@ -from typing import List +from decimal import Decimal import pytest # type: ignore @@ -17,47 +17,49 @@ def customer_fidelity_1100() -> Customer: @pytest.fixture -def cart_plain() -> List[LineItem]: - return [ - LineItem('banana', 4, 0.5), - LineItem('apple', 10, 1.5), - LineItem('watermelon', 5, 5.0), - ] +def cart_plain() -> tuple[LineItem, ...]: + return ( + LineItem('banana', 4, Decimal('0.5')), + LineItem('apple', 10, Decimal('1.5')), + LineItem('watermelon', 5, Decimal('5.0')), + ) def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None: order = Order(customer_fidelity_0, cart_plain, FidelityPromo()) - assert order.total() == 42.0 - assert order.due() == 42.0 + assert order.total() == 42 + assert order.due() == 42 def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None: order = Order(customer_fidelity_1100, cart_plain, FidelityPromo()) - assert order.total() == 42.0 - assert order.due() == 39.9 + assert order.total() == 42 + assert order.due() == Decimal('39.9') def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None: order = Order(customer_fidelity_0, cart_plain, BulkItemPromo()) - assert order.total() == 42.0 - assert order.due() == 42.0 + assert order.total() == 42 + assert order.due() == 42 def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None: - cart = [LineItem('banana', 30, 0.5), LineItem('apple', 10, 1.5)] + cart = [LineItem('banana', 30, Decimal('0.5')), + LineItem('apple', 10, Decimal('1.5'))] order = Order(customer_fidelity_0, cart, BulkItemPromo()) - assert order.total() == 30.0 - assert order.due() == 28.5 + assert order.total() == 30 + assert order.due() == Decimal('28.5') def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None: order = Order(customer_fidelity_0, cart_plain, LargeOrderPromo()) - assert order.total() == 42.0 - assert order.due() == 42.0 + assert order.total() == 42 + assert order.due() == 42 def test_large_order_promo_with_discount(customer_fidelity_0) -> None: - cart = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)] + cart = [LineItem(str(item_code), 1, Decimal(1)) + for item_code in range(10)] order = Order(customer_fidelity_0, cart, LargeOrderPromo()) - assert order.total() == 10.0 - assert order.due() == 9.3 + assert order.total() == 10 + assert order.due() == Decimal('9.3') diff --git a/10-dp-1class-func/monkeytype/classic_strategy.py b/10-dp-1class-func/monkeytype/classic_strategy.py index 0057d96..670c4cb 100644 --- a/10-dp-1class-func/monkeytype/classic_strategy.py +++ b/10-dp-1class-func/monkeytype/classic_strategy.py @@ -17,9 +17,9 @@ ... LineItem('apple', 10, 1.5)] >>> Order(joe, banana_cart, BulkItemPromo()) # <6> - >>> long_order = [LineItem(str(item_code), 1, 1.0) # <7> + >>> long_cart = [LineItem(str(item_code), 1, 1.0) # <7> ... for item_code in range(10)] - >>> Order(joe, long_order, LargeOrderPromo()) # <8> + >>> Order(joe, long_cart, LargeOrderPromo()) # <8> >>> Order(joe, cart, LargeOrderPromo()) diff --git a/10-dp-1class-func/promotions.py b/10-dp-1class-func/promotions.py index 0f8a823..ee64b20 100644 --- a/10-dp-1class-func/promotions.py +++ b/10-dp-1class-func/promotions.py @@ -1,20 +1,25 @@ -def fidelity_promo(order): +from decimal import Decimal +from strategy import Order + +def fidelity_promo(order: Order) -> Decimal: # <3> """5% discount for customers with 1000 or more fidelity points""" - return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0 + if order.customer.fidelity >= 1000: + return order.total() * Decimal('0.05') + return Decimal(0) -def bulk_item_promo(order): +def bulk_item_promo(order: Order) -> Decimal: """10% discount for each LineItem with 20 or more units""" - discount = 0 + discount = Decimal(0) for item in order.cart: if item.quantity >= 20: - discount += item.total() * 0.1 + discount += item.total() * Decimal('0.1') return discount -def large_order_promo(order): +def large_order_promo(order: Order) -> Decimal: """7% discount for orders with 10 or more distinct items""" distinct_items = {item.product for item in order.cart} if len(distinct_items) >= 10: - return order.total() * 0.07 - return 0 + return order.total() * Decimal('0.07') + return Decimal(0) diff --git a/10-dp-1class-func/pytypes/classic_strategy.py b/10-dp-1class-func/pytypes/classic_strategy.py index d929ffa..710fd26 100644 --- a/10-dp-1class-func/pytypes/classic_strategy.py +++ b/10-dp-1class-func/pytypes/classic_strategy.py @@ -17,9 +17,9 @@ ... LineItem('apple', 10, 1.5)] >>> Order(joe, banana_cart, BulkItemPromo()) # <6> - >>> long_order = [LineItem(str(item_code), 1, 1.0) # <7> + >>> long_cart = [LineItem(str(item_code), 1, 1.0) # <7> ... for item_code in range(10)] - >>> Order(joe, long_order, LargeOrderPromo()) # <8> + >>> Order(joe, long_cart, LargeOrderPromo()) # <8> >>> Order(joe, cart, LargeOrderPromo()) diff --git a/10-dp-1class-func/requirements.txt b/10-dp-1class-func/requirements.txt index 1826922..4c4f0e1 100644 --- a/10-dp-1class-func/requirements.txt +++ b/10-dp-1class-func/requirements.txt @@ -1,13 +1,2 @@ -mypy==0.770 -mypy-extensions==0.4.3 -typed-ast==1.4.1 -typing-extensions==3.7.4.1 -attrs==19.3.0 -more-itertools==8.2.0 -packaging==20.3 -pluggy==0.13.1 -py==1.10.0 -pyparsing==2.4.6 -pytest==5.4.1 -six==1.14.0 -wcwidth==0.1.9 +mypy==0.910 +pytest==6.2.4 diff --git a/10-dp-1class-func/strategy.py b/10-dp-1class-func/strategy.py index 2ab49d1..89a93ba 100644 --- a/10-dp-1class-func/strategy.py +++ b/10-dp-1class-func/strategy.py @@ -6,20 +6,20 @@ >>> joe = Customer('John Doe', 0) # <1> >>> ann = Customer('Ann Smith', 1100) - >>> cart = [LineItem('banana', 4, .5), - ... LineItem('apple', 10, 1.5), - ... LineItem('watermelon', 5, 5.0)] + >>> cart = [LineItem('banana', 4, Decimal('.5')), + ... LineItem('apple', 10, Decimal('1.5')), + ... LineItem('watermelon', 5, Decimal(5))] >>> Order(joe, cart, fidelity_promo) # <2> >>> Order(ann, cart, fidelity_promo) - >>> banana_cart = [LineItem('banana', 30, .5), - ... LineItem('apple', 10, 1.5)] + >>> banana_cart = [LineItem('banana', 30, Decimal('.5')), + ... LineItem('apple', 10, Decimal('1.5'))] >>> Order(joe, banana_cart, bulk_item_promo) # <3> - >>> big_cart = [LineItem(str(item_code), 1, 1.0) + >>> long_cart = [LineItem(str(item_code), 1, Decimal(1)) ... for item_code in range(10)] - >>> Order(joe, big_cart, large_order_promo) + >>> Order(joe, long_cart, large_order_promo) >>> Order(joe, cart, large_order_promo) @@ -28,75 +28,71 @@ """ # tag::STRATEGY[] -import typing -from typing import Sequence, Optional, Callable +from collections.abc import Sequence +from dataclasses import dataclass +from decimal import Decimal +from typing import Optional, Callable, NamedTuple -class Customer(typing.NamedTuple): +class Customer(NamedTuple): name: str fidelity: int -class LineItem: - def __init__(self, product: str, quantity: int, price: float): - self.product = product - self.quantity = quantity - self.price = price +class LineItem(NamedTuple): + product: str + quantity: int + price: Decimal def total(self): return self.price * self.quantity - +@dataclass(frozen=True) class Order: # the Context - def __init__( - self, - customer: Customer, - cart: Sequence[LineItem], - promotion: Optional[Callable[['Order'], float]] = None, - ) -> None: - self.customer = customer - self.cart = list(cart) - self.promotion = promotion - - def total(self) -> float: - if not hasattr(self, '__total'): - self.__total = sum(item.total() for item in self.cart) - return self.__total - - def due(self) -> float: + customer: Customer + cart: Sequence[LineItem] + promotion: Optional[Callable[['Order'], Decimal]] = None # <1> + + def total(self) -> Decimal: + totals = (item.total() for item in self.cart) + return sum(totals, start=Decimal(0)) + + def due(self) -> Decimal: if self.promotion is None: - discount = 0.0 + discount = Decimal(0) else: - discount = self.promotion(self) # <1> + discount = self.promotion(self) # <2> return self.total() - discount def __repr__(self): return f'' -# <2> +# <3> -def fidelity_promo(order: Order) -> float: # <3> +def fidelity_promo(order: Order) -> Decimal: # <4> """5% discount for customers with 1000 or more fidelity points""" - return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0 + if order.customer.fidelity >= 1000: + return order.total() * Decimal('0.05') + return Decimal(0) -def bulk_item_promo(order: Order): +def bulk_item_promo(order: Order) -> Decimal: """10% discount for each LineItem with 20 or more units""" - discount = 0 + discount = Decimal(0) for item in order.cart: if item.quantity >= 20: - discount += item.total() * 0.1 + discount += item.total() * Decimal('0.1') return discount -def large_order_promo(order: Order): +def large_order_promo(order: Order) -> Decimal: """7% discount for orders with 10 or more distinct items""" distinct_items = {item.product for item in order.cart} if len(distinct_items) >= 10: - return order.total() * 0.07 - return 0 + return order.total() * Decimal('0.07') + return Decimal(0) # end::STRATEGY[] diff --git a/10-dp-1class-func/strategy_best.py b/10-dp-1class-func/strategy_best.py index a7c4d74..68cab48 100644 --- a/10-dp-1class-func/strategy_best.py +++ b/10-dp-1class-func/strategy_best.py @@ -3,19 +3,20 @@ # selecting best promotion from static list of functions """ + >>> from strategy import Customer, LineItem >>> joe = Customer('John Doe', 0) >>> ann = Customer('Ann Smith', 1100) - >>> cart = [LineItem('banana', 4, .5), - ... LineItem('apple', 10, 1.5), - ... LineItem('watermelon', 5, 5.0)] - >>> banana_cart = [LineItem('banana', 30, .5), - ... LineItem('apple', 10, 1.5)] - >>> big_cart = [LineItem(str(item_code), 1, 1.0) + >>> cart = [LineItem('banana', 4, Decimal('.5')), + ... LineItem('apple', 10, Decimal('1.5')), + ... LineItem('watermelon', 5, Decimal(5))] + >>> banana_cart = [LineItem('banana', 30, Decimal('.5')), + ... LineItem('apple', 10, Decimal('1.5'))] + >>> long_cart = [LineItem(str(item_code), 1, Decimal(1)) ... for item_code in range(10)] # tag::STRATEGY_BEST_TESTS[] - >>> Order(joe, big_cart, best_promo) # <1> + >>> Order(joe, long_cart, best_promo) # <1> >>> Order(joe, banana_cart, best_promo) # <2> @@ -25,7 +26,9 @@ # end::STRATEGY_BEST_TESTS[] """ -from strategy import Customer, LineItem, Order +from decimal import Decimal + +from strategy import Order from strategy import fidelity_promo, bulk_item_promo, large_order_promo # tag::STRATEGY_BEST[] @@ -33,9 +36,8 @@ promos = [fidelity_promo, bulk_item_promo, large_order_promo] # <1> -def best_promo(order) -> float: # <2> - """Select best discount available - """ +def best_promo(order: Order) -> Decimal: # <2> + """Compute the best discount available""" return max(promo(order) for promo in promos) # <3> diff --git a/10-dp-1class-func/strategy_best2.py b/10-dp-1class-func/strategy_best2.py index 62a993e..1c6ad9c 100644 --- a/10-dp-1class-func/strategy_best2.py +++ b/10-dp-1class-func/strategy_best2.py @@ -3,29 +3,31 @@ # selecting best promotion from current module globals """ + >>> from decimal import Decimal + >>> from strategy import Customer, LineItem, Order >>> joe = Customer('John Doe', 0) >>> ann = Customer('Ann Smith', 1100) - >>> cart = [LineItem('banana', 4, .5), - ... LineItem('apple', 10, 1.5), - ... LineItem('watermelon', 5, 5.0)] + >>> cart = [LineItem('banana', 4, Decimal('.5')), + ... LineItem('apple', 10, Decimal('1.5')), + ... LineItem('watermelon', 5, Decimal(5))] >>> Order(joe, cart, fidelity_promo) >>> Order(ann, cart, fidelity_promo) - >>> banana_cart = [LineItem('banana', 30, .5), - ... LineItem('apple', 10, 1.5)] + >>> banana_cart = [LineItem('banana', 30, Decimal('.5')), + ... LineItem('apple', 10, Decimal('1.5'))] >>> Order(joe, banana_cart, bulk_item_promo) - >>> long_order = [LineItem(str(item_code), 1, 1.0) + >>> long_cart = [LineItem(str(item_code), 1, Decimal(1)) ... for item_code in range(10)] - >>> Order(joe, long_order, large_order_promo) + >>> Order(joe, long_cart, large_order_promo) >>> Order(joe, cart, large_order_promo) # tag::STRATEGY_BEST_TESTS[] - >>> Order(joe, long_order, best_promo) + >>> Order(joe, long_cart, best_promo) >>> Order(joe, banana_cart, best_promo) @@ -35,78 +37,21 @@ # end::STRATEGY_BEST_TESTS[] """ -from collections import namedtuple - -Customer = namedtuple('Customer', 'name fidelity') - - -class LineItem: - def __init__(self, product, quantity, price): - self.product = product - self.quantity = quantity - self.price = price - - def total(self): - return self.price * self.quantity - - -class Order: # the Context - def __init__(self, customer, cart, promotion=None): - self.customer = customer - self.cart = list(cart) - self.promotion = promotion - - def total(self): - if not hasattr(self, '__total'): - self.__total = sum(item.total() for item in self.cart) - return self.__total - - def due(self): - if self.promotion is None: - discount = 0 - else: - discount = self.promotion(self) - return self.total() - discount - - def __repr__(self): - return f'' - - -def fidelity_promo(order): - """5% discount for customers with 1000 or more fidelity points""" - return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0 - - -def bulk_item_promo(order): - """10% discount for each LineItem with 20 or more units""" - discount = 0 - for item in order.cart: - if item.quantity >= 20: - discount += item.total() * 0.1 - return discount - - -def large_order_promo(order): - """7% discount for orders with 10 or more distinct items""" - distinct_items = {item.product for item in order.cart} - if len(distinct_items) >= 10: - return order.total() * 0.07 - return 0 - - # tag::STRATEGY_BEST2[] +from decimal import Decimal +from strategy import Order +from strategy import ( + fidelity_promo, bulk_item_promo, large_order_promo # <1> +) -promos = [ - globals()[name] - for name in globals() # <1> - if name.endswith('_promo') and name != 'best_promo' # <2> -] # <3> - +promos = [promo for name, promo in globals().items() # <2> + if name.endswith('_promo') and # <3> + name != 'best_promo' # <4> +] -def best_promo(order): - """Select best discount available - """ - return max(promo(order) for promo in promos) # <4> +def best_promo(order: Order) -> Decimal: # <5> + """Compute the best discount available""" + return max(promo(order) for promo in promos) # end::STRATEGY_BEST2[] diff --git a/10-dp-1class-func/strategy_best3.py b/10-dp-1class-func/strategy_best3.py index 39ce3bf..8b16ceb 100644 --- a/10-dp-1class-func/strategy_best3.py +++ b/10-dp-1class-func/strategy_best3.py @@ -3,30 +3,32 @@ # selecting best promotion from imported module """ + >>> from decimal import Decimal + >>> from strategy import Customer, LineItem, Order >>> from promotions import * >>> joe = Customer('John Doe', 0) >>> ann = Customer('Ann Smith', 1100) - >>> cart = [LineItem('banana', 4, .5), - ... LineItem('apple', 10, 1.5), - ... LineItem('watermelon', 5, 5.0)] + >>> cart = [LineItem('banana', 4, Decimal('.5')), + ... LineItem('apple', 10, Decimal('1.5')), + ... LineItem('watermelon', 5, Decimal(5))] >>> Order(joe, cart, fidelity_promo) >>> Order(ann, cart, fidelity_promo) - >>> banana_cart = [LineItem('banana', 30, .5), - ... LineItem('apple', 10, 1.5)] + >>> banana_cart = [LineItem('banana', 30, Decimal('.5')), + ... LineItem('apple', 10, Decimal('1.5'))] >>> Order(joe, banana_cart, bulk_item_promo) - >>> long_order = [LineItem(str(item_code), 1, 1.0) + >>> long_cart = [LineItem(str(item_code), 1, Decimal(1)) ... for item_code in range(10)] - >>> Order(joe, long_order, large_order_promo) + >>> Order(joe, long_cart, large_order_promo) >>> Order(joe, cart, large_order_promo) # tag::STRATEGY_BEST_TESTS[] - >>> Order(joe, long_order, best_promo) + >>> Order(joe, long_cart, best_promo) >>> Order(joe, banana_cart, best_promo) @@ -36,55 +38,20 @@ # end::STRATEGY_BEST_TESTS[] """ -from collections import namedtuple +# tag::STRATEGY_BEST3[] + +from decimal import Decimal import inspect +from strategy import Order import promotions -Customer = namedtuple('Customer', 'name fidelity') - - -class LineItem: - def __init__(self, product, quantity, price): - self.product = product - self.quantity = quantity - self.price = price - - def total(self): - return self.price * self.quantity - - -class Order: # the Context - def __init__(self, customer, cart, promotion=None): - self.customer = customer - self.cart = list(cart) - self.promotion = promotion - def total(self): - if not hasattr(self, '__total'): - self.__total = sum(item.total() for item in self.cart) - return self.__total +promos = [func for _, func in inspect.getmembers(promotions, inspect.isfunction)] - def due(self): - if self.promotion is None: - discount = 0 - else: - discount = self.promotion(self) - return self.total() - discount - def __repr__(self): - return f'' - - -# tag::STRATEGY_BEST3[] - -promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)] - - -def best_promo(order): - """Select best discount available - """ +def best_promo(order: Order) -> Decimal: + """Compute the best discount available""" return max(promo(order) for promo in promos) - # end::STRATEGY_BEST3[] diff --git a/10-dp-1class-func/strategy_best4.py b/10-dp-1class-func/strategy_best4.py index 955ac3a..8e124bb 100644 --- a/10-dp-1class-func/strategy_best4.py +++ b/10-dp-1class-func/strategy_best4.py @@ -4,29 +4,32 @@ # registered by a decorator """ + >>> from decimal import Decimal + >>> from strategy import Customer, LineItem, Order + >>> from promotions import * >>> joe = Customer('John Doe', 0) >>> ann = Customer('Ann Smith', 1100) - >>> cart = [LineItem('banana', 4, .5), - ... LineItem('apple', 10, 1.5), - ... LineItem('watermelon', 5, 5.0)] - >>> Order(joe, cart, fidelity) + >>> cart = [LineItem('banana', 4, Decimal('.5')), + ... LineItem('apple', 10, Decimal('1.5')), + ... LineItem('watermelon', 5, Decimal(5))] + >>> Order(joe, cart, fidelity_promo) - >>> Order(ann, cart, fidelity) + >>> Order(ann, cart, fidelity_promo) - >>> banana_cart = [LineItem('banana', 30, .5), - ... LineItem('apple', 10, 1.5)] - >>> Order(joe, banana_cart, bulk_item) + >>> banana_cart = [LineItem('banana', 30, Decimal('.5')), + ... LineItem('apple', 10, Decimal('1.5'))] + >>> Order(joe, banana_cart, bulk_item_promo) - >>> long_order = [LineItem(str(item_code), 1, 1.0) + >>> long_cart = [LineItem(str(item_code), 1, Decimal(1)) ... for item_code in range(10)] - >>> Order(joe, long_order, large_order) + >>> Order(joe, long_cart, large_order_promo) - >>> Order(joe, cart, large_order) + >>> Order(joe, cart, large_order_promo) # tag::STRATEGY_BEST_TESTS[] - >>> Order(joe, long_order, best_promo) + >>> Order(joe, long_cart, best_promo) >>> Order(joe, banana_cart, best_promo) @@ -36,47 +39,16 @@ # end::STRATEGY_BEST_TESTS[] """ -from collections import namedtuple -from typing import Callable, List - -Customer = namedtuple('Customer', 'name fidelity') - - -class LineItem: - def __init__(self, product, quantity, price): - self.product = product - self.quantity = quantity - self.price = price - - def total(self): - return self.price * self.quantity - - -class Order: # the Context - def __init__(self, customer, cart, promotion=None): - self.customer = customer - self.cart = list(cart) - self.promotion = promotion - - def total(self): - if not hasattr(self, '__total'): - self.__total = sum(item.total() for item in self.cart) - return self.__total - - def due(self): - if self.promotion is None: - discount = 0 - else: - discount = self.promotion(self) - return self.total() - discount - - def __repr__(self): - return f'' +from decimal import Decimal +from typing import Callable +from strategy import Order # tag::STRATEGY_BEST4[] -Promotion = Callable[[Order], float] # <2> +Promotion = Callable[[Order], Decimal] + +promos: list[Promotion] = [] # <1> def promotion(promo: Promotion) -> Promotion: # <2> @@ -84,38 +56,35 @@ def promotion(promo: Promotion) -> Promotion: # <2> return promo -promos: List[Promotion] = [] # <1> +def best_promo(order: Order) -> Decimal: + """Compute the best discount available""" + return max(promo(order) for promo in promos) # <3> -@promotion # <3> -def fidelity(order: Order) -> float: +@promotion # <4> +def fidelity(order: Order) -> Decimal: """5% discount for customers with 1000 or more fidelity points""" - return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0 + if order.customer.fidelity >= 1000: + return order.total() * Decimal('0.05') + return Decimal(0) @promotion -def bulk_item(order: Order) -> float: +def bulk_item(order: Order) -> Decimal: """10% discount for each LineItem with 20 or more units""" - discount = 0 + discount = Decimal(0) for item in order.cart: if item.quantity >= 20: - discount += item.total() * 0.1 + discount += item.total() * Decimal('0.1') return discount @promotion -def large_order(order: Order) -> float: +def large_order(order: Order) -> Decimal: """7% discount for orders with 10 or more distinct items""" distinct_items = {item.product for item in order.cart} if len(distinct_items) >= 10: - return order.total() * 0.07 - return 0 - - -def best_promo(order: Order) -> float: # <4> - """Select best discount available - """ - return max(promo(order) for promo in promos) - + return order.total() * Decimal('0.07') + return Decimal(0) # end::STRATEGY_BEST4[] diff --git a/10-dp-1class-func/strategy_param.py b/10-dp-1class-func/strategy_param.py index 7318530..5490666 100644 --- a/10-dp-1class-func/strategy_param.py +++ b/10-dp-1class-func/strategy_param.py @@ -15,9 +15,9 @@ ... LineItem('apple', 10, 1.5)] >>> Order(joe, banana_cart, bulk_item_promo(10)) - >>> big_cart = [LineItem(str(item_code), 1, 1.0) + >>> long_cart = [LineItem(str(item_code), 1, 1.0) ... for item_code in range(10)] - >>> Order(joe, big_cart, LargeOrderPromo(7)) + >>> Order(joe, long_cart, LargeOrderPromo(7)) >>> Order(joe, cart, LargeOrderPromo(7)) diff --git a/10-dp-1class-func/strategy_param_test.py b/10-dp-1class-func/strategy_param_test.py index d550e2b..cfb965e 100644 --- a/10-dp-1class-func/strategy_param_test.py +++ b/10-dp-1class-func/strategy_param_test.py @@ -47,7 +47,7 @@ def test_large_order_promo_with_discount(customer_fidelity_0) -> None: assert order.due() == 9.3 -def test_general_discount(customer_fidelity_0, cart_plain) -> None: +def test_general_discount(customer_fidelity_1100, cart_plain) -> None: general_promo: Promotion = functools.partial(general_discount, 5) order = Order(customer_fidelity_1100, cart_plain, general_promo) assert order.total() == 42.0 diff --git a/10-dp-1class-func/strategy_test.py b/10-dp-1class-func/strategy_test.py index bf225ae..c47440e 100644 --- a/10-dp-1class-func/strategy_test.py +++ b/10-dp-1class-func/strategy_test.py @@ -1,4 +1,4 @@ -from typing import List +from decimal import Decimal import pytest # type: ignore @@ -17,47 +17,50 @@ def customer_fidelity_1100() -> Customer: @pytest.fixture -def cart_plain() -> List[LineItem]: - return [ - LineItem('banana', 4, 0.5), - LineItem('apple', 10, 1.5), - LineItem('watermelon', 5, 5.0), - ] +def cart_plain() -> tuple[LineItem, ...]: + return ( + LineItem('banana', 4, Decimal('0.5')), + LineItem('apple', 10, Decimal('1.5')), + LineItem('watermelon', 5, Decimal('5.0')), + ) def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None: order = Order(customer_fidelity_0, cart_plain, fidelity_promo) - assert order.total() == 42.0 - assert order.due() == 42.0 + assert order.total() == 42 + assert order.due() == 42 def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None: order = Order(customer_fidelity_1100, cart_plain, fidelity_promo) - assert order.total() == 42.0 - assert order.due() == 39.9 + assert order.total() == 42 + assert order.due() == Decimal('39.9') def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None: order = Order(customer_fidelity_0, cart_plain, bulk_item_promo) - assert order.total() == 42.0 - assert order.due() == 42.0 + assert order.total() == 42 + assert order.due() == 42 def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None: - cart = [LineItem('banana', 30, 0.5), LineItem('apple', 10, 1.5)] + cart = [LineItem('banana', 30, Decimal('0.5')), + LineItem('apple', 10, Decimal('1.5'))] order = Order(customer_fidelity_0, cart, bulk_item_promo) - assert order.total() == 30.0 - assert order.due() == 28.5 + assert order.total() == 30 + assert order.due() == Decimal('28.5') def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None: order = Order(customer_fidelity_0, cart_plain, large_order_promo) - assert order.total() == 42.0 - assert order.due() == 42.0 + assert order.total() == 42 + assert order.due() == 42 def test_large_order_promo_with_discount(customer_fidelity_0) -> None: - cart = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)] + + cart = [LineItem(str(item_code), 1, Decimal(1)) + for item_code in range(10)] order = Order(customer_fidelity_0, cart, large_order_promo) - assert order.total() == 10.0 - assert order.due() == 9.3 + assert order.total() == 10 + assert order.due() == Decimal('9.3') diff --git a/10-dp-1class-func/untyped/classic_strategy.py b/10-dp-1class-func/untyped/classic_strategy.py index b969780..61cccd5 100644 --- a/10-dp-1class-func/untyped/classic_strategy.py +++ b/10-dp-1class-func/untyped/classic_strategy.py @@ -17,9 +17,9 @@ ... LineItem('apple', 10, 1.5)] >>> Order(joe, banana_cart, BulkItemPromo()) # <6> - >>> long_order = [LineItem(str(item_code), 1, 1.0) # <7> + >>> long_cart = [LineItem(str(item_code), 1, 1.0) # <7> ... for item_code in range(10)] - >>> Order(joe, long_order, LargeOrderPromo()) # <8> + >>> Order(joe, long_cart, LargeOrderPromo()) # <8> >>> Order(joe, cart, LargeOrderPromo()) diff --git a/10-dp-1class-func/untyped/strategy.py b/10-dp-1class-func/untyped/strategy.py index 1f8ad4c..518c69d 100644 --- a/10-dp-1class-func/untyped/strategy.py +++ b/10-dp-1class-func/untyped/strategy.py @@ -17,9 +17,9 @@ ... LineItem('apple', 10, 1.5)] >>> Order(joe, banana_cart, bulk_item_promo) # <3> - >>> long_order = [LineItem(str(item_code), 1, 1.0) + >>> long_cart = [LineItem(str(item_code), 1, 1.0) ... for item_code in range(10)] - >>> Order(joe, long_order, large_order_promo) + >>> Order(joe, long_cart, large_order_promo) >>> Order(joe, cart, large_order_promo) diff --git a/10-dp-1class-func/untyped/strategy_best.py b/10-dp-1class-func/untyped/strategy_best.py index c0585f7..718b672 100644 --- a/10-dp-1class-func/untyped/strategy_best.py +++ b/10-dp-1class-func/untyped/strategy_best.py @@ -16,16 +16,16 @@ ... LineItem('apple', 10, 1.5)] >>> Order(joe, banana_cart, bulk_item_promo) - >>> long_order = [LineItem(str(item_code), 1, 1.0) + >>> long_cart = [LineItem(str(item_code), 1, 1.0) ... for item_code in range(10)] - >>> Order(joe, long_order, large_order_promo) + >>> Order(joe, long_cart, large_order_promo) >>> Order(joe, cart, large_order_promo) # tag::STRATEGY_BEST_TESTS[] - >>> Order(joe, long_order, best_promo) # <1> + >>> Order(joe, long_cart, best_promo) # <1> >>> Order(joe, banana_cart, best_promo) # <2> diff --git a/10-dp-1class-func/untyped/strategy_best2.py b/10-dp-1class-func/untyped/strategy_best2.py index 1f4700f..bfcd839 100644 --- a/10-dp-1class-func/untyped/strategy_best2.py +++ b/10-dp-1class-func/untyped/strategy_best2.py @@ -16,16 +16,16 @@ ... LineItem('apple', 10, 1.5)] >>> Order(joe, banana_cart, bulk_item_promo) - >>> long_order = [LineItem(str(item_code), 1, 1.0) + >>> long_cart = [LineItem(str(item_code), 1, 1.0) ... for item_code in range(10)] - >>> Order(joe, long_order, large_order_promo) + >>> Order(joe, long_cart, large_order_promo) >>> Order(joe, cart, large_order_promo) # tag::STRATEGY_BEST_TESTS[] - >>> Order(joe, long_order, best_promo) + >>> Order(joe, long_cart, best_promo) >>> Order(joe, banana_cart, best_promo) diff --git a/10-dp-1class-func/untyped/strategy_best3.py b/10-dp-1class-func/untyped/strategy_best3.py index 8d21ffc..f911ce3 100644 --- a/10-dp-1class-func/untyped/strategy_best3.py +++ b/10-dp-1class-func/untyped/strategy_best3.py @@ -17,16 +17,16 @@ ... LineItem('apple', 10, 1.5)] >>> Order(joe, banana_cart, bulk_item_promo) - >>> long_order = [LineItem(str(item_code), 1, 1.0) + >>> long_cart = [LineItem(str(item_code), 1, 1.0) ... for item_code in range(10)] - >>> Order(joe, long_order, large_order_promo) + >>> Order(joe, long_cart, large_order_promo) >>> Order(joe, cart, large_order_promo) # tag::STRATEGY_BEST_TESTS[] - >>> Order(joe, long_order, best_promo) + >>> Order(joe, long_cart, best_promo) >>> Order(joe, banana_cart, best_promo) diff --git a/10-dp-1class-func/untyped/strategy_best4.py b/10-dp-1class-func/untyped/strategy_best4.py index b523752..9573176 100644 --- a/10-dp-1class-func/untyped/strategy_best4.py +++ b/10-dp-1class-func/untyped/strategy_best4.py @@ -17,16 +17,16 @@ ... LineItem('apple', 10, 1.5)] >>> Order(joe, banana_cart, bulk_item) - >>> long_order = [LineItem(str(item_code), 1, 1.0) + >>> long_cart = [LineItem(str(item_code), 1, 1.0) ... for item_code in range(10)] - >>> Order(joe, long_order, large_order) + >>> Order(joe, long_cart, large_order) >>> Order(joe, cart, large_order) # tag::STRATEGY_BEST_TESTS[] - >>> Order(joe, long_order, best_promo) + >>> Order(joe, long_cart, best_promo) >>> Order(joe, banana_cart, best_promo) diff --git a/10-dp-1class-func/untyped/strategy_param.py b/10-dp-1class-func/untyped/strategy_param.py index d5cc931..ab07132 100644 --- a/10-dp-1class-func/untyped/strategy_param.py +++ b/10-dp-1class-func/untyped/strategy_param.py @@ -15,9 +15,9 @@ ... LineItem('apple', 10, 1.5)] >>> Order(joe, banana_cart, bulk_item_promo(10)) - >>> long_order = [LineItem(str(item_code), 1, 1.0) + >>> long_cart = [LineItem(str(item_code), 1, 1.0) ... for item_code in range(10)] - >>> Order(joe, long_order, large_order_promo(7)) + >>> Order(joe, long_cart, large_order_promo(7)) >>> Order(joe, cart, large_order_promo(7)) diff --git a/10-dp-1class-func/untyped/strategy_param2.py b/10-dp-1class-func/untyped/strategy_param2.py index 625bbca..332f49d 100644 --- a/10-dp-1class-func/untyped/strategy_param2.py +++ b/10-dp-1class-func/untyped/strategy_param2.py @@ -15,9 +15,9 @@ ... LineItem('apple', 10, 1.5)] >>> Order(joe, banana_cart, BulkItemPromo(10)) - >>> long_order = [LineItem(str(item_code), 1, 1.0) + >>> long_cart = [LineItem(str(item_code), 1, 1.0) ... for item_code in range(10)] - >>> Order(joe, long_order, LargeOrderPromo(7)) + >>> Order(joe, long_cart, LargeOrderPromo(7)) >>> Order(joe, cart, LargeOrderPromo(7)) diff --git a/11-pythonic-obj/mem_test.py b/11-pythonic-obj/mem_test.py index 0d745f2..12fc54e 100644 --- a/11-pythonic-obj/mem_test.py +++ b/11-pythonic-obj/mem_test.py @@ -4,20 +4,25 @@ NUM_VECTORS = 10**7 +module = None if len(sys.argv) == 2: module_name = sys.argv[1].replace('.py', '') module = importlib.import_module(module_name) else: print(f'Usage: {sys.argv[0]} ') - sys.exit(2) # command line usage error -fmt = 'Selected Vector2d type: {.__name__}.{.__name__}' -print(fmt.format(module, module.Vector2d)) +if module is None: + print('Running test with built-in `complex`') + cls = complex +else: + fmt = 'Selected Vector2d type: {.__name__}.{.__name__}' + print(fmt.format(module, module.Vector2d)) + cls = module.Vector2d mem_init = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss -print(f'Creating {NUM_VECTORS:,} Vector2d instances') +print(f'Creating {NUM_VECTORS:,} {cls.__qualname__!r} instances') -vectors = [module.Vector2d(3.0, 4.0) for i in range(NUM_VECTORS)] +vectors = [cls(3.0, 4.0) for i in range(NUM_VECTORS)] mem_final = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss print(f'Initial RAM usage: {mem_init:14,}') diff --git a/11-pythonic-obj/patterns.py b/11-pythonic-obj/patterns.py new file mode 100644 index 0000000..9317a4c --- /dev/null +++ b/11-pythonic-obj/patterns.py @@ -0,0 +1,53 @@ +from vector2d_v3 import Vector2d + +# tag::KEYWORD_PATTERNS[] +def keyword_pattern_demo(v: Vector2d) -> None: + match v: + case Vector2d(x=0, y=0): + print(f'{v!r} is null') + case Vector2d(x=0): + print(f'{v!r} is vertical') + case Vector2d(y=0): + print(f'{v!r} is horizontal') + case Vector2d(x=x, y=y) if x==y: + print(f'{v!r} is diagonal') + case _: + print(f'{v!r} is awesome') +# end::KEYWORD_PATTERNS[] + +# tag::POSITIONAL_PATTERNS[] +def positional_pattern_demo(v: Vector2d) -> None: + match v: + case Vector2d(0, 0): + print(f'{v!r} is null') + case Vector2d(0): + print(f'{v!r} is vertical') + case Vector2d(_, 0): + print(f'{v!r} is horizontal') + case Vector2d(x, y) if x==y: + print(f'{v!r} is diagonal') + case _: + print(f'{v!r} is awesome') +# end::POSITIONAL_PATTERNS[] + + +def main(): + vectors = ( + Vector2d(1, 1), + Vector2d(0, 1), + Vector2d(1, 0), + Vector2d(1, 2), + Vector2d(0, 0), + ) + print('KEYWORD PATTERNS:') + for vector in vectors: + keyword_pattern_demo(vector) + + print('POSITIONAL PATTERNS:') + for vector in vectors: + positional_pattern_demo(vector) + + + +if __name__ == '__main__': + main() diff --git a/11-pythonic-obj/slots.rst b/11-pythonic-obj/slots.rst new file mode 100644 index 0000000..351c9de --- /dev/null +++ b/11-pythonic-obj/slots.rst @@ -0,0 +1,52 @@ +# tag::PIXEL[] +>>> class Pixel: +... __slots__ = ('x', 'y') # <1> +... +>>> p = Pixel() # <2> +>>> p.__dict__ # <3> +Traceback (most recent call last): + ... +AttributeError: 'Pixel' object has no attribute '__dict__' +>>> p.x = 10 # <4> +>>> p.y = 20 +>>> p.color = 'red' # <5> +Traceback (most recent call last): + ... +AttributeError: 'Pixel' object has no attribute 'color' + +# end::PIXEL[] + +# tag::OPEN_PIXEL[] +>>> class OpenPixel(Pixel): # <1> +... pass +... +>>> op = OpenPixel() +>>> op.__dict__ # <2> +{} +>>> op.x = 8 # <3> +>>> op.__dict__ # <4> +{} +>>> op.x # <5> +8 +>>> op.color = 'green' # <6> +>>> op.__dict__ # <7> +{'color': 'green'} + +# end::OPEN_PIXEL[] + +# tag::COLOR_PIXEL[] +>>> class ColorPixel(Pixel): +... __slots__ = ('color',) # <1> +>>> cp = ColorPixel() +>>> cp.__dict__ # <2> +Traceback (most recent call last): + ... +AttributeError: 'ColorPixel' object has no attribute '__dict__' +>>> cp.x = 2 +>>> cp.color = 'blue' # <3> +>>> cp.flavor = 'banana' +Traceback (most recent call last): + ... +AttributeError: 'ColorPixel' object has no attribute 'flavor' + +# end::COLOR_PIXEL[] diff --git a/11-pythonic-obj/vector2d_v3.py b/11-pythonic-obj/vector2d_v3.py index 376a8a4..0edffb7 100644 --- a/11-pythonic-obj/vector2d_v3.py +++ b/11-pythonic-obj/vector2d_v3.py @@ -72,15 +72,13 @@ >>> v1.x = 123 Traceback (most recent call last): ... - AttributeError: can't set attribute + AttributeError: can't set attribute 'x' Tests of hashing: >>> v1 = Vector2d(3, 4) >>> v2 = Vector2d(3.1, 4.2) - >>> hash(v1), hash(v2) - (7, 384307168202284039) >>> len({v1, v2}) 2 @@ -90,6 +88,8 @@ import math class Vector2d: + __match_args__ = ('x', 'y') + typecode = 'd' def __init__(self, x, y): @@ -122,7 +122,7 @@ def __eq__(self, other): return tuple(self) == tuple(other) def __hash__(self): - return hash(self.x) ^ hash(self.y) + return hash((self.x, self.y)) def __abs__(self): return math.hypot(self.x, self.y) diff --git a/11-pythonic-obj/vector2d_v3_prophash.py b/11-pythonic-obj/vector2d_v3_prophash.py index 3552530..1652cd4 100644 --- a/11-pythonic-obj/vector2d_v3_prophash.py +++ b/11-pythonic-obj/vector2d_v3_prophash.py @@ -72,7 +72,7 @@ >>> v1.x = 123 Traceback (most recent call last): ... - AttributeError: can't set attribute + AttributeError: can't set attribute 'x' # end::VECTOR2D_V3_HASH_DEMO[] @@ -81,8 +81,6 @@ >>> v1 = Vector2d(3, 4) >>> v2 = Vector2d(3.1, 4.2) - >>> hash(v1), hash(v2) - (7, 384307168202284039) >>> len({v1, v2}) 2 @@ -112,7 +110,7 @@ def y(self): def __iter__(self): return (i for i in (self.x, self.y)) # <6> - # remaining methods follow (omitted in book listing) + # remaining methods: same as previous Vector2d # end::VECTOR2D_V3_PROP[] def __repr__(self): @@ -131,7 +129,7 @@ def __eq__(self, other): # tag::VECTOR_V3_HASH[] def __hash__(self): - return hash(self.x) ^ hash(self.y) + return hash((self.x, self.y)) # end::VECTOR_V3_HASH[] def __abs__(self): diff --git a/11-pythonic-obj/vector2d_v3_slots.py b/11-pythonic-obj/vector2d_v3_slots.py index 20b6da4..89da973 100644 --- a/11-pythonic-obj/vector2d_v3_slots.py +++ b/11-pythonic-obj/vector2d_v3_slots.py @@ -72,14 +72,12 @@ >>> v1.x = 123 Traceback (most recent call last): ... - AttributeError: can't set attribute + AttributeError: can't set attribute 'x' Tests of hashing: >>> v1 = Vector2d(3, 4) >>> v2 = Vector2d(3.1, 4.2) - >>> hash(v1), hash(v2) - (7, 384307168202284039) >>> len({v1, v2}) 2 @@ -90,11 +88,10 @@ # tag::VECTOR2D_V3_SLOTS[] class Vector2d: - __slots__ = ('__x', '__y') + __match_args__ = ('x', 'y') # <1> + __slots__ = ('__x', '__y') # <2> typecode = 'd' - - # methods follow (omitted in book listing) # end::VECTOR2D_V3_SLOTS[] def __init__(self, x, y): @@ -127,7 +124,7 @@ def __eq__(self, other): return tuple(self) == tuple(other) def __hash__(self): - return hash(self.x) ^ hash(self.y) + return hash((self.x, self.y)) def __abs__(self): return math.hypot(self.x, self.y) diff --git a/12-seq-hacking/vector_v3.py b/12-seq-hacking/vector_v3.py index 6ee18b5..c1f7859 100644 --- a/12-seq-hacking/vector_v3.py +++ b/12-seq-hacking/vector_v3.py @@ -199,15 +199,17 @@ def __getitem__(self, key): return self._components[index] # tag::VECTOR_V3_GETATTR[] - shortcut_names = 'xyzt' + __match_args__ = ('x', 'y', 'z', 't') # <1> def __getattr__(self, name): - cls = type(self) # <1> - if len(name) == 1: # <2> - pos = cls.shortcut_names.find(name) # <3> - if 0 <= pos < len(self._components): # <4> - return self._components[pos] - msg = f'{cls.__name__!r} object has no attribute {name!r}' # <5> + cls = type(self) # <2> + try: + pos = cls.__match_args__.index(name) # <3> + except ValueError: # <4> + pos = -1 + if 0 <= pos < len(self._components): # <5> + return self._components[pos] + msg = f'{cls.__name__!r} object has no attribute {name!r}' # <6> raise AttributeError(msg) # end::VECTOR_V3_GETATTR[] @@ -215,8 +217,8 @@ def __getattr__(self, name): def __setattr__(self, name, value): cls = type(self) if len(name) == 1: # <1> - if name in cls.shortcut_names: # <2> - error = 'read-only attribute {attr_name!r}' + if name in cls.__match_args__: # <2> + error = 'readonly attribute {attr_name!r}' elif name.islower(): # <3> error = "can't set attributes 'a' to 'z' in {cls_name!r}" else: diff --git a/12-seq-hacking/vector_v4.py b/12-seq-hacking/vector_v4.py index 95530eb..856f338 100644 --- a/12-seq-hacking/vector_v4.py +++ b/12-seq-hacking/vector_v4.py @@ -199,14 +199,16 @@ def __getitem__(self, key): index = operator.index(key) return self._components[index] - shortcut_names = 'xyzt' + __match_args__ = ('x', 'y', 'z', 't') def __getattr__(self, name): cls = type(self) - if len(name) == 1: - pos = cls.shortcut_names.find(name) - if 0 <= pos < len(self._components): - return self._components[pos] + try: + pos = cls.__match_args__.index(name) + except ValueError: + pos = -1 + if 0 <= pos < len(self._components): + return self._components[pos] msg = f'{cls.__name__!r} object has no attribute {name!r}' raise AttributeError(msg) diff --git a/12-seq-hacking/vector_v5.py b/12-seq-hacking/vector_v5.py index ebbd523..09b3044 100644 --- a/12-seq-hacking/vector_v5.py +++ b/12-seq-hacking/vector_v5.py @@ -242,14 +242,16 @@ def __getitem__(self, key): index = operator.index(key) return self._components[index] - shortcut_names = 'xyzt' + __match_args__ = ('x', 'y', 'z', 't') def __getattr__(self, name): cls = type(self) - if len(name) == 1: - pos = cls.shortcut_names.find(name) - if 0 <= pos < len(self._components): - return self._components[pos] + try: + pos = cls.__match_args__.index(name) + except ValueError: + pos = -1 + if 0 <= pos < len(self._components): + return self._components[pos] msg = f'{cls.__name__!r} object has no attribute {name!r}' raise AttributeError(msg) diff --git a/13-protocol-abc/frenchdeck2.py b/13-protocol-abc/frenchdeck2.py index e3ccc2c..612581d 100644 --- a/13-protocol-abc/frenchdeck2.py +++ b/13-protocol-abc/frenchdeck2.py @@ -1,8 +1,8 @@ -import collections +from collections import namedtuple, abc -Card = collections.namedtuple('Card', ['rank', 'suit']) +Card = namedtuple('Card', ['rank', 'suit']) -class FrenchDeck2(collections.MutableSequence): +class FrenchDeck2(abc.MutableSequence): ranks = [str(n) for n in range(2, 11)] + list('JQKA') suits = 'spades diamonds clubs hearts'.split() diff --git a/13-protocol-abc/typing/randompick_test.py b/13-protocol-abc/typing/randompick_test.py index f090022..115c4d3 100644 --- a/13-protocol-abc/typing/randompick_test.py +++ b/13-protocol-abc/typing/randompick_test.py @@ -12,14 +12,14 @@ def pick(self) -> Any: # <3> return self._items.pop() def test_isinstance() -> None: # <4> - popper = SimplePicker([1]) - assert isinstance(popper, RandomPicker) + popper: RandomPicker = SimplePicker([1]) # <5> + assert isinstance(popper, RandomPicker) # <6> -def test_item_type() -> None: # <5> +def test_item_type() -> None: # <7> items = [1, 2] popper = SimplePicker(items) item = popper.pick() assert item in items if TYPE_CHECKING: - reveal_type(item) # <6> + reveal_type(item) # <8> assert isinstance(item, int) diff --git a/13-protocol-abc/typing/vector2d_v4.py b/13-protocol-abc/typing/vector2d_v4.py index 1e9d4ac..deaa824 100644 --- a/13-protocol-abc/typing/vector2d_v4.py +++ b/13-protocol-abc/typing/vector2d_v4.py @@ -168,5 +168,5 @@ def __complex__(self): @classmethod def fromcomplex(cls, datum): - return Vector2d(datum.real, datum.imag) # <1> + return cls(datum.real, datum.imag) # <1> # end::VECTOR2D_V4_COMPLEX[] diff --git a/13-protocol-abc/typing/vector2d_v5.py b/13-protocol-abc/typing/vector2d_v5.py index ceda21c..378b826 100644 --- a/13-protocol-abc/typing/vector2d_v5.py +++ b/13-protocol-abc/typing/vector2d_v5.py @@ -170,5 +170,5 @@ def __complex__(self) -> complex: # <2> @classmethod def fromcomplex(cls, datum: SupportsComplex) -> Vector2d: # <3> c = complex(datum) # <4> - return Vector2d(c.real, c.imag) + return cls(c.real, c.imag) # end::VECTOR2D_V5_COMPLEX[] diff --git a/14-inheritance/README.rst b/14-inheritance/README.rst index 2f7426c..496ffb2 100644 --- a/14-inheritance/README.rst +++ b/14-inheritance/README.rst @@ -1,4 +1,4 @@ -Sample code for Chapter 14 - "Inheritance: for good or for worse" +Sample code for Chapter 14 - "Inheritance: for better or for worse" From the book "Fluent Python, Second Edition" by Luciano Ramalho (O'Reilly, 2021) https://learning.oreilly.com/library/view/fluent-python-2nd/9781492056348/ diff --git a/14-inheritance/diamond.py b/14-inheritance/diamond.py index 5aaac18..1df747b 100644 --- a/14-inheritance/diamond.py +++ b/14-inheritance/diamond.py @@ -1,27 +1,62 @@ -class A: - def ping(self): - print('ping:', self) +""" +diamond1.py: Demo of diamond-shaped class graph. +# tag::LEAF_MRO[] +>>> Leaf.__mro__ # doctest:+NORMALIZE_WHITESPACE + (, , , + , ) -class B(A): - def pong(self): - print('pong:', self) +# end::LEAF_MRO[] + +# tag::DIAMOND_CALLS[] + >>> leaf1 = Leaf() # <1> + >>> leaf1.ping() # <2> + .ping() in Leaf + .ping() in A + .ping() in B + .ping() in Root + >>> leaf1.pong() # <3> + .pong() in A + .pong() in B + +# end::DIAMOND_CALLS[] +""" + +# tag::DIAMOND_CLASSES[] +class Root: # <1> + def ping(self): + print(f'{self}.ping() in Root') -class C(A): def pong(self): - print('PONG:', self) + print(f'{self}.pong() in Root') + def __repr__(self): + cls_name = type(self).__name__ + return f'' -class D(B, C): +class A(Root): # <2> def ping(self): + print(f'{self}.ping() in A') super().ping() - print('post-ping:', self) - def pingpong(self): - self.ping() - super().ping() - self.pong() + def pong(self): + print(f'{self}.pong() in A') super().pong() - C.pong(self) + + +class B(Root): # <3> + def ping(self): + print(f'{self}.ping() in B') + super().ping() + + def pong(self): + print(f'{self}.pong() in B') + + +class Leaf(A, B): # <4> + def ping(self): + print(f'{self}.ping() in Leaf') + super().ping() +# end::DIAMOND_CLASSES[] diff --git a/14-inheritance/diamond2.py b/14-inheritance/diamond2.py new file mode 100644 index 0000000..d92db69 --- /dev/null +++ b/14-inheritance/diamond2.py @@ -0,0 +1,67 @@ +""" +unrelated.py: examples with ``super()`` in a sibling class. + +``U`` is unrelated (does not subclass ``Root``) + +Calling ``ping`` on an instance of ``U`` fails:: + +# tag::UNRELATED_DEMO_1[] + >>> u = U() + >>> u.ping() + Traceback (most recent call last): + ... + AttributeError: 'super' object has no attribute 'ping' + +# end::UNRELATED_DEMO_1[] + + +But if ``U`` is part of a cooperative arrangement of base classes, +its ``ping`` method works:: + +# tag::UNRELATED_DEMO_2[] + + >>> leaf2 = LeafUA() + >>> leaf2.ping() + .ping() in LeafUA + .ping() in U + .ping() in A + .ping() in Root + >>> LeafUA.__mro__ # doctest:+NORMALIZE_WHITESPACE + (, , + , , ) + +# end::UNRELATED_DEMO_2[] + + +Here ``U.ping`` is never called because ``Root.ping`` does not call ``super``. + + >>> o6 = LeafAU() + >>> o6.ping() + .ping() in LeafAU + .ping() in A + .ping() in Root + >>> LeafAU.__mro__ # doctest:+NORMALIZE_WHITESPACE + (, , , + , ) + +""" + +# tag::DIAMOND_CLASSES[] +from diamond import A # <1> + +class U(): # <2> + def ping(self): + print(f'{self}.ping() in U') + super().ping() # <3> + +class LeafUA(U, A): # <4> + def ping(self): + print(f'{self}.ping() in LeafUA') + super().ping() +# end::DIAMOND_CLASSES[] + +class LeafAU(A, U): + def ping(self): + print(f'{self}.ping() in LeafAU') + super().ping() + diff --git a/14-inheritance/uppermixin.py b/14-inheritance/uppermixin.py new file mode 100644 index 0000000..12c4a17 --- /dev/null +++ b/14-inheritance/uppermixin.py @@ -0,0 +1,140 @@ +""" +Short demos +=========== + +``UpperDict`` behaves like a case-insensitive mapping`:: + +# tag::UPPERDICT_DEMO[] + >>> d = UpperDict([('a', 'letter A'), (2, 'digit two')]) + >>> list(d.keys()) + ['A', 2] + >>> d['b'] = 'letter B' + >>> 'b' in d + True + >>> d['a'], d.get('B') + ('letter A', 'letter B') + >>> list(d.keys()) + ['A', 2, 'B'] + +# end::UPPERDICT_DEMO[] + +And ``UpperCounter`` is also case-insensitive:: + +# tag::UPPERCOUNTER_DEMO[] + >>> c = UpperCounter('BaNanA') + >>> c.most_common() + [('A', 3), ('N', 2), ('B', 1)] + +# end::UPPERCOUNTER_DEMO[] + +Detailed tests +============== + +UpperDict uppercases all string keys. + + >>> d = UpperDict([('a', 'letter A'), ('B', 'letter B'), (2, 'digit two')]) + + +Tests for item retrieval using `d[key]` notation:: + + >>> d['A'] + 'letter A' + >>> d['b'] + 'letter B' + >>> d[2] + 'digit two' + + +Tests for missing key:: + + >>> d['z'] + Traceback (most recent call last): + ... + KeyError: 'Z' + >>> d[99] + Traceback (most recent call last): + ... + KeyError: 99 + + +Tests for item retrieval using `d.get(key)` notation:: + + >>> d.get('a') + 'letter A' + >>> d.get('B') + 'letter B' + >>> d.get(2) + 'digit two' + >>> d.get('z', '(not found)') + '(not found)' + +Tests for the `in` operator:: + + >>> ('a' in d, 'B' in d, 'z' in d) + (True, True, False) + +Test for item assignment using lowercase key:: + + >>> d['c'] = 'letter C' + >>> d['C'] + 'letter C' + +Tests for update using a `dict` or a sequence of pairs:: + + >>> d.update({'D': 'letter D', 'e': 'letter E'}) + >>> list(d.keys()) + ['A', 'B', 2, 'C', 'D', 'E'] + >>> d.update([('f', 'letter F'), ('G', 'letter G')]) + >>> list(d.keys()) + ['A', 'B', 2, 'C', 'D', 'E', 'F', 'G'] + >>> d # doctest:+NORMALIZE_WHITESPACE + {'A': 'letter A', 'B': 'letter B', 2: 'digit two', + 'C': 'letter C', 'D': 'letter D', 'E': 'letter E', + 'F': 'letter F', 'G': 'letter G'} + +UpperCounter uppercases all `str` keys. + +Test for initializer: keys are uppercased. + + >>> d = UpperCounter('AbracAdaBrA') + >>> sorted(d.keys()) + ['A', 'B', 'C', 'D', 'R'] + +Tests for count retrieval using `d[key]` notation:: + + >>> d['a'] + 5 + >>> d['z'] + 0 + +""" +# tag::UPPERCASE_MIXIN[] +import collections + +def _upper(key): # <1> + try: + return key.upper() + except AttributeError: + return key + +class UpperCaseMixin: # <2> + def __setitem__(self, key, item): + super().__setitem__(_upper(key), item) + + def __getitem__(self, key): + return super().__getitem__(_upper(key)) + + def get(self, key, default=None): + return super().get(_upper(key), default) + + def __contains__(self, key): + return super().__contains__(_upper(key)) +# end::UPPERCASE_MIXIN[] + +# tag::UPPERDICT[] +class UpperDict(UpperCaseMixin, collections.UserDict): # <1> + pass + +class UpperCounter(UpperCaseMixin, collections.Counter): # <2> + """Specialized 'Counter' that uppercases string keys""" # <3> +# end::UPPERDICT[] diff --git a/15-more-types/cast/find.py b/15-more-types/cast/find.py index c853ea8..e79fe5b 100644 --- a/15-more-types/cast/find.py +++ b/15-more-types/cast/find.py @@ -3,7 +3,7 @@ def find_first_str(a: list[object]) -> str: index = next(i for i, x in enumerate(a) if isinstance(x, str)) - # We only get here if there's at least one string in a + # We only get here if there's at least one string return cast(str, a[index]) # end::CAST[] diff --git a/15-more-types/protocol/abs_demo.py b/15-more-types/protocol/abs_demo.py index 9cb7f80..9c0b74c 100755 --- a/15-more-types/protocol/abs_demo.py +++ b/15-more-types/protocol/abs_demo.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import math from typing import NamedTuple, SupportsAbs diff --git a/15-more-types/protocol/mymax/mymax.py b/15-more-types/protocol/mymax/mymax.py index 5b9f4b2..6096985 100644 --- a/15-more-types/protocol/mymax/mymax.py +++ b/15-more-types/protocol/mymax/mymax.py @@ -1,5 +1,6 @@ # tag::MYMAX_TYPES[] -from typing import Protocol, Any, TypeVar, overload, Callable, Iterable, Union +from collections.abc import Callable, Iterable +from typing import Protocol, Any, TypeVar, overload, Union class SupportsLessThan(Protocol): def __lt__(self, other: Any) -> bool: ... diff --git a/15-more-types/typeddict/books.py b/15-more-types/typeddict/books.py index e33e21e..8caca46 100644 --- a/15-more-types/typeddict/books.py +++ b/15-more-types/typeddict/books.py @@ -1,6 +1,6 @@ +import json # tag::BOOKDICT[] from typing import TypedDict -import json class BookDict(TypedDict): isbn: str @@ -10,14 +10,14 @@ class BookDict(TypedDict): # end::BOOKDICT[] # tag::TOXML[] -AUTHOR_EL = '{}' +AUTHOR_ELEMENT = '{}' def to_xml(book: BookDict) -> str: # <1> elements: list[str] = [] # <2> for key, value in book.items(): if isinstance(value, list): # <3> elements.extend( - AUTHOR_EL.format(n) for n in value) # <4> + AUTHOR_ELEMENT.format(n) for n in value) # <4> else: tag = key.upper() elements.append(f'<{tag}>{value}') diff --git a/15-more-types/typeddict/books_any.py b/15-more-types/typeddict/books_any.py index 49a544e..0a5e1e4 100644 --- a/15-more-types/typeddict/books_any.py +++ b/15-more-types/typeddict/books_any.py @@ -10,13 +10,13 @@ class BookDict(TypedDict): # end::BOOKDICT[] # tag::TOXML[] -AUTHOR_EL = '{}' +AUTHOR_ELEMENT = '{}' def to_xml(book: BookDict) -> str: # <1> elements: List[str] = [] # <2> for key, value in book.items(): if isinstance(value, list): # <3> - elements.extend(AUTHOR_EL.format(n) + elements.extend(AUTHOR_ELEMENT.format(n) for n in value) else: tag = key.upper() diff --git a/16-op-overloading/bingoaddable.py b/16-op-overloading/bingoaddable.py index 38a70c7..da667d4 100644 --- a/16-op-overloading/bingoaddable.py +++ b/16-op-overloading/bingoaddable.py @@ -46,7 +46,7 @@ >>> globe += 1 # <6> Traceback (most recent call last): ... - TypeError: right operand in += must be 'AddableBingoCage' or an iterable + TypeError: right operand in += must be 'Tombola' or an iterable # end::ADDABLE_BINGO_IADD_DEMO[] @@ -72,9 +72,9 @@ def __iadd__(self, other): try: other_iterable = iter(other) # <4> except TypeError: # <5> - self_cls = type(self).__name__ - msg = "right operand in += must be {!r} or an iterable" - raise TypeError(msg.format(self_cls)) + msg = ('right operand in += must be ' + "'Tombola' or an iterable") + raise TypeError(msg) self.load(other_iterable) # <6> return self # <7> diff --git a/16-op-overloading/vector2d_v3.py b/16-op-overloading/vector2d_v3.py index 5812dcf..9ee716c 100644 --- a/16-op-overloading/vector2d_v3.py +++ b/16-op-overloading/vector2d_v3.py @@ -1,5 +1,5 @@ """ -A 2-dimensional vector class +A two-dimensional vector class >>> v1 = Vector2d(3, 4) >>> print(v1.x, v1.y) @@ -72,7 +72,7 @@ >>> v1.x = 123 Traceback (most recent call last): ... - AttributeError: can't set attribute + AttributeError: can't set attribute 'x' Tests of hashing: @@ -81,7 +81,7 @@ >>> v2 = Vector2d(3.1, 4.2) >>> hash(v1), hash(v2) (7, 384307168202284039) - >>> len(set([v1, v2])) + >>> len({v1, v2}) 2 """ @@ -90,6 +90,8 @@ import math class Vector2d: + __match_args__ = ('x', 'y') + typecode = 'd' def __init__(self, x, y): diff --git a/16-op-overloading/vector_v6.py b/16-op-overloading/vector_v6.py index 7154851..6ae4dcf 100644 --- a/16-op-overloading/vector_v6.py +++ b/16-op-overloading/vector_v6.py @@ -305,14 +305,16 @@ def __getitem__(self, key): index = operator.index(key) return self._components[index] - shortcut_names = 'xyzt' + __match_args__ = ('x', 'y', 'z', 't') def __getattr__(self, name): cls = type(self) - if len(name) == 1: - pos = cls.shortcut_names.find(name) - if 0 <= pos < len(self._components): - return self._components[pos] + try: + pos = cls.__match_args__.index(name) + except ValueError: + pos = -1 + if 0 <= pos < len(self._components): + return self._components[pos] msg = f'{cls.__name__!r} object has no attribute {name!r}' raise AttributeError(msg) diff --git a/16-op-overloading/vector_v7.py b/16-op-overloading/vector_v7.py index e59c896..953622e 100644 --- a/16-op-overloading/vector_v7.py +++ b/16-op-overloading/vector_v7.py @@ -355,14 +355,16 @@ def __getitem__(self, key): index = operator.index(key) return self._components[index] - shortcut_names = 'xyzt' + __match_args__ = ('x', 'y', 'z', 't') def __getattr__(self, name): cls = type(self) - if len(name) == 1: - pos = cls.shortcut_names.find(name) - if 0 <= pos < len(self._components): - return self._components[pos] + try: + pos = cls.__match_args__.index(name) + except ValueError: + pos = -1 + if 0 <= pos < len(self._components): + return self._components[pos] msg = f'{cls.__name__!r} object has no attribute {name!r}' raise AttributeError(msg) diff --git a/16-op-overloading/vector_v8.py b/16-op-overloading/vector_v8.py index ee2cb48..44c05fd 100644 --- a/16-op-overloading/vector_v8.py +++ b/16-op-overloading/vector_v8.py @@ -361,14 +361,16 @@ def __getitem__(self, key): index = operator.index(key) return self._components[index] - shortcut_names = 'xyzt' + __match_args__ = ('x', 'y', 'z', 't') def __getattr__(self, name): cls = type(self) - if len(name) == 1: - pos = cls.shortcut_names.find(name) - if 0 <= pos < len(self._components): - return self._components[pos] + try: + pos = cls.__match_args__.index(name) + except ValueError: + pos = -1 + if 0 <= pos < len(self._components): + return self._components[pos] msg = f'{cls.__name__!r} object has no attribute {name!r}' raise AttributeError(msg) diff --git a/17-it-generator/aritprog_v1.py b/17-it-generator/aritprog_v1.py index 2ae66c4..69259bc 100644 --- a/17-it-generator/aritprog_v1.py +++ b/17-it-generator/aritprog_v1.py @@ -19,7 +19,7 @@ >>> from decimal import Decimal >>> ap = ArithmeticProgression(0, Decimal('.1'), .3) >>> list(ap) - [Decimal('0.0'), Decimal('0.1'), Decimal('0.2')] + [Decimal('0'), Decimal('0.1'), Decimal('0.2')] # end::ARITPROG_CLASS_DEMO[] """ diff --git a/17-it-generator/aritprog_v2.py b/17-it-generator/aritprog_v2.py index be211d8..42e27f8 100644 --- a/17-it-generator/aritprog_v2.py +++ b/17-it-generator/aritprog_v2.py @@ -14,7 +14,7 @@ >>> from decimal import Decimal >>> ap = aritprog_gen(0, Decimal('.1'), .3) >>> list(ap) - [Decimal('0.0'), Decimal('0.1'), Decimal('0.2')] + [Decimal('0'), Decimal('0.1'), Decimal('0.2')] """ diff --git a/17-it-generator/aritprog_v3.py b/17-it-generator/aritprog_v3.py index 3dd8e18..914f5c9 100644 --- a/17-it-generator/aritprog_v3.py +++ b/17-it-generator/aritprog_v3.py @@ -5,7 +5,7 @@ def aritprog_gen(begin, step, end=None): first = type(begin + step)(begin) ap_gen = itertools.count(first, step) - if end is not None: - ap_gen = itertools.takewhile(lambda n: n < end, ap_gen) - return ap_gen + if end is None: + return ap_gen + return itertools.takewhile(lambda n: n < end, ap_gen) # end::ARITPROG_ITERTOOLS[] diff --git a/17-it-generator/columnize_iter.py b/17-it-generator/columnize_iter.py index c9a6f7d..4362bc1 100644 --- a/17-it-generator/columnize_iter.py +++ b/17-it-generator/columnize_iter.py @@ -1,26 +1,27 @@ # tag::COLUMNIZE[] -from typing import Sequence, Tuple, Iterator +from collections.abc import Sequence, Iterator -def columnize(sequence: Sequence[str], num_columns: int = 0) -> Iterator[Tuple[str, ...]]: +def columnize( + sequence: Sequence[str], num_columns: int = 0 +) -> Iterator[tuple[str, ...]]: # <1> if num_columns == 0: - num_columns = round(len(sequence) ** .5) - num_rows, reminder = divmod(len(sequence), num_columns) - num_rows += bool(reminder) - return (tuple(sequence[i::num_rows]) for i in range(num_rows)) + num_columns = round(len(sequence) ** 0.5) + num_rows, remainder = divmod(len(sequence), num_columns) + num_rows += bool(remainder) + return (tuple(sequence[i::num_rows]) for i in range(num_rows)) # <2> # end::COLUMNIZE[] - def demo() -> None: - nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India' - ' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo' - ' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu' - ).split() + nato = ( + 'Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India' + ' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo' + ' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu' + ).split() for row in columnize(nato, 4): for word in row: print(f'{word:15}', end='') print() - if __name__ == '__main__': demo() diff --git a/17-it-generator/coroaverager.py b/17-it-generator/coroaverager.py new file mode 100644 index 0000000..d976760 --- /dev/null +++ b/17-it-generator/coroaverager.py @@ -0,0 +1,43 @@ +""" +A coroutine to compute a running average + +# tag::CORO_AVERAGER_TEST[] + >>> coro_avg = averager() # <1> + >>> next(coro_avg) # <2> + 0.0 + >>> coro_avg.send(10) # <3> + 10.0 + >>> coro_avg.send(30) + 20.0 + >>> coro_avg.send(5) + 15.0 + +# end::CORO_AVERAGER_TEST[] +# tag::CORO_AVERAGER_TEST_CONT[] + + >>> coro_avg.send(20) # <1> + 16.25 + >>> coro_avg.close() # <2> + >>> coro_avg.close() # <3> + >>> coro_avg.send(5) # <4> + Traceback (most recent call last): + ... + StopIteration + +# end::CORO_AVERAGER_TEST_CONT[] + +""" + +# tag::CORO_AVERAGER[] +from collections.abc import Generator + +def averager() -> Generator[float, float, None]: # <1> + total = 0.0 + count = 0 + average = 0.0 + while True: # <2> + term = yield average # <3> + total += term + count += 1 + average = total/count +# end::CORO_AVERAGER[] diff --git a/17-it-generator/coroaverager2.py b/17-it-generator/coroaverager2.py new file mode 100644 index 0000000..3a15f5e --- /dev/null +++ b/17-it-generator/coroaverager2.py @@ -0,0 +1,96 @@ +""" +A coroutine to compute a running average. + +Testing ``averager2`` by itself:: + +# tag::RETURNING_AVERAGER_DEMO_1[] + + >>> coro_avg = averager2() + >>> next(coro_avg) + >>> coro_avg.send(10) # <1> + >>> coro_avg.send(30) + >>> coro_avg.send(6.5) + >>> coro_avg.close() # <2> + +# end::RETURNING_AVERAGER_DEMO_1[] + +Catching `StopIteration` to extract the value returned by +the coroutine:: + +# tag::RETURNING_AVERAGER_DEMO_2[] + + >>> coro_avg = averager2() + >>> next(coro_avg) + >>> coro_avg.send(10) + >>> coro_avg.send(30) + >>> coro_avg.send(6.5) + >>> try: + ... coro_avg.send(STOP) # <1> + ... except StopIteration as exc: + ... result = exc.value # <2> + ... + >>> result # <3> + Result(count=3, average=15.5) + +# end::RETURNING_AVERAGER_DEMO_2[] + +Using `yield from`: + + +# tag::RETURNING_AVERAGER_DEMO_3[] + + >>> def compute(): + ... res = yield from averager2(True) # <1> + ... print('computed:', res) # <2> + ... return res # <3> + ... + >>> comp = compute() # <4> + >>> for v in [None, 10, 20, 30, STOP]: # <5> + ... try: + ... comp.send(v) # <6> + ... except StopIteration as exc: # <7> + ... result = exc.value + received: 10 + received: 20 + received: 30 + received: + computed: Result(count=3, average=20.0) + >>> result # <8> + Result(count=3, average=20.0) + +# end::RETURNING_AVERAGER_DEMO_3[] +""" + +# tag::RETURNING_AVERAGER_TOP[] +from collections.abc import Generator +from typing import Union, NamedTuple + +class Result(NamedTuple): # <1> + count: int # type: ignore # <2> + average: float + +class Sentinel: # <3> + def __repr__(self): + return f'' + +STOP = Sentinel() # <4> + +SendType = Union[float, Sentinel] # <5> +# end::RETURNING_AVERAGER_TOP[] +# tag::RETURNING_AVERAGER[] +def averager2(verbose: bool = False) -> Generator[None, SendType, Result]: # <1> + total = 0.0 + count = 0 + average = 0.0 + while True: + term = yield # <2> + if verbose: + print('received:', term) + if isinstance(term, Sentinel): # <3> + break + total += term # <4> + count += 1 + average = total / count + return Result(count, average) # <5> + +# end::RETURNING_AVERAGER[] diff --git a/17-it-generator/fibo_gen.py b/17-it-generator/fibo_gen.py new file mode 100644 index 0000000..d7296ac --- /dev/null +++ b/17-it-generator/fibo_gen.py @@ -0,0 +1,7 @@ +from collections.abc import Iterator + +def fibonacci() -> Iterator[int]: + a, b = 0, 1 + while True: + yield a + a, b = b, a + b diff --git a/17-it-generator/iter_gen_type.py b/17-it-generator/iter_gen_type.py new file mode 100644 index 0000000..1730d87 --- /dev/null +++ b/17-it-generator/iter_gen_type.py @@ -0,0 +1,13 @@ +from collections.abc import Iterator +from keyword import kwlist +from typing import TYPE_CHECKING + +short_kw = (k for k in kwlist if len(k) < 5) # <1> + +if TYPE_CHECKING: + reveal_type(short_kw) # <2> + +long_kw: Iterator[str] = (k for k in kwlist if len(k) >= 4) # <3> + +if TYPE_CHECKING: # <4> + reveal_type(long_kw) diff --git a/17-it-generator/sentence_gen.py b/17-it-generator/sentence_gen.py index 3dbc606..1f7ebeb 100644 --- a/17-it-generator/sentence_gen.py +++ b/17-it-generator/sentence_gen.py @@ -21,7 +21,7 @@ def __repr__(self): def __iter__(self): for word in self.words: # <1> yield word # <2> - return # <3> + # <3> # done! <4> diff --git a/17-it-generator/tree/classtree/classtree.py b/17-it-generator/tree/classtree/classtree.py new file mode 100755 index 0000000..56bbec3 --- /dev/null +++ b/17-it-generator/tree/classtree/classtree.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +from importlib import import_module +import sys + + +SP = '\N{SPACE}' +HLIN = '\N{BOX DRAWINGS LIGHT HORIZONTAL}' * 2 + SP # ── +VLIN = '\N{BOX DRAWINGS LIGHT VERTICAL}' + SP * 3 # │ +TEE = '\N{BOX DRAWINGS LIGHT VERTICAL AND RIGHT}' + HLIN # ├── +ELBOW = '\N{BOX DRAWINGS LIGHT UP AND RIGHT}' + HLIN # └── + + +def subclasses(cls): + try: + return cls.__subclasses__() + except TypeError: # handle the `type` type + return cls.__subclasses__(cls) + + +def tree(cls, level=0, last_sibling=True): + yield cls, level, last_sibling + chidren = subclasses(cls) + if chidren: + last = chidren[-1] + for child in chidren: + yield from tree(child, level + 1, child is last) + + +def render_lines(tree_generator): + cls, _, _ = next(tree_generator) + yield cls.__name__ + prefix = '' + for cls, level, last in tree_generator: + prefix = prefix[: 4 * (level - 1)] + prefix = prefix.replace(TEE, VLIN).replace(ELBOW, SP * 4) + prefix += ELBOW if last else TEE + yield prefix + cls.__name__ + + +def draw(cls): + for line in render_lines(tree(cls)): + print(line) + + +def parse(name): + if '.' in name: + return name.rsplit('.', 1) + else: + return 'builtins', name + + +def main(name): + module_name, cls_name = parse(name) + try: + cls = getattr(import_module(module_name), cls_name) + except ModuleNotFoundError: + print(f'*** Could not import {module_name!r}.') + except AttributeError: + print(f'*** {cls_name!r} not found in {module_name!r}.') + else: + if isinstance(cls, type): + draw(cls) + else: + print(f'*** {cls_name!r} is not a class.') + + +if __name__ == '__main__': + if len(sys.argv) == 2: + main(sys.argv[1]) + else: + print('Usage:' + f'\t{sys.argv[0]} Class # for builtin classes\n' + f'\t{sys.argv[0]} package.Class # for other classes' + ) diff --git a/17-it-generator/tree/classtree/classtree_test.py b/17-it-generator/tree/classtree/classtree_test.py new file mode 100644 index 0000000..4535747 --- /dev/null +++ b/17-it-generator/tree/classtree/classtree_test.py @@ -0,0 +1,181 @@ +from textwrap import dedent +from typing import SupportsBytes +from classtree import tree, render_lines, main, subclasses + +from abc import ABCMeta + + +def test_subclasses(): + result = subclasses(UnicodeError) + assert set(result) >= {UnicodeEncodeError, UnicodeDecodeError} + + +def test_subclasses_of_type(): + """ + The `type` class is a special case because `type.__subclasses__()` + is an unbound method when called on it, so we must call it as + `type.__subclasses__(type)` just for `type`. + + This test does not verify the full list of results, but just + checks that `abc.ABCMeta` is included, because that's the only + subclass of `type` (i.e. metaclass) I we get when I run + `$ classtree.py type` at the command line. + + However, the Python console and `pytest` both load other modules, + so `subclasses` may find more subclasses of `type`—for example, + `enum.EnumMeta`. + """ + result = subclasses(type) + assert ABCMeta in result + + +def test_tree_1_level(): + result = list(tree(TabError)) + assert result == [(TabError, 0, True)] + + +def test_tree_2_levels(): + result = list(tree(IndentationError)) + assert result == [ + (IndentationError, 0, True), + (TabError, 1, True), + ] + + +def test_render_lines_1_level(): + result = list(render_lines(tree(TabError))) + assert result == ['TabError'] + + +def test_render_lines_2_levels_1_leaf(): + result = list(render_lines(tree(IndentationError))) + expected = [ + 'IndentationError', + '└── TabError', + ] + assert expected == result + + +def test_render_lines_3_levels_1_leaf(): + class X: pass + class Y(X): pass + class Z(Y): pass + result = list(render_lines(tree(X))) + expected = [ + 'X', + '└── Y', + ' └── Z', + ] + assert expected == result + + +def test_render_lines_4_levels_1_leaf(): + class Level0: pass + class Level1(Level0): pass + class Level2(Level1): pass + class Level3(Level2): pass + + result = list(render_lines(tree(Level0))) + expected = [ + 'Level0', + '└── Level1', + ' └── Level2', + ' └── Level3', + ] + assert expected == result + + +def test_render_lines_2_levels_2_leaves(): + class Branch: pass + class Leaf1(Branch): pass + class Leaf2(Branch): pass + result = list(render_lines(tree(Branch))) + expected = [ + 'Branch', + '├── Leaf1', + '└── Leaf2', + ] + assert expected == result + + +def test_render_lines_3_levels_2_leaves_dedent(): + class A: pass + class B(A): pass + class C(B): pass + class D(A): pass + class E(D): pass + + result = list(render_lines(tree(A))) + expected = [ + 'A', + '├── B', + '│ └── C', + '└── D', + ' └── E', + ] + assert expected == result + + +def test_render_lines_4_levels_4_leaves_dedent(): + class A: pass + class B1(A): pass + class C1(B1): pass + class D1(C1): pass + class D2(C1): pass + class C2(B1): pass + class B2(A): pass + expected = [ + 'A', + '├── B1', + '│ ├── C1', + '│ │ ├── D1', + '│ │ └── D2', + '│ └── C2', + '└── B2', + ] + + result = list(render_lines(tree(A))) + assert expected == result + + +def test_main_simple(capsys): + main('IndentationError') + expected = dedent(""" + IndentationError + └── TabError + """).lstrip() + captured = capsys.readouterr() + assert captured.out == expected + + +def test_main_dotted(capsys): + main('collections.abc.Sequence') + expected = dedent(""" + Sequence + ├── ByteString + ├── MutableSequence + │ └── UserList + """).lstrip() + captured = capsys.readouterr() + assert captured.out.startswith(expected) + + +def test_main_class_not_found(capsys): + main('NoSuchClass') + expected = "*** 'NoSuchClass' not found in 'builtins'.\n" + captured = capsys.readouterr() + assert captured.out == expected + + +def test_main_module_not_found(capsys): + main('nosuch.module') + expected = "*** Could not import 'nosuch'.\n" + captured = capsys.readouterr() + assert captured.out == expected + + +def test_main_not_a_class(capsys): + main('collections.abc') + expected = "*** 'abc' is not a class.\n" + captured = capsys.readouterr() + assert captured.out == expected diff --git a/17-it-generator/tree/extra/drawtree.py b/17-it-generator/tree/extra/drawtree.py new file mode 100644 index 0000000..b74cd33 --- /dev/null +++ b/17-it-generator/tree/extra/drawtree.py @@ -0,0 +1,44 @@ +from tree import tree + +SP = '\N{SPACE}' +HLIN = '\N{BOX DRAWINGS LIGHT HORIZONTAL}' * 2 + SP # ── +VLIN = '\N{BOX DRAWINGS LIGHT VERTICAL}' + SP * 3 # │ +TEE = '\N{BOX DRAWINGS LIGHT VERTICAL AND RIGHT}' + HLIN # ├── +ELBOW = '\N{BOX DRAWINGS LIGHT UP AND RIGHT}' + HLIN # └── + + +def subclasses(cls): + try: + return cls.__subclasses__() + except TypeError: # handle the `type` type + return cls.__subclasses__(cls) + + +def tree(cls, level=0, last_sibling=True): + yield cls, level, last_sibling + children = subclasses(cls) + if children: + last = children[-1] + for child in children: + yield from tree(child, level+1, child is last) + + +def render_lines(tree_iter): + cls, _, _ = next(tree_iter) + yield cls.__name__ + prefix = '' + + for cls, level, last in tree_iter: + prefix = prefix[:4 * (level - 1)] + prefix = prefix.replace(TEE, VLIN).replace(ELBOW, SP * 4) + prefix += ELBOW if last else TEE + yield prefix + cls.__name__ + + +def draw(cls): + for line in render_lines(tree(cls)): + print(line) + + +if __name__ == '__main__': + draw(BaseException) diff --git a/17-it-generator/tree/extra/pretty_tree.py b/17-it-generator/tree/extra/pretty_tree.py deleted file mode 100644 index 6d6d91a..0000000 --- a/17-it-generator/tree/extra/pretty_tree.py +++ /dev/null @@ -1,35 +0,0 @@ -from tree import tree - -SPACES = ' ' * 4 -HLINE = '\u2500' # ─ BOX DRAWINGS LIGHT HORIZONTAL -HLINE2 = HLINE * 2 -ELBOW = f'\u2514{HLINE2} ' # └ BOX DRAWINGS LIGHT UP AND RIGHT -TEE = f'\u251C{HLINE2} ' # ├ BOX DRAWINGS LIGHT VERTICAL AND RIGHT -PIPE = '\u2502 ' # │ BOX DRAWINGS LIGHT VERTICAL - - -def render_lines(tree_iter): - name, _, _ = next(tree_iter) - yield name - prefix = '' - - for name, level, last in tree_iter: - if last: - connector = ELBOW - else: - connector = TEE - - prefix = prefix[:4 * (level-1)] - prefix = prefix.replace(TEE, PIPE).replace(ELBOW, SPACES) - prefix += connector - - yield prefix + name - - -def display(cls): - for line in render_lines(tree(cls)): - print(line) - - -if __name__ == '__main__': - display(BaseException) diff --git a/17-it-generator/tree/extra/test_pretty_tree.py b/17-it-generator/tree/extra/test_drawtree.py similarity index 94% rename from 17-it-generator/tree/extra/test_pretty_tree.py rename to 17-it-generator/tree/extra/test_drawtree.py index 70019a3..f66dfa4 100644 --- a/17-it-generator/tree/extra/test_pretty_tree.py +++ b/17-it-generator/tree/extra/test_drawtree.py @@ -1,4 +1,4 @@ -from pretty_tree import tree, render_lines +from drawtree import tree, render_lines def test_1_level(): result = list(render_lines(tree(BrokenPipeError))) @@ -59,7 +59,7 @@ class Leaf2(Branch): pass assert expected == result -def test_3_levels_2_leaves(): +def test_3_levels_2_leaves_dedent(): class A: pass class B(A): pass class C(B): pass @@ -77,7 +77,7 @@ class E(D): pass assert expected == result -def test_4_levels_4_leaves(): +def test_4_levels_4_leaves_dedent(): class A: pass class B1(A): pass class C1(B1): pass diff --git a/17-it-generator/tree/extra/test_tree.py b/17-it-generator/tree/extra/test_tree.py index 4c6404c..b06cf3b 100644 --- a/17-it-generator/tree/extra/test_tree.py +++ b/17-it-generator/tree/extra/test_tree.py @@ -4,7 +4,8 @@ def test_1_level(): class One: pass expected = [('One', 0, True)] - result = list(tree(One)) + result = [(cls.__name__, level, last) + for cls, level, last in tree(One)] assert expected == result @@ -17,7 +18,8 @@ class Leaf2(Branch): pass ('Leaf1', 1, False), ('Leaf2', 1, True), ] - result = list(tree(Branch)) + result = [(cls.__name__, level, last) + for cls, level, last in tree(Branch)] assert expected == result @@ -30,7 +32,8 @@ class Z(Y): pass ('Y', 1, True), ('Z', 2, True), ] - result = list(tree(X)) + result = [(cls.__name__, level, last) + for cls, level, last in tree(X)] assert expected == result @@ -46,7 +49,8 @@ class Level3(Level2): pass ('Level3', 3, True), ] - result = list(tree(Level0)) + result = [(cls.__name__, level, last) + for cls, level, last in tree(Level0)] assert expected == result @@ -68,7 +72,8 @@ class D2(C1): pass ('C2', 2, True), ] - result = list(tree(A)) + result = [(cls.__name__, level, last) + for cls, level, last in tree(A)] assert expected == result @@ -83,7 +88,8 @@ class Root: pass expected.append((name, level, True)) parent = cls - result = list(tree(Root)) + result = [(cls.__name__, level, last) + for cls, level, last in tree(Root)] assert len(result) == level_count assert result[0] == ('Root', 0, True) assert result[-1] == ('Sub99', 99, True) diff --git a/17-it-generator/tree/extra/tree.py b/17-it-generator/tree/extra/tree.py index 7169a28..aeebd37 100644 --- a/17-it-generator/tree/extra/tree.py +++ b/17-it-generator/tree/extra/tree.py @@ -1,5 +1,5 @@ -def tree(cls, level=0, last_in_level=True): - yield cls.__name__, level, last_in_level +def tree(cls, level=0, last_sibling=True): + yield cls, level, last_sibling subclasses = cls.__subclasses__() if subclasses: last = subclasses[-1] @@ -8,9 +8,9 @@ def tree(cls, level=0, last_in_level=True): def display(cls): - for cls_name, level, _ in tree(cls): + for cls, level, _ in tree(cls): indent = ' ' * 4 * level - print(f'{indent}{cls_name}') + print(f'{indent}{cls.__name__}') if __name__ == '__main__': diff --git a/17-it-generator/tree/step2/tree.py b/17-it-generator/tree/step2/tree.py index d4078b4..6a0e1fa 100644 --- a/17-it-generator/tree/step2/tree.py +++ b/17-it-generator/tree/step2/tree.py @@ -9,7 +9,7 @@ def sub_tree(cls): def display(cls): - for cls_name, level in tree(cls): + for cls_name, level in tree(cls): # <3> indent = ' ' * 4 * level print(f'{indent}{cls_name}') diff --git a/18-with-match/lisplus/examples_test.py b/18-with-match/lisplus/examples_test.py deleted file mode 100644 index 38510a3..0000000 --- a/18-with-match/lisplus/examples_test.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -Doctests for `parse`: - ->>> from lis import parse - -# tag::PARSE_DEMO[] ->>> parse('1.5') # <1> -1.5 ->>> parse('set!') # <2> -'set!' ->>> parse('(gcd 18 44)') # <3> -['gcd', 18, 44] ->>> parse('(- m (* n (// m n)))') # <4> -['-', 'm', ['*', 'n', ['//', 'm', 'n']]] - -# end::PARSE_DEMO[] - -""" - -import math - -from lis import run - - -fact_src = """ -(define (! n) - (if (< n 2) - 1 - (* n (! (- n 1))) - ) -) -(! 42) -""" -def test_factorial(): - got = run(fact_src) - assert got == 1405006117752879898543142606244511569936384000000000 - assert got == math.factorial(42) - - -gcd_src = """ -(define (mod m n) - (- m (* n (// m n)))) -(define (gcd m n) - (if (= n 0) - m - (gcd n (mod m n)))) -(gcd 18 45) -""" -def test_gcd(): - got = run(gcd_src) - assert got == 9 - - -quicksort_src = """ -(define (quicksort lst) - (if (null? lst) - lst - (begin - (define pivot (car lst)) - (define rest (cdr lst)) - (append - (quicksort - (filter (lambda (x) (< x pivot)) rest)) - (list pivot) - (quicksort - (filter (lambda (x) (>= x pivot)) rest))) - ) - ) -) -(quicksort (list 2 1 6 3 4 0 8 9 7 5)) -""" -def test_quicksort(): - got = run(quicksort_src) - assert got == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - - -# Example from Structure and Interpretation of Computer Programs -# https://mitpress.mit.edu/sites/default/files/sicp/full-text/sicp/book/node12.html - -newton_src = """ -(define (sqrt x) - (sqrt-iter 1.0 x)) -(define (sqrt-iter guess x) - (if (good-enough? guess x) - guess - (sqrt-iter (improve guess x) x))) -(define (good-enough? guess x) - (< (abs (- (* guess guess) x)) 0.001)) -(define (improve guess x) - (average guess (/ x guess))) -(define (average x y) - (/ (+ x y) 2)) -(sqrt 123454321) -""" -def test_newton(): - got = run(newton_src) - assert math.isclose(got, 11111) - - -closure_src = """ -(define (make-adder increment) - (lambda (x) (+ increment x)) -) -(define inc (make-adder 1)) -(inc 99) -""" -def test_newton(): - got = run(closure_src) - assert got == 100 diff --git a/18-with-match/lisplus/meta_test.py b/18-with-match/lisplus/meta_test.py deleted file mode 100644 index cb3d062..0000000 --- a/18-with-match/lisplus/meta_test.py +++ /dev/null @@ -1,64 +0,0 @@ -import operator as op - -from lis import run - -env_scm = """ -(define standard-env (list - (list (quote +) +) - (list (quote -) -) -)) -standard-env -""" - -def test_env_build(): - got = run(env_scm) - assert got == [['+', op.add], ['-', op.sub]] - -scan_scm = """ -(define l (quote (a b c))) -(define (scan what where) - (if (null? where) - () - (if (eq? what (car where)) - what - (scan what (cdr where)))) -) -""" - -def test_scan(): - source = scan_scm + '(scan (quote a) l )' - got = run(source) - assert got == 'a' - - -def test_scan_not_found(): - source = scan_scm + '(scan (quote z) l )' - got = run(source) - assert got == [] - - -lookup_scm = """ -(define env (list - (list (quote +) +) - (list (quote -) -) -)) -(define (lookup what where) - (if (null? where) - () - (if (eq? what (car (car where))) - (car (cdr (car where))) - (lookup what (cdr where)))) -) -""" - -def test_lookup(): - source = lookup_scm + '(lookup (quote +) env)' - got = run(source) - assert got == op.add - - -def test_lookup_not_found(): - source = lookup_scm + '(lookup (quote z) env )' - got = run(source) - assert got == [] - diff --git a/02-array-seq/lispy/LICENSE b/18-with-match/lispy/LICENSE similarity index 100% rename from 02-array-seq/lispy/LICENSE rename to 18-with-match/lispy/LICENSE diff --git a/02-array-seq/lispy/README.md b/18-with-match/lispy/README.md similarity index 64% rename from 02-array-seq/lispy/README.md rename to 18-with-match/lispy/README.md index 9ac6f56..38458bb 100644 --- a/02-array-seq/lispy/README.md +++ b/18-with-match/lispy/README.md @@ -23,4 +23,14 @@ The copyright holder is Peter Norvig and the code is licensed under the [MIT license](https://github.com/norvig/pytudes/blob/60168bce8cdfacf57c92a5b2979f0b2e95367753/LICENSE). +## Changes to Norvig's code + +I made small changes to the programs in `original/`: + +* In `lis.py`: + * The `Procedure` class accepts a list of expressions as the `body`, and `__call__` evaluates those expressions in order, and returns the value of the last. This is consistent with Scheme's `lambda` syntax and provided a useful example for pattern matching. + * In the `elif` block for `'lambda'`, I added the `*` in front of the `*body` variable in the tuple unpacking to capture the expressions as a list, before calling the `Procedure` constructor. + +* In `lispy.py` I made [changes and a pull request](https://github.com/norvig/pytudes/pull/106) to make it run on Python 3. + _Luciano Ramalho
June 29, 2021_ diff --git a/02-array-seq/lispy/original/LICENSE b/18-with-match/lispy/original/LICENSE similarity index 100% rename from 02-array-seq/lispy/original/LICENSE rename to 18-with-match/lispy/original/LICENSE diff --git a/02-array-seq/lispy/original/README.md b/18-with-match/lispy/original/README.md similarity index 100% rename from 02-array-seq/lispy/original/README.md rename to 18-with-match/lispy/original/README.md diff --git a/02-array-seq/lispy/original/lis.py b/18-with-match/lispy/original/lis.py similarity index 100% rename from 02-array-seq/lispy/original/lis.py rename to 18-with-match/lispy/original/lis.py diff --git a/02-array-seq/lispy/original/lispy.py b/18-with-match/lispy/original/lispy.py similarity index 100% rename from 02-array-seq/lispy/original/lispy.py rename to 18-with-match/lispy/original/lispy.py diff --git a/02-array-seq/lispy/original/lispytest.py b/18-with-match/lispy/original/lispytest.py similarity index 100% rename from 02-array-seq/lispy/original/lispytest.py rename to 18-with-match/lispy/original/lispytest.py diff --git a/18-with-match/lispy/py3.10/examples_test.py b/18-with-match/lispy/py3.10/examples_test.py new file mode 100644 index 0000000..7025dad --- /dev/null +++ b/18-with-match/lispy/py3.10/examples_test.py @@ -0,0 +1,259 @@ +""" +Doctests for `parse` +-------------------- + +# tag::PARSE[] +>>> from lis import parse +>>> parse('1.5') +1.5 +>>> parse('ni!') +'ni!' +>>> parse('(gcd 18 45)') +['gcd', 18, 45] +>>> parse(''' +... (define double +... (lambda (n) +... (* n 2))) +... ''') +['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]] + +# end::PARSE[] + +Doctest for `Environment` +------------------------- + +# tag::ENVIRONMENT[] +>>> from lis import Environment +>>> inner_env = {'a': 2} +>>> outer_env = {'a': 0, 'b': 1} +>>> env = Environment(inner_env, outer_env) +>>> env['a'] # <1> +2 +>>> env['a'] = 111 # <2> +>>> env['c'] = 222 +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1}) +>>> env.change('b', 333) # <3> +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333}) + +# end::ENVIRONMENT[] + +Doctests for `evaluate` +----------------------- + +# tag::EVAL_NUMBER[] +>>> from lis import parse, evaluate, standard_env +>>> evaluate(parse('1.5'), {}) +1.5 + +# end::EVAL_NUMBER[] + +# tag::EVAL_SYMBOL[] +>>> evaluate(parse('+'), standard_env()) + +>>> evaluate(parse('ni!'), standard_env()) +Traceback (most recent call last): + ... +KeyError: 'ni!' + +# end::EVAL_SYMBOL[] + + +# tag::EVAL_QUOTE[] +>>> evaluate(parse('(quote no-such-name)'), standard_env()) +'no-such-name' +>>> evaluate(parse('(quote (99 bottles of beer))'), standard_env()) +[99, 'bottles', 'of', 'beer'] +>>> evaluate(parse('(quote (/ 10 0))'), standard_env()) +['/', 10, 0] + +# end::EVAL_QUOTE[] + +# tag::EVAL_IF[] +>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env()) +1 +>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env()) +0 + +# end::EVAL_IF[] + + +# tag::EVAL_LAMBDA[] +>>> expr = '(lambda (a b) (* (/ a b) 100))' +>>> f = evaluate(parse(expr), standard_env()) +>>> f # doctest: +ELLIPSIS + +>>> f(15, 20) +75.0 + +# end::EVAL_LAMBDA[] + +# tag::EVAL_DEFINE[] +>>> global_env = standard_env() +>>> evaluate(parse('(define answer (* 7 6))'), global_env) +>>> global_env['answer'] +42 + +# end::EVAL_DEFINE[] + +# tag::EVAL_DEFUN[] +>>> global_env = standard_env() +>>> percent = '(define (% a b) (* (/ a b) 100))' +>>> evaluate(parse(percent), global_env) +>>> global_env['%'] # doctest: +ELLIPSIS + +>>> global_env['%'](170, 200) +85.0 + +# end::EVAL_DEFUN[] + +function call: + +# tag::EVAL_CALL[] +>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env) +42.0 + +# end::EVAL_CALL[] + +# tag::EVAL_SYNTAX_ERROR[] +>>> evaluate(parse('(lambda is not like this)'), standard_env()) +Traceback (most recent call last): + ... +SyntaxError: (lambda is not like this) + +# end::EVAL_SYNTAX_ERROR[] + +""" + +import math + +from lis import run + + +fact_src = """ +(define (! n) + (if (< n 2) + 1 + (* n (! (- n 1))) + ) +) +(! 42) +""" +def test_factorial(): + got = run(fact_src) + assert got == 1405006117752879898543142606244511569936384000000000 + assert got == math.factorial(42) + + +gcd_src = """ +(define (mod m n) + (- m (* n (quotient m n)))) +(define (gcd m n) + (if (= n 0) + m + (gcd n (mod m n)))) +(display (gcd 18 45)) +""" +def test_gcd(capsys): + run(gcd_src) + captured = capsys.readouterr() + assert captured.out == '9\n' + + +quicksort_src = """ +(define (quicksort lst) + (if (null? lst) + lst + (begin + (define pivot (car lst)) + (define rest (cdr lst)) + (append + (quicksort + (filter (lambda (x) (< x pivot)) rest)) + (list pivot) + (quicksort + (filter (lambda (x) (>= x pivot)) rest))) + ) + ) +) +(quicksort (list 2 1 6 3 4 0 8 9 7 5)) +""" +def test_quicksort(): + got = run(quicksort_src) + assert got == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + +# Example from Structure and Interpretation of Computer Programs +# https://mitpress.mit.edu/sites/default/files/sicp/full-text/sicp/book/node12.html + +newton_src = """ +(define (sqrt x) + (sqrt-iter 1.0 x)) +(define (sqrt-iter guess x) + (if (good-enough? guess x) + guess + (sqrt-iter (improve guess x) x))) +(define (good-enough? guess x) + (< (abs (- (* guess guess) x)) 0.001)) +(define (improve guess x) + (average guess (/ x guess))) +(define (average x y) + (/ (+ x y) 2)) +(sqrt 123454321) +""" +def test_newton(): + got = run(newton_src) + assert math.isclose(got, 11111) + + +closure_src = """ +(define (make-adder increment) + (lambda (x) (+ increment x)) +) +(define inc (make-adder 1)) +(inc 99) +""" +def test_closure(): + got = run(closure_src) + assert got == 100 + +closure_with_change_src = """ +(define (make-counter) + (define n 0) + (lambda () + (set! n (+ n 1)) + n) +) +(define counter (make-counter)) +(display (counter)) +(display (counter)) +(display (counter)) +""" +def test_closure_with_change(capsys): + run(closure_with_change_src) + captured = capsys.readouterr() + assert captured.out == '1\n2\n3\n' + + + +# tag::RUN_AVERAGER[] +closure_averager_src = """ +(define (make-averager) + (define count 0) + (define total 0) + (lambda (new-value) + (set! count (+ count 1)) + (set! total (+ total new-value)) + (/ total count) + ) +) +(define avg (make-averager)) +(avg 10) +(avg 11) +(avg 15) +""" +def test_closure_averager(): + got = run(closure_averager_src) + assert got == 12.0 +# end::RUN_AVERAGER[] diff --git a/18-with-match/lisplus/lis.py b/18-with-match/lispy/py3.10/lis.py old mode 100644 new mode 100755 similarity index 62% rename from 18-with-match/lisplus/lis.py rename to 18-with-match/lispy/py3.10/lis.py index 7ae0a9d..f2d982c --- a/18-with-match/lisplus/lis.py +++ b/18-with-match/lispy/py3.10/lis.py @@ -1,54 +1,89 @@ +#!/usr/bin/env python + ################ Lispy: Scheme Interpreter in Python 3.10 ## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html ## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021) ## by Luciano Ramalho, adding type hints and pattern matching. -################ imports and types +################ Imports and Types + +# tag::IMPORTS[] import math import operator as op from collections import ChainMap -from collections.abc import MutableMapping, Iterator from itertools import chain -from typing import Any, TypeAlias +from typing import Any, TypeAlias, NoReturn Symbol: TypeAlias = str Atom: TypeAlias = float | int | Symbol Expression: TypeAlias = Atom | list +# end::IMPORTS[] -Environment: TypeAlias = MutableMapping[Symbol, object] +################ Parsing: parse, tokenize, and read_from_tokens -class Procedure: - "A user-defined Scheme procedure." +def parse(program: str) -> Expression: + "Read a Scheme expression from a string." + return read_from_tokens(tokenize(program)) - def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment): - self.parms = parms - self.body = body - self.env = env +def tokenize(s: str) -> list[str]: + "Convert a string into a list of tokens." + return s.replace('(', ' ( ').replace(')', ' ) ').split() + +def read_from_tokens(tokens: list[str]) -> Expression: + "Read an expression from a sequence of tokens." + if len(tokens) == 0: + raise SyntaxError('unexpected EOF while reading') + token = tokens.pop(0) + if '(' == token: + exp = [] + while tokens[0] != ')': + exp.append(read_from_tokens(tokens)) + tokens.pop(0) # discard ')' + return exp + elif ')' == token: + raise SyntaxError('unexpected )') + else: + return parse_atom(token) + +def parse_atom(token: str) -> Atom: + "Numbers become numbers; every other token is a symbol." + try: + return int(token) + except ValueError: + try: + return float(token) + except ValueError: + return Symbol(token) - def __call__(self, *args: Expression) -> Any: - local_env = dict(zip(self.parms, args)) - env: Environment = ChainMap(local_env, self.env) - for exp in self.body: - result = evaluate(exp, env) - return result +################ Global Environment -################ global environment +# tag::ENV_CLASS[] +class Environment(ChainMap[Symbol, Any]): + "A ChainMap that allows changing an item in-place." + def change(self, key: Symbol, value: Any) -> None: + "Find where key is defined and change the value there." + for map in self.maps: + if key in map: + map[key] = value # type: ignore[index] + return + raise KeyError(key) +# end::ENV_CLASS[] def standard_env() -> Environment: "An environment with some Scheme standard procedures." - env: Environment = {} + env = Environment() env.update(vars(math)) # sin, cos, sqrt, pi, ... env.update({ '+': op.add, '-': op.sub, '*': op.mul, '/': op.truediv, - '//': op.floordiv, + 'quotient': op.floordiv, '>': op.gt, '<': op.lt, '>=': op.ge, @@ -61,6 +96,7 @@ def standard_env() -> Environment: 'car': lambda x: x[0], 'cdr': lambda x: x[1:], 'cons': lambda x, y: [x] + y, + 'display': lambda x: print(lispstr(x)), 'eq?': op.is_, 'equal?': op.eq, 'filter': lambda *args: list(filter(*args)), @@ -80,70 +116,32 @@ def standard_env() -> Environment: return env -################ parse, tokenize, and read_from_tokens - - -def parse(program: str) -> Expression: - "Read a Scheme expression from a string." - return read_from_tokens(tokenize(program)) - - -def tokenize(s: str) -> list[str]: - "Convert a string into a list of tokens." - return s.replace('(', ' ( ').replace(')', ' ) ').split() +################ Interaction: A REPL - -def read_from_tokens(tokens: list[str]) -> Expression: - "Read an expression from a sequence of tokens." - if len(tokens) == 0: - raise SyntaxError('unexpected EOF while reading') - token = tokens.pop(0) - if '(' == token: - exp = [] - while tokens[0] != ')': - exp.append(read_from_tokens(tokens)) - tokens.pop(0) # discard ')' - return exp - elif ')' == token: - raise SyntaxError('unexpected )') - else: - return parse_atom(token) - - -def parse_atom(token: str) -> Atom: - "Numbers become numbers; every other token is a symbol." - try: - return int(token) - except ValueError: - try: - return float(token) - except ValueError: - return Symbol(token) - - -################ interaction: a REPL - - -def repl(prompt: str = 'lis.py> ') -> None: - "A prompt-read-evaluate-print loop." - global_env: Environment = standard_env() +# tag::REPL[] +def repl(prompt: str = 'lis.py> ') -> NoReturn: + "A prompt-read-eval-print loop." + global_env = Environment({}, standard_env()) while True: - val = evaluate(parse(input(prompt)), global_env) + ast = parse(input(prompt)) + val = evaluate(ast, global_env) if val is not None: print(lispstr(val)) - def lispstr(exp: object) -> str: "Convert a Python object back into a Lisp-readable string." if isinstance(exp, list): return '(' + ' '.join(map(lispstr, exp)) + ')' else: return str(exp) +# end::REPL[] -################ eval +################ Evaluator # tag::EVALUATE[] +KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!'] + def evaluate(exp: Expression, env: Environment) -> Any: "Evaluate an expression in an environment." match exp: @@ -151,42 +149,66 @@ def evaluate(exp: Expression, env: Environment) -> Any: return x case Symbol(var): return env[var] - case []: - return [] - case ['quote', exp]: - return exp + case ['quote', x]: + return x case ['if', test, consequence, alternative]: if evaluate(test, env): return evaluate(consequence, env) else: return evaluate(alternative, env) - case ['define', Symbol(var), value_exp]: - env[var] = evaluate(value_exp, env) - case ['define', [Symbol(name), *parms], *body]: - env[name] = Procedure(parms, body, env) - case ['lambda', [*parms], *body]: + case ['lambda', [*parms], *body] if body: return Procedure(parms, body, env) - case [op, *args]: - proc = evaluate(op, env) + case ['define', Symbol(name), value_exp]: + env[name] = evaluate(value_exp, env) + case ['define', [Symbol(name), *parms], *body] if body: + env[name] = Procedure(parms, body, env) + case ['set!', Symbol(name), value_exp]: + env.change(name, evaluate(value_exp, env)) + case [func_exp, *args] if func_exp not in KEYWORDS: + proc = evaluate(func_exp, env) values = [evaluate(arg, env) for arg in args] return proc(*values) case _: - raise SyntaxError(repr(exp)) + raise SyntaxError(lispstr(exp)) # end::EVALUATE[] +# tag::PROCEDURE[] +class Procedure: + "A user-defined Scheme procedure." + + def __init__( # <1> + self, parms: list[Symbol], body: list[Expression], env: Environment + ): + self.parms = parms # <2> + self.body = body + self.env = env + + def __call__(self, *args: Expression) -> Any: # <3> + local_env = dict(zip(self.parms, args)) # <4> + env = Environment(local_env, self.env) # <5> + for exp in self.body: # <6> + result = evaluate(exp, env) + return result # <7> +# end::PROCEDURE[] -################ non-interactive execution +################ command-line interface -def run_lines(source: str) -> Iterator[Any]: - global_env: Environment = standard_env() +def run(source: str) -> Any: + global_env = Environment({}, standard_env()) tokens = tokenize(source) while tokens: exp = read_from_tokens(tokens) - yield evaluate(exp, global_env) + result = evaluate(exp, global_env) + return result +def main(args: list[str]) -> None: + if len(args) == 1: + with open(args[0]) as fp: + run(fp.read()) + else: + repl() -def run(source: str) -> Any: - for result in run_lines(source): - pass - return result +if __name__ == '__main__': + import sys + main(sys.argv[1:]) diff --git a/18-with-match/lisplus/lis_test.py b/18-with-match/lispy/py3.10/lis_test.py similarity index 97% rename from 18-with-match/lisplus/lis_test.py rename to 18-with-match/lispy/py3.10/lis_test.py index 3688888..03dc15c 100644 --- a/18-with-match/lisplus/lis_test.py +++ b/18-with-match/lispy/py3.10/lis_test.py @@ -73,10 +73,10 @@ def test_evaluate(source: str, expected: Optional[Expression]) -> None: def std_env() -> Environment: return standard_env() -# tests for each of the cases in evaluate +# tests for cases in evaluate def test_evaluate_variable() -> None: - env: Environment = dict(x=10) + env = Environment({'x': 10}) source = 'x' expected = 10 got = evaluate(parse(source), env) @@ -168,8 +168,6 @@ def test_invocation_user_procedure(std_env: Environment) -> None: assert got == 22 -###################################### for py3.10/lis.py only - def test_define_function(std_env: Environment) -> None: source = '(define (max a b) (if (>= a b) a b))' got = evaluate(parse(source), std_env) diff --git a/18-with-match/lispy/py3.10/quicksort.scm b/18-with-match/lispy/py3.10/quicksort.scm new file mode 100644 index 0000000..08dd596 --- /dev/null +++ b/18-with-match/lispy/py3.10/quicksort.scm @@ -0,0 +1,17 @@ +(define (quicksort lst) + (if (null? lst) + lst + (begin + (define pivot (car lst)) + (define rest (cdr lst)) + (append + (quicksort + (filter (lambda (x) (< x pivot)) rest)) + (list pivot) + (quicksort + (filter (lambda (x) (>= x pivot)) rest))) + ) + ) +) +(display + (quicksort (list 2 1 6 3 4 0 8 9 7 5))) diff --git a/02-array-seq/lispy/py3.9-no-hints/README.md b/18-with-match/lispy/py3.9/README.md similarity index 86% rename from 02-array-seq/lispy/py3.9-no-hints/README.md rename to 18-with-match/lispy/py3.9/README.md index 2b6f4ea..fdb1f08 100644 --- a/02-array-seq/lispy/py3.9-no-hints/README.md +++ b/18-with-match/lispy/py3.9/README.md @@ -5,9 +5,9 @@ use in _Fluent Python, Second Edition_, I made a few changes for didactic reason _Luciano Ramalho_ -## Significant changes +## Major changes -* Make the `lambda` form accept more than one expression as the body. This is consistent with _Scheme_ syntax, and provides a useful example for the book. To implement this: +* Make the `lambda` form accept more than one expression as the body. This is consistent with [_Scheme_ syntax](https://web.mit.edu/scheme_v9.2/doc/mit-scheme-ref/Lambda-Expressions.html), and provides a useful example for the book. To implement this: * In `Procedure.__call__`: evaluate `self.body` as a list of expressions, instead of a single expression. Return the value of the last expression. * In `evaluate()`: when processing `lambda`, unpack expression into `(_, parms, *body)`, to accept a list of expressions as the body. * Remove the `global_env` global `dict`. It is only used as a default value for the `env` parameter in `evaluate()`, but it is unsafe to use mutable data structures as parameter default values. To implement this: @@ -16,13 +16,13 @@ _Luciano Ramalho_ * Rewrite the custom test script [lispytest.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispytest.py) as [lis_test.py](https://github.com/fluentpython/example-code-2e/blob/master/02-array-seq/lispy/py3.9/lis_test.py): -a standard [pytest](https://docs.pytest.org) test suite including new test cases, preserving all Norvig's the test cases for +a standard [pytest](https://docs.pytest.org) test suite including new test cases, preserving all Norvig's test cases for [lis.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py) but removing the test cases for the features implemented only in -[lispy.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispy.py) +[lispy.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispy.py). -## Name changes +## Minor changes Cosmetic changes to make the code look more familiar to Python programmers, the audience of _Fluent Python_. diff --git a/18-with-match/lispy/py3.9/examples_test.py b/18-with-match/lispy/py3.9/examples_test.py new file mode 100644 index 0000000..603ccd3 --- /dev/null +++ b/18-with-match/lispy/py3.9/examples_test.py @@ -0,0 +1,258 @@ +""" +Doctests for `parse` +-------------------- + +# tag::PARSE[] +>>> from lis import parse +>>> parse('1.5') +1.5 +>>> parse('ni!') +'ni!' +>>> parse('(gcd 18 45)') +['gcd', 18, 45] +>>> parse(''' +... (define double +... (lambda (n) +... (* n 2))) +... ''') +['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]] + +# end::PARSE[] + +Doctest for `Environment` +------------------------- + +# tag::ENVIRONMENT[] +>>> from lis import Environment +>>> inner_env = {'a': 2} +>>> outer_env = {'a': 0, 'b': 1} +>>> env = Environment(inner_env, outer_env) +>>> env['a'] = 111 # <1> +>>> env['c'] = 222 +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1}) +>>> env.change('b', 333) # <2> +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333}) + +# end::ENVIRONMENT[] + +Doctests for `evaluate` +----------------------- + +# tag::EVAL_NUMBER[] +>>> from lis import parse, evaluate, standard_env +>>> evaluate(parse('1.5'), {}) +1.5 + +# end::EVAL_NUMBER[] + +# tag::EVAL_SYMBOL[] +>>> from lis import standard_env +>>> evaluate(parse('+'), standard_env()) + +>>> evaluate(parse('ni!'), standard_env()) +Traceback (most recent call last): + ... +KeyError: 'ni!' + +# end::EVAL_SYMBOL[] + + +# tag::EVAL_QUOTE[] +>>> evaluate(parse('(quote no-such-name)'), standard_env()) +'no-such-name' +>>> evaluate(parse('(quote (99 bottles of beer))'), standard_env()) +[99, 'bottles', 'of', 'beer'] +>>> evaluate(parse('(quote (/ 10 0))'), standard_env()) +['/', 10, 0] + +# end::EVAL_QUOTE[] + +# tag::EVAL_IF[] +>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env()) +1 +>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env()) +0 + +# end::EVAL_IF[] + + +# tag::EVAL_LAMBDA[] +>>> expr = '(lambda (a b) (* (/ a b) 100))' +>>> f = evaluate(parse(expr), standard_env()) +>>> f # doctest: +ELLIPSIS + +>>> f(15, 20) +75.0 + +# end::EVAL_LAMBDA[] + +# tag::EVAL_DEFINE[] +>>> global_env = standard_env() +>>> evaluate(parse('(define answer (* 7 6))'), global_env) +>>> global_env['answer'] +42 + +# end::EVAL_DEFINE[] + +# tag::EVAL_DEFUN[] +>>> global_env = standard_env() +>>> percent = '(define (% a b) (* (/ a b) 100))' +>>> evaluate(parse(percent), global_env) +>>> global_env['%'] # doctest: +ELLIPSIS + +>>> global_env['%'](170, 200) +85.0 + +# end::EVAL_DEFUN[] + +function call: + +# tag::EVAL_CALL[] +>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env) +42.0 + +# end::EVAL_CALL[] + +# tag::EVAL_SYNTAX_ERROR[] +>>> evaluate(parse('(lambda is not like this)'), standard_env()) +Traceback (most recent call last): + ... +SyntaxError: (lambda is not like this) + +# end::EVAL_SYNTAX_ERROR[] + +""" + +import math + +from lis import run + + +fact_src = """ +(define (! n) + (if (< n 2) + 1 + (* n (! (- n 1))) + ) +) +(! 42) +""" +def test_factorial(): + got = run(fact_src) + assert got == 1405006117752879898543142606244511569936384000000000 + assert got == math.factorial(42) + + +gcd_src = """ +(define (mod m n) + (- m (* n (quotient m n)))) +(define (gcd m n) + (if (= n 0) + m + (gcd n (mod m n)))) +(display (gcd 18 45)) +""" +def test_gcd(capsys): + run(gcd_src) + captured = capsys.readouterr() + assert captured.out == '9\n' + + +quicksort_src = """ +(define (quicksort lst) + (if (null? lst) + lst + (begin + (define pivot (car lst)) + (define rest (cdr lst)) + (append + (quicksort + (filter (lambda (x) (< x pivot)) rest)) + (list pivot) + (quicksort + (filter (lambda (x) (>= x pivot)) rest))) + ) + ) +) +(quicksort (list 2 1 6 3 4 0 8 9 7 5)) +""" +def test_quicksort(): + got = run(quicksort_src) + assert got == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + +# Example from Structure and Interpretation of Computer Programs +# https://mitpress.mit.edu/sites/default/files/sicp/full-text/sicp/book/node12.html + +newton_src = """ +(define (sqrt x) + (sqrt-iter 1.0 x)) +(define (sqrt-iter guess x) + (if (good-enough? guess x) + guess + (sqrt-iter (improve guess x) x))) +(define (good-enough? guess x) + (< (abs (- (* guess guess) x)) 0.001)) +(define (improve guess x) + (average guess (/ x guess))) +(define (average x y) + (/ (+ x y) 2)) +(sqrt 123454321) +""" +def test_newton(): + got = run(newton_src) + assert math.isclose(got, 11111) + + +closure_src = """ +(define (make-adder increment) + (lambda (x) (+ increment x)) +) +(define inc (make-adder 1)) +(inc 99) +""" +def test_closure(): + got = run(closure_src) + assert got == 100 + +closure_with_change_src = """ +(define (make-counter) + (define n 0) + (lambda () + (set! n (+ n 1)) + n) +) +(define counter (make-counter)) +(display (counter)) +(display (counter)) +(display (counter)) +""" +def test_closure_with_change(capsys): + run(closure_with_change_src) + captured = capsys.readouterr() + assert captured.out == '1\n2\n3\n' + + + +# tag::RUN_AVERAGER[] +closure_averager_src = """ +(define (make-averager) + (define count 0) + (define total 0) + (lambda (new-value) + (set! count (+ count 1)) + (set! total (+ total new-value)) + (/ total count) + ) +) +(define avg (make-averager)) +(avg 10) +(avg 11) +(avg 15) +""" +def test_closure_averager(): + got = run(closure_averager_src) + assert got == 12.0 +# end::RUN_AVERAGER[] diff --git a/18-with-match/lispy/py3.9/lis.py b/18-with-match/lispy/py3.9/lis.py new file mode 100644 index 0000000..e467211 --- /dev/null +++ b/18-with-match/lispy/py3.9/lis.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python + +################ Lispy: Scheme Interpreter in Python 3.9 + +## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html +## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021) +## by Luciano Ramalho, adding type hints and pattern matching. + + +################ Imports and Types + +import math +import operator as op +from collections import ChainMap +from itertools import chain +from typing import Any, Union, NoReturn + +Symbol = str +Atom = Union[float, int, Symbol] +Expression = Union[Atom, list] + + +################ Parsing: parse, tokenize, and read_from_tokens + +def parse(program: str) -> Expression: + "Read a Scheme expression from a string." + return read_from_tokens(tokenize(program)) + +def tokenize(s: str) -> list[str]: + "Convert a string into a list of tokens." + return s.replace('(', ' ( ').replace(')', ' ) ').split() + +def read_from_tokens(tokens: list[str]) -> Expression: + "Read an expression from a sequence of tokens." + if len(tokens) == 0: + raise SyntaxError('unexpected EOF while reading') + token = tokens.pop(0) + if '(' == token: + exp = [] + while tokens[0] != ')': + exp.append(read_from_tokens(tokens)) + tokens.pop(0) # discard ')' + return exp + elif ')' == token: + raise SyntaxError('unexpected )') + else: + return parse_atom(token) + +def parse_atom(token: str) -> Atom: + "Numbers become numbers; every other token is a symbol." + try: + return int(token) + except ValueError: + try: + return float(token) + except ValueError: + return Symbol(token) + + +################ Global Environment + +class Environment(ChainMap[Symbol, Any]): + "A ChainMap that allows changing an item in-place." + + def change(self, key: Symbol, value: Any) -> None: + "Find where key is defined and change the value there." + for map in self.maps: + if key in map: + map[key] = value # type: ignore[index] + return + raise KeyError(key) + + +def standard_env() -> Environment: + "An environment with some Scheme standard procedures." + env = Environment() + env.update(vars(math)) # sin, cos, sqrt, pi, ... + env.update({ + '+': op.add, + '-': op.sub, + '*': op.mul, + '/': op.truediv, + 'quotient': op.floordiv, + '>': op.gt, + '<': op.lt, + '>=': op.ge, + '<=': op.le, + '=': op.eq, + 'abs': abs, + 'append': lambda *args: list(chain(*args)), + 'apply': lambda proc, args: proc(*args), + 'begin': lambda *x: x[-1], + 'car': lambda x: x[0], + 'cdr': lambda x: x[1:], + 'cons': lambda x, y: [x] + y, + 'display': lambda x: print(lispstr(x)), + 'eq?': op.is_, + 'equal?': op.eq, + 'filter': lambda *args: list(filter(*args)), + 'length': len, + 'list': lambda *x: list(x), + 'list?': lambda x: isinstance(x, list), + 'map': lambda *args: list(map(*args)), + 'max': max, + 'min': min, + 'not': op.not_, + 'null?': lambda x: x == [], + 'number?': lambda x: isinstance(x, (int, float)), + 'procedure?': callable, + 'round': round, + 'symbol?': lambda x: isinstance(x, Symbol), + }) + return env + + +################ Interaction: A REPL + +def repl(prompt: str = 'lis.py> ') -> NoReturn: + "A prompt-read-eval-print loop." + global_env = Environment({}, standard_env()) + while True: + ast = parse(input(prompt)) + val = evaluate(ast, global_env) + if val is not None: + print(lispstr(val)) + +def lispstr(exp: object) -> str: + "Convert a Python object back into a Lisp-readable string." + if isinstance(exp, list): + return '(' + ' '.join(map(lispstr, exp)) + ')' + else: + return str(exp) + + +################ Evaluator + +def evaluate(exp: Expression, env: Environment) -> Any: + "Evaluate an expression in an environment." + if isinstance(exp, Symbol): # variable reference + return env[exp] + elif not isinstance(exp, list): # constant literal + return exp + elif exp[0] == 'quote': # (quote exp) + (_, x) = exp + return x + elif exp[0] == 'if': # (if test conseq alt) + (_, test, consequence, alternative) = exp + if evaluate(test, env): + return evaluate(consequence, env) + else: + return evaluate(alternative, env) + elif exp[0] == 'lambda': # (lambda (parm…) body…) + (_, parms, *body) = exp + if not isinstance(parms, list): + raise SyntaxError(lispstr(exp)) + return Procedure(parms, body, env) + elif exp[0] == 'define': + (_, name_exp, *rest) = exp + if isinstance(name_exp, Symbol): # (define name exp) + value_exp = rest[0] + env[name_exp] = evaluate(value_exp, env) + else: # (define (name parm…) body…) + name, *parms = name_exp + env[name] = Procedure(parms, rest, env) + elif exp[0] == 'set!': + (_, var, value_exp) = exp + env.change(var, evaluate(value_exp, env)) + else: # (proc arg…) + (func_exp, *args) = exp + proc = evaluate(func_exp, env) + args = [evaluate(arg, env) for arg in args] + return proc(*args) + + +class Procedure: + "A user-defined Scheme procedure." + + def __init__( + self, parms: list[Symbol], body: list[Expression], env: Environment + ): + self.parms = parms + self.body = body + self.env = env + + def __call__(self, *args: Expression) -> Any: + local_env = dict(zip(self.parms, args)) + env = Environment(local_env, self.env) + for exp in self.body: + result = evaluate(exp, env) + return result + + +################ command-line interface + +def run(source: str) -> Any: + global_env = Environment({}, standard_env()) + tokens = tokenize(source) + while tokens: + exp = read_from_tokens(tokens) + result = evaluate(exp, global_env) + return result + +def main(args: list[str]) -> None: + if len(args) == 1: + with open(args[0]) as fp: + run(fp.read()) + else: + repl() + +if __name__ == '__main__': + import sys + main(sys.argv[1:]) diff --git a/02-array-seq/lispy/py3.9-no-hints/lis_test.py b/18-with-match/lispy/py3.9/lis_test.py similarity index 76% rename from 02-array-seq/lispy/py3.9-no-hints/lis_test.py rename to 18-with-match/lispy/py3.9/lis_test.py index f5f4f8e..03dc15c 100644 --- a/02-array-seq/lispy/py3.9-no-hints/lis_test.py +++ b/18-with-match/lispy/py3.9/lis_test.py @@ -1,6 +1,8 @@ +from typing import Optional + from pytest import mark, fixture -from lis import parse, evaluate, standard_env +from lis import parse, evaluate, Expression, Environment, standard_env ############################################################# tests for parse @@ -12,7 +14,7 @@ ('99 100', 99), # parse stops at the first complete expression ('(a)(b)', ['a']), ]) -def test_parse(source: str, expected): +def test_parse(source: str, expected: Expression) -> None: got = parse(source) assert got == expected @@ -62,61 +64,61 @@ def test_parse(source: str, expected): ("((repeat riff-shuffle) (list 1 2 3 4 5 6 7 8))", [1, 3, 5, 7, 2, 4, 6, 8]), ("(riff-shuffle (riff-shuffle (riff-shuffle (list 1 2 3 4 5 6 7 8))))", [1,2,3,4,5,6,7,8]), ]) -def test_evaluate(source, expected): +def test_evaluate(source: str, expected: Optional[Expression]) -> None: got = evaluate(parse(source), global_env_for_first_test) assert got == expected @fixture -def std_env(): +def std_env() -> Environment: return standard_env() -# tests for each of the cases in evaluate +# tests for cases in evaluate -def test_evaluate_variable(): - env = dict(x=10) +def test_evaluate_variable() -> None: + env = Environment({'x': 10}) source = 'x' expected = 10 got = evaluate(parse(source), env) assert got == expected -def test_evaluate_literal(std_env): +def test_evaluate_literal(std_env: Environment) -> None: source = '3.3' expected = 3.3 got = evaluate(parse(source), std_env) assert got == expected -def test_evaluate_quote(std_env): +def test_evaluate_quote(std_env: Environment) -> None: source = '(quote (1.1 is not 1))' expected = [1.1, 'is', 'not', 1] got = evaluate(parse(source), std_env) assert got == expected -def test_evaluate_if_true(std_env) -> None: +def test_evaluate_if_true(std_env: Environment) -> None: source = '(if 1 10 no-such-thing)' expected = 10 got = evaluate(parse(source), std_env) assert got == expected -def test_evaluate_if_false(std_env) -> None: +def test_evaluate_if_false(std_env: Environment) -> None: source = '(if 0 no-such-thing 20)' expected = 20 got = evaluate(parse(source), std_env) assert got == expected -def test_define(std_env) -> None: +def test_define(std_env: Environment) -> None: source = '(define answer (* 6 7))' got = evaluate(parse(source), std_env) assert got is None assert std_env['answer'] == 42 -def test_lambda(std_env) -> None: +def test_lambda(std_env: Environment) -> None: source = '(lambda (a b) (if (>= a b) a b))' func = evaluate(parse(source), std_env) assert func.parms == ['a', 'b'] @@ -126,7 +128,7 @@ def test_lambda(std_env) -> None: assert func(3, 2) == 3 -def test_begin(std_env) -> None: +def test_begin(std_env: Environment) -> None: source = """ (begin (define x (* 2 3)) @@ -137,25 +139,25 @@ def test_begin(std_env) -> None: assert got == 42 -def test_invocation_builtin_car(std_env) -> None: +def test_invocation_builtin_car(std_env: Environment) -> None: source = '(car (quote (11 22 33)))' got = evaluate(parse(source), std_env) assert got == 11 -def test_invocation_builtin_append(std_env) -> None: +def test_invocation_builtin_append(std_env: Environment) -> None: source = '(append (quote (a b)) (quote (c d)))' got = evaluate(parse(source), std_env) assert got == ['a', 'b', 'c', 'd'] -def test_invocation_builtin_map(std_env) -> None: +def test_invocation_builtin_map(std_env: Environment) -> None: source = '(map (lambda (x) (* x 2)) (quote (1 2 3))))' got = evaluate(parse(source), std_env) assert got == [2, 4, 6] -def test_invocation_user_procedure(std_env): +def test_invocation_user_procedure(std_env: Environment) -> None: source = """ (begin (define max (lambda (a b) (if (>= a b) a b))) @@ -164,3 +166,15 @@ def test_invocation_user_procedure(std_env): """ got = evaluate(parse(source), std_env) assert got == 22 + + +def test_define_function(std_env: Environment) -> None: + source = '(define (max a b) (if (>= a b) a b))' + got = evaluate(parse(source), std_env) + assert got is None + max_fn = std_env['max'] + assert max_fn.parms == ['a', 'b'] + assert max_fn.body == [['if', ['>=', 'a', 'b'], 'a', 'b']] + assert max_fn.env is std_env + assert max_fn(1, 2) == 2 + assert max_fn(3, 2) == 3 diff --git a/18-with-match/mirror.py b/18-with-match/mirror.py index ba31944..841c6ab 100644 --- a/18-with-match/mirror.py +++ b/18-with-match/mirror.py @@ -11,11 +11,11 @@ ... print('Alice, Kitty and Snowdrop') # <2> ... print(what) ... - pordwonS dna yttiK ,ecilA # <3> + pordwonS dna yttiK ,ecilA YKCOWREBBAJ - >>> what # <4> + >>> what # <3> 'JABBERWOCKY' - >>> print('Back to normal.') # <5> + >>> print('Back to normal.') # <4> Back to normal. # end::MIRROR_DEMO_1[] @@ -27,15 +27,15 @@ >>> from mirror import LookingGlass >>> manager = LookingGlass() # <1> - >>> manager - + >>> manager # doctest: +ELLIPSIS + >>> monster = manager.__enter__() # <2> >>> monster == 'JABBERWOCKY' # <3> eurT >>> monster 'YKCOWREBBAJ' - >>> manager - >ca875a2x0 ta tcejbo ssalGgnikooL.rorrim< + >>> manager # doctest: +ELLIPSIS + >... ta tcejbo ssalGgnikooL.rorrim< >>> manager.__exit__(None, None, None) # <4> >>> monster 'JABBERWOCKY' @@ -69,10 +69,11 @@ # tag::MIRROR_EX[] +import sys + class LookingGlass: def __enter__(self): # <1> - import sys self.original_write = sys.stdout.write # <2> sys.stdout.write = self.reverse_write # <3> return 'JABBERWOCKY' # <4> @@ -81,12 +82,9 @@ def reverse_write(self, text): # <5> self.original_write(text[::-1]) def __exit__(self, exc_type, exc_value, traceback): # <6> - import sys # <7> - sys.stdout.write = self.original_write # <8> - if exc_type is ZeroDivisionError: # <9> + sys.stdout.write = self.original_write # <7> + if exc_type is ZeroDivisionError: # <8> print('Please DO NOT divide by zero!') - return True # <10> - # <11> - - + return True # <9> + # <10> # end::MIRROR_EX[] diff --git a/18-with-match/mirror_gen.py b/18-with-match/mirror_gen.py index 457955a..2758045 100644 --- a/18-with-match/mirror_gen.py +++ b/18-with-match/mirror_gen.py @@ -15,6 +15,9 @@ YKCOWREBBAJ >>> what 'JABBERWOCKY' + >>> print('back to normal') + back to normal + # end::MIRROR_GEN_DEMO_1[] @@ -35,22 +38,36 @@ >>> manager # doctest: +ELLIPSIS >...x0 ta tcejbo reganaMtxetnoCrotareneG_.biltxetnoc< >>> manager.__exit__(None, None, None) # <4> + False >>> monster 'JABBERWOCKY' # end::MIRROR_GEN_DEMO_2[] +The decorated generator also works as a decorator: + + +# tag::MIRROR_GEN_DECO[] + >>> @looking_glass() + ... def verse(): + ... print('The time has come') + ... + >>> verse() # <1> + emoc sah emit ehT + >>> print('back to normal') # <2> + back to normal + +# end::MIRROR_GEN_DECO[] + """ # tag::MIRROR_GEN_EX[] - import contextlib - +import sys @contextlib.contextmanager # <1> def looking_glass(): - import sys original_write = sys.stdout.write # <2> def reverse_write(text): # <3> @@ -59,6 +76,4 @@ def reverse_write(text): # <3> sys.stdout.write = reverse_write # <4> yield 'JABBERWOCKY' # <5> sys.stdout.write = original_write # <6> - - # end::MIRROR_GEN_EX[] diff --git a/18-with-match/mirror_gen_exc.py b/18-with-match/mirror_gen_exc.py index c446918..0c20a3b 100644 --- a/18-with-match/mirror_gen_exc.py +++ b/18-with-match/mirror_gen_exc.py @@ -35,6 +35,7 @@ >>> manager # doctest: +ELLIPSIS >...x0 ta tcejbo reganaMtxetnoCrotareneG_.biltxetnoc< >>> manager.__exit__(None, None, None) # <4> + False >>> monster 'JABBERWOCKY' @@ -48,7 +49,7 @@ # tag::MIRROR_GEN_DEMO_3[] - >>> from mirror_gen import looking_glass + >>> from mirror_gen_exc import looking_glass >>> with looking_glass(): ... print('Humpty Dumpty') ... x = 1/0 # <1> @@ -74,13 +75,11 @@ # tag::MIRROR_GEN_EXC[] - import contextlib - +import sys @contextlib.contextmanager def looking_glass(): - import sys original_write = sys.stdout.write def reverse_write(text): @@ -96,6 +95,4 @@ def reverse_write(text): sys.stdout.write = original_write # <3> if msg: print(msg) # <4> - - # end::MIRROR_GEN_EXC[] diff --git a/19-concurrency/primes/README.md b/19-concurrency/primes/README.md new file mode 100644 index 0000000..772592a --- /dev/null +++ b/19-concurrency/primes/README.md @@ -0,0 +1,16 @@ +# Race condition in orignal procs.py + +Thanks to reader Michael Albert who noticed the code I published during the Early Release had a race condition in `proc.py`. + +If you are curious, +[this diff](https://github.com/fluentpython/example-code-2e/commit/2c1230579db99738a5e5e6802063bda585f6476d) +shows the bug and how I fixed it—but note that I later refactored +the example to delegate parts of `main` to the `start_jobs` and `report` functions. + +The problem was that I ended the `while` loop that retrieved the results when the `jobs` queue was empty. +However, it was possible that the queue was empty but there were still processes working. +If that happened, one or more results would not be reported. +I did not notice the problem when I tested my original code, +but Albert showed that adding a `sleep(1)` call before the `if jobs.empty()` line made the bug occur frequently. +I adopted one of his solutions: have the `worker` function send back a `PrimeResult` with `n = 0` as a sentinel, +to let the main loop know that the process had completed, ending the loop when all processes were done. \ No newline at end of file diff --git a/20-concurrency/primes/log-procs.txt b/19-concurrency/primes/log-procs.txt similarity index 100% rename from 20-concurrency/primes/log-procs.txt rename to 19-concurrency/primes/log-procs.txt diff --git a/20-concurrency/primes/primes.py b/19-concurrency/primes/primes.py similarity index 100% rename from 20-concurrency/primes/primes.py rename to 19-concurrency/primes/primes.py diff --git a/19-concurrency/primes/procs.py b/19-concurrency/primes/procs.py new file mode 100644 index 0000000..50e3d9f --- /dev/null +++ b/19-concurrency/primes/procs.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +""" +procs.py: shows that multiprocessing on a multicore machine +can be faster than sequential code for CPU-intensive work. +""" + +# tag::PRIMES_PROC_TOP[] +import sys +from time import perf_counter +from typing import NamedTuple +from multiprocessing import Process, SimpleQueue, cpu_count # <1> +from multiprocessing import queues # <2> + +from primes import is_prime, NUMBERS + +class PrimeResult(NamedTuple): # <3> + n: int + prime: bool + elapsed: float + +JobQueue = queues.SimpleQueue[int] # <4> +ResultQueue = queues.SimpleQueue[PrimeResult] # <5> + +def check(n: int) -> PrimeResult: # <6> + t0 = perf_counter() + res = is_prime(n) + return PrimeResult(n, res, perf_counter() - t0) + +def worker(jobs: JobQueue, results: ResultQueue) -> None: # <7> + while n := jobs.get(): # <8> + results.put(check(n)) # <9> + results.put(PrimeResult(0, False, 0.0)) # <10> + +def start_jobs( + procs: int, jobs: JobQueue, results: ResultQueue # <11> +) -> None: + for n in NUMBERS: + jobs.put(n) # <12> + for _ in range(procs): + proc = Process(target=worker, args=(jobs, results)) # <13> + proc.start() # <14> + jobs.put(0) # <15> +# end::PRIMES_PROC_TOP[] + +# tag::PRIMES_PROC_MAIN[] +def main() -> None: + if len(sys.argv) < 2: # <1> + procs = cpu_count() + else: + procs = int(sys.argv[1]) + + print(f'Checking {len(NUMBERS)} numbers with {procs} processes:') + t0 = perf_counter() + jobs: JobQueue = SimpleQueue() # <2> + results: ResultQueue = SimpleQueue() + start_jobs(procs, jobs, results) # <3> + checked = report(procs, results) # <4> + elapsed = perf_counter() - t0 + print(f'{checked} checks in {elapsed:.2f}s') # <5> + +def report(procs: int, results: ResultQueue) -> int: # <6> + checked = 0 + procs_done = 0 + while procs_done < procs: # <7> + n, prime, elapsed = results.get() # <8> + if n == 0: # <9> + procs_done += 1 + else: + checked += 1 # <10> + label = 'P' if prime else ' ' + print(f'{n:16} {label} {elapsed:9.6f}s') + return checked + +if __name__ == '__main__': + main() +# end::PRIMES_PROC_MAIN[] diff --git a/20-concurrency/primes/procs.py b/19-concurrency/primes/procs_race_condition.py old mode 100644 new mode 100755 similarity index 73% rename from 20-concurrency/primes/procs.py rename to 19-concurrency/primes/procs_race_condition.py index fc84049..aa5e172 --- a/20-concurrency/primes/procs.py +++ b/19-concurrency/primes/procs_race_condition.py @@ -30,20 +30,12 @@ def check(n: int) -> PrimeResult: # <6> def worker(jobs: JobQueue, results: ResultQueue) -> None: # <7> while n := jobs.get(): # <8> results.put(check(n)) # <9> + results.put(PrimeResult(0, False, 0.0)) # end::PRIMES_PROC_TOP[] -# tag::PRIMES_PROC_MAIN[] -def main() -> None: - if len(sys.argv) < 2: # <1> - workers = cpu_count() - else: - workers = int(sys.argv[1]) - - print(f'Checking {len(NUMBERS)} numbers with {workers} processes:') - +def start_jobs(workers: int) -> ResultQueue: jobs: JobQueue = SimpleQueue() # <2> results: ResultQueue = SimpleQueue() - t0 = perf_counter() for n in NUMBERS: # <3> jobs.put(n) @@ -53,15 +45,35 @@ def main() -> None: proc.start() # <5> jobs.put(0) # <6> - while True: + return results + +def report(workers: int, results: ResultQueue) -> int: + workers_done = 0 + checked = 0 + while workers_done < workers: n, prime, elapsed = results.get() # <7> - label = 'P' if prime else ' ' - print(f'{n:16} {label} {elapsed:9.6f}s') # <8> - if jobs.empty(): # <9> - break + if n == 0: + workers_done += 1 + else: + checked += 1 + label = 'P' if prime else ' ' + print(f'{n:16} {label} {elapsed:9.6f}s') # <8> + return checked + +# tag::PRIMES_PROC_MAIN[] +def main() -> None: + if len(sys.argv) < 2: # <1> + workers = cpu_count() + else: + workers = int(sys.argv[1]) + + print(f'Checking {len(NUMBERS)} numbers with {workers} processes:') + t0 = perf_counter() + results = start_jobs(workers) + checked = report(workers, results) elapsed = perf_counter() - t0 - print(f'Total time: {elapsed:.2f}s') + print(f'{checked} checks in {elapsed:.2f}s') if __name__ == '__main__': main() diff --git a/20-concurrency/primes/py36/primes.py b/19-concurrency/primes/py36/primes.py similarity index 100% rename from 20-concurrency/primes/py36/primes.py rename to 19-concurrency/primes/py36/primes.py diff --git a/20-concurrency/primes/py36/procs.py b/19-concurrency/primes/py36/procs.py similarity index 100% rename from 20-concurrency/primes/py36/procs.py rename to 19-concurrency/primes/py36/procs.py diff --git a/20-concurrency/primes/run_procs.sh b/19-concurrency/primes/run_procs.sh similarity index 100% rename from 20-concurrency/primes/run_procs.sh rename to 19-concurrency/primes/run_procs.sh diff --git a/20-concurrency/primes/sequential.py b/19-concurrency/primes/sequential.py similarity index 100% rename from 20-concurrency/primes/sequential.py rename to 19-concurrency/primes/sequential.py diff --git a/20-concurrency/primes/spinner_prime_async_broken.py b/19-concurrency/primes/spinner_prime_async_broken.py similarity index 100% rename from 20-concurrency/primes/spinner_prime_async_broken.py rename to 19-concurrency/primes/spinner_prime_async_broken.py diff --git a/20-concurrency/primes/spinner_prime_async_nap.py b/19-concurrency/primes/spinner_prime_async_nap.py similarity index 81% rename from 20-concurrency/primes/spinner_prime_async_nap.py rename to 19-concurrency/primes/spinner_prime_async_nap.py index 4d6fa47..b6e6518 100644 --- a/20-concurrency/primes/spinner_prime_async_nap.py +++ b/19-concurrency/primes/spinner_prime_async_nap.py @@ -7,6 +7,7 @@ import asyncio import itertools import math +import functools # tag::PRIME_NAP[] async def is_prime(n): @@ -21,8 +22,8 @@ async def is_prime(n): for i in range(3, root + 1, 2): if n % i == 0: return False - if i % 100_000 == 1: # <2> - await asyncio.sleep(0) + if i % 100_000 == 1: + await asyncio.sleep(0) # <1> return True # end::PRIME_NAP[] @@ -39,13 +40,13 @@ async def spin(msg: str) -> None: print(f'\r{blanks}\r', end='') async def check(n: int) -> int: - return await is_prime(n) # <4> + return await is_prime(n) async def supervisor(n: int) -> int: - spinner = asyncio.create_task(spin('thinking!')) # <1> - print('spinner object:', spinner) # <2> - result = await check(n) # <3> - spinner.cancel() # <5> + spinner = asyncio.create_task(spin('thinking!')) + print('spinner object:', spinner) + result = await check(n) + spinner.cancel() return result def main() -> None: diff --git a/20-concurrency/primes/spinner_prime_proc.py b/19-concurrency/primes/spinner_prime_proc.py similarity index 100% rename from 20-concurrency/primes/spinner_prime_proc.py rename to 19-concurrency/primes/spinner_prime_proc.py diff --git a/20-concurrency/primes/spinner_prime_thread.py b/19-concurrency/primes/spinner_prime_thread.py similarity index 100% rename from 20-concurrency/primes/spinner_prime_thread.py rename to 19-concurrency/primes/spinner_prime_thread.py diff --git a/20-concurrency/primes/stats-procs.ipynb b/19-concurrency/primes/stats-procs.ipynb similarity index 100% rename from 20-concurrency/primes/stats-procs.ipynb rename to 19-concurrency/primes/stats-procs.ipynb diff --git a/19-concurrency/primes/threads.py b/19-concurrency/primes/threads.py new file mode 100644 index 0000000..ea00bbe --- /dev/null +++ b/19-concurrency/primes/threads.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +""" +threads.py: shows that Python threads are slower +than sequential code for CPU-intensive work. +""" + +import os +import sys +from queue import SimpleQueue +from time import perf_counter +from typing import NamedTuple +from threading import Thread + +from primes import is_prime, NUMBERS + +class PrimeResult(NamedTuple): + n: int + prime: bool + elapsed: float + +JobQueue = SimpleQueue[int] # <4> +ResultQueue = SimpleQueue[PrimeResult] # <5> + +def check(n: int) -> PrimeResult: # <6> + t0 = perf_counter() + res = is_prime(n) + return PrimeResult(n, res, perf_counter() - t0) + +def worker(jobs: JobQueue, results: ResultQueue) -> None: # <7> + while n := jobs.get(): # <8> + results.put(check(n)) # <9> + results.put(PrimeResult(0, False, 0.0)) + +def start_jobs(workers: int, jobs: JobQueue, results: ResultQueue) -> None: + for n in NUMBERS: # <3> + jobs.put(n) + for _ in range(workers): + proc = Thread(target=worker, args=(jobs, results)) # <4> + proc.start() # <5> + jobs.put(0) # <6> + +def report(workers: int, results: ResultQueue) -> int: + checked = 0 + workers_done = 0 + while workers_done < workers: + n, prime, elapsed = results.get() + if n == 0: + workers_done += 1 + else: + checked += 1 + label = 'P' if prime else ' ' + print(f'{n:16} {label} {elapsed:9.6f}s') + return checked + +def main() -> None: + if len(sys.argv) < 2: + workers = os.cpu_count() + else: + workers = int(sys.argv[1]) + + print(f'Checking {len(NUMBERS)} numbers with {workers} threads:') + t0 = perf_counter() + jobs: JobQueue = SimpleQueue() + results: ResultQueue = SimpleQueue() + start_jobs(workers, jobs, results) + checked = report(workers, results) + elapsed = perf_counter() - t0 + print(f'{checked} checks in {elapsed:.2f}s') + +if __name__ == '__main__': + main() + diff --git a/20-concurrency/spinner_async.py b/19-concurrency/spinner_async.py similarity index 100% rename from 20-concurrency/spinner_async.py rename to 19-concurrency/spinner_async.py diff --git a/20-concurrency/spinner_async_experiment.py b/19-concurrency/spinner_async_experiment.py similarity index 100% rename from 20-concurrency/spinner_async_experiment.py rename to 19-concurrency/spinner_async_experiment.py diff --git a/20-concurrency/spinner_proc.py b/19-concurrency/spinner_proc.py similarity index 100% rename from 20-concurrency/spinner_proc.py rename to 19-concurrency/spinner_proc.py diff --git a/20-concurrency/spinner_thread.py b/19-concurrency/spinner_thread.py similarity index 100% rename from 20-concurrency/spinner_thread.py rename to 19-concurrency/spinner_thread.py diff --git a/19-coroutine/README.rst b/19-coroutine/README.rst deleted file mode 100644 index 2771530..0000000 --- a/19-coroutine/README.rst +++ /dev/null @@ -1,4 +0,0 @@ -Sample code for Chapter 16 - "Coroutines" - -From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015) -http://shop.oreilly.com/product/0636920032519.do diff --git a/19-coroutine/coro_exc_demo.py b/19-coroutine/coro_exc_demo.py deleted file mode 100644 index a68684f..0000000 --- a/19-coroutine/coro_exc_demo.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Coroutine closing demonstration:: - -# tag::DEMO_CORO_EXC_1[] - >>> exc_coro = demo_exc_handling() - >>> next(exc_coro) - -> coroutine started - >>> exc_coro.send(11) - -> coroutine received: 11 - >>> exc_coro.send(22) - -> coroutine received: 22 - >>> exc_coro.close() - >>> from inspect import getgeneratorstate - >>> getgeneratorstate(exc_coro) - 'GEN_CLOSED' - -# end::DEMO_CORO_EXC_1[] - -Coroutine handling exception:: - -# tag::DEMO_CORO_EXC_2[] - >>> exc_coro = demo_exc_handling() - >>> next(exc_coro) - -> coroutine started - >>> exc_coro.send(11) - -> coroutine received: 11 - >>> exc_coro.throw(DemoException) - *** DemoException handled. Continuing... - >>> getgeneratorstate(exc_coro) - 'GEN_SUSPENDED' - -# end::DEMO_CORO_EXC_2[] - -Coroutine not handling exception:: - -# tag::DEMO_CORO_EXC_3[] - >>> exc_coro = demo_exc_handling() - >>> next(exc_coro) - -> coroutine started - >>> exc_coro.send(11) - -> coroutine received: 11 - >>> exc_coro.throw(ZeroDivisionError) - Traceback (most recent call last): - ... - ZeroDivisionError - >>> getgeneratorstate(exc_coro) - 'GEN_CLOSED' - -# end::DEMO_CORO_EXC_3[] -""" - -# tag::EX_CORO_EXC[] -class DemoException(Exception): - """An exception type for the demonstration.""" - -def demo_exc_handling(): - print('-> coroutine started') - while True: - try: - x = yield - except DemoException: # <1> - print('*** DemoException handled. Continuing...') - else: # <2> - print(f'-> coroutine received: {x!r}') - raise RuntimeError('This line should never run.') # <3> -# end::EX_CORO_EXC[] diff --git a/19-coroutine/coro_finally_demo.py b/19-coroutine/coro_finally_demo.py deleted file mode 100644 index fd4545d..0000000 --- a/19-coroutine/coro_finally_demo.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Second coroutine closing demonstration:: - - >>> fin_coro = demo_finally() - >>> next(fin_coro) - -> coroutine started - >>> fin_coro.send(11) - -> coroutine received: 11 - >>> fin_coro.send(22) - -> coroutine received: 22 - >>> fin_coro.close() - -> coroutine ending - - -Second coroutine not handling exception:: - - >>> fin_coro = demo_finally() - >>> next(fin_coro) - -> coroutine started - >>> fin_coro.send(11) - -> coroutine received: 11 - >>> fin_coro.throw(ZeroDivisionError) # doctest: +SKIP - -> coroutine ending - Traceback (most recent call last): - File "", line 1, in - File "coro_exception_demos.py", line 109, in demo_finally - print(f'-> coroutine received: {x!r}') - ZeroDivisionError - - -The last test above must be skipped because the output '-> coroutine ending' -is not detected by doctest, which raises a false error. However, if you -run this file as shown below, you'll see that output "leak" into standard -output:: - - - $ python3 -m doctest coro_exception_demo.py - -> coroutine ending - -""" - - -# tag::EX_CORO_FINALLY[] -class DemoException(Exception): - """An exception type for the demonstration.""" - - -def demo_finally(): - print('-> coroutine started') - try: - while True: - try: - x = yield - except DemoException: - print('*** DemoException handled. Continuing...') - else: - print(f'-> coroutine received: {x!r}') - finally: - print('-> coroutine ending') - -# end::EX_CORO_FINALLY[] diff --git a/19-coroutine/coroaverager0.py b/19-coroutine/coroaverager0.py deleted file mode 100644 index 10ccfdc..0000000 --- a/19-coroutine/coroaverager0.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -A coroutine to compute a running average - -# tag::CORO_AVERAGER_TEST[] - >>> coro_avg = averager() # <1> - >>> next(coro_avg) # <2> - >>> coro_avg.send(10) # <3> - 10.0 - >>> coro_avg.send(30) - 20.0 - >>> coro_avg.send(5) - 15.0 - -# end::CORO_AVERAGER_TEST[] - -""" - -# tag::CORO_AVERAGER[] -def averager(): - total = 0.0 - count = 0 - average = None - while True: # <1> - term = yield average # <2> - total += term - count += 1 - average = total/count -# end::CORO_AVERAGER[] diff --git a/19-coroutine/coroaverager1.py b/19-coroutine/coroaverager1.py deleted file mode 100644 index 3b3c5e9..0000000 --- a/19-coroutine/coroaverager1.py +++ /dev/null @@ -1,30 +0,0 @@ -# tag::DECORATED_AVERAGER[] -""" -A coroutine to compute a running average - - >>> coro_avg = averager() # <1> - >>> from inspect import getgeneratorstate - >>> getgeneratorstate(coro_avg) # <2> - 'GEN_SUSPENDED' - >>> coro_avg.send(10) # <3> - 10.0 - >>> coro_avg.send(30) - 20.0 - >>> coro_avg.send(5) - 15.0 - -""" - -from coroutil import coroutine # <4> - -@coroutine # <5> -def averager(): # <6> - total = 0.0 - count = 0 - average = None - while True: - term = yield average - total += term - count += 1 - average = total/count -# end::DECORATED_AVERAGER[] diff --git a/19-coroutine/coroaverager2.py b/19-coroutine/coroaverager2.py deleted file mode 100644 index 9d724c0..0000000 --- a/19-coroutine/coroaverager2.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -A coroutine to compute a running average. - -Testing ``averager`` by itself:: - -# tag::RETURNING_AVERAGER_DEMO1[] - - >>> coro_avg = averager() - >>> next(coro_avg) - >>> coro_avg.send(10) # <1> - >>> coro_avg.send(30) - >>> coro_avg.send(6.5) - >>> coro_avg.send(None) # <2> - Traceback (most recent call last): - ... - StopIteration: Result(count=3, average=15.5) - -# end::RETURNING_AVERAGER_DEMO1[] - -Catching `StopIteration` to extract the value returned by -the coroutine:: - -# tag::RETURNING_AVERAGER_DEMO2[] - - >>> coro_avg = averager() - >>> next(coro_avg) - >>> coro_avg.send(10) - >>> coro_avg.send(30) - >>> coro_avg.send(6.5) - >>> try: - ... coro_avg.send(None) - ... except StopIteration as exc: - ... result = exc.value - ... - >>> result - Result(count=3, average=15.5) - -# end::RETURNING_AVERAGER_DEMO2[] - - -""" - -# tag::RETURNING_AVERAGER[] -from collections import namedtuple - -Result = namedtuple('Result', 'count average') - - -def averager(): - total = 0.0 - count = 0 - average = None - while True: - term = yield - if term is None: - break # <1> - total += term - count += 1 - average = total/count - return Result(count, average) # <2> -# end::RETURNING_AVERAGER[] diff --git a/19-coroutine/coroaverager3.py b/19-coroutine/coroaverager3.py deleted file mode 100644 index 2b24df9..0000000 --- a/19-coroutine/coroaverager3.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -A coroutine to compute a running average. - -Testing ``averager`` by itself:: - - >>> coro_avg = averager() - >>> next(coro_avg) - >>> coro_avg.send(10) - >>> coro_avg.send(30) - >>> coro_avg.send(6.5) - >>> coro_avg.send(None) - Traceback (most recent call last): - ... - StopIteration: Result(count=3, average=15.5) - - -Driving it with ``yield from``:: - - >>> def summarize(results): - ... while True: - ... result = yield from averager() - ... results.append(result) - ... - >>> results = [] - >>> summary = summarize(results) - >>> next(summary) - >>> for height in data['girls;m']: - ... summary.send(height) - ... - >>> summary.send(None) - >>> for height in data['boys;m']: - ... summary.send(height) - ... - >>> summary.send(None) - >>> results == [ - ... Result(count=10, average=1.4279999999999997), - ... Result(count=9, average=1.3888888888888888) - ... ] - True - -""" - -# tag::YIELD_FROM_AVERAGER[] -from collections import namedtuple - -Result = namedtuple('Result', 'count average') - - -# the subgenerator -def averager(): # <1> - total = 0.0 - count = 0 - average = None - while True: - term = yield # <2> - if term is None: # <3> - break - total += term - count += 1 - average = total/count - return Result(count, average) # <4> - - -# the delegating generator -def grouper(results, key): # <5> - while True: # <6> - results[key] = yield from averager() # <7> - - -# the client code, a.k.a. the caller -def main(data): # <8> - results = {} - for key, values in data.items(): - group = grouper(results, key) # <9> - next(group) # <10> - for value in values: - group.send(value) # <11> - group.send(None) # important! <12> - - # print(results) # uncomment to debug - report(results) - - -# output report -def report(results): - for key, result in sorted(results.items()): - group, unit = key.split(';') - print(f'{result.count:2} {group:5}', - f'averaging {result.average:.2f}{unit}') - - -data = { - 'girls;kg': - [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5], - 'girls;m': - [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43], - 'boys;kg': - [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3], - 'boys;m': - [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46], -} - - -if __name__ == '__main__': - main(data) - -# end::YIELD_FROM_AVERAGER[] diff --git a/19-coroutine/coroutil.py b/19-coroutine/coroutil.py deleted file mode 100644 index 4420db8..0000000 --- a/19-coroutine/coroutil.py +++ /dev/null @@ -1,12 +0,0 @@ -# tag::CORO_DECO[] -from functools import wraps - -def coroutine(func): - """Decorator: primes `func` by advancing to first `yield`""" - @wraps(func) - def primer(*args, **kwargs): # <1> - gen = func(*args, **kwargs) # <2> - next(gen) # <3> - return gen # <4> - return primer -# end::CORO_DECO[] diff --git a/19-coroutine/taxi_sim.py b/19-coroutine/taxi_sim.py deleted file mode 100644 index 6bf0ce9..0000000 --- a/19-coroutine/taxi_sim.py +++ /dev/null @@ -1,203 +0,0 @@ - -""" -Taxi simulator -============== - -Driving a taxi from the console:: - - >>> from taxi_sim import taxi_process - >>> taxi = taxi_process(ident=13, trips=2, start_time=0) - >>> next(taxi) - Event(time=0, proc=13, action='leave garage') - >>> taxi.send(_.time + 7) - Event(time=7, proc=13, action='pick up passenger') - >>> taxi.send(_.time + 23) - Event(time=30, proc=13, action='drop off passenger') - >>> taxi.send(_.time + 5) - Event(time=35, proc=13, action='pick up passenger') - >>> taxi.send(_.time + 48) - Event(time=83, proc=13, action='drop off passenger') - >>> taxi.send(_.time + 1) - Event(time=84, proc=13, action='going home') - >>> taxi.send(_.time + 10) - Traceback (most recent call last): - File "", line 1, in - StopIteration - -Sample run with two cars, random seed 10. This is a valid doctest:: - - >>> main(num_taxis=2, seed=10) - taxi: 0 Event(time=0, proc=0, action='leave garage') - taxi: 0 Event(time=5, proc=0, action='pick up passenger') - taxi: 1 Event(time=5, proc=1, action='leave garage') - taxi: 1 Event(time=10, proc=1, action='pick up passenger') - taxi: 1 Event(time=15, proc=1, action='drop off passenger') - taxi: 0 Event(time=17, proc=0, action='drop off passenger') - taxi: 1 Event(time=24, proc=1, action='pick up passenger') - taxi: 0 Event(time=26, proc=0, action='pick up passenger') - taxi: 0 Event(time=30, proc=0, action='drop off passenger') - taxi: 0 Event(time=34, proc=0, action='going home') - taxi: 1 Event(time=46, proc=1, action='drop off passenger') - taxi: 1 Event(time=48, proc=1, action='pick up passenger') - taxi: 1 Event(time=110, proc=1, action='drop off passenger') - taxi: 1 Event(time=139, proc=1, action='pick up passenger') - taxi: 1 Event(time=140, proc=1, action='drop off passenger') - taxi: 1 Event(time=150, proc=1, action='going home') - *** end of events *** - -See longer sample run at the end of this module. - -""" - -import argparse -import collections -import random -import queue - - -DEFAULT_NUMBER_OF_TAXIS = 3 -DEFAULT_END_TIME = 180 -SEARCH_DURATION = 5 -TRIP_DURATION = 20 -DEPARTURE_INTERVAL = 5 - -Event = collections.namedtuple('Event', 'time proc action') - - -# tag::TAXI_PROCESS[] -def taxi_process(ident, trips, start_time=0): # <1> - """Yield to simulator issuing event at each state change""" - time = yield Event(start_time, ident, 'leave garage') # <2> - for i in range(trips): # <3> - time = yield Event(time, ident, 'pick up passenger') # <4> - time = yield Event(time, ident, 'drop off passenger') # <5> - - yield Event(time, ident, 'going home') # <6> - # end of taxi process # <7> -# end::TAXI_PROCESS[] - - -# tag::TAXI_SIMULATOR[] -class Simulator: - - def __init__(self, procs_map): - self.events = queue.PriorityQueue() - self.procs = dict(procs_map) - - def run(self, end_time): # <1> - """Schedule and display events until time is up""" - # schedule the first event for each cab - for _, proc in sorted(self.procs.items()): # <2> - first_event = next(proc) # <3> - self.events.put(first_event) # <4> - - # main loop of the simulation - sim_time = 0 # <5> - while sim_time < end_time: # <6> - if self.events.empty(): # <7> - print('*** end of events ***') - break - - current_event = self.events.get() # <8> - sim_time, proc_id, previous_action = current_event # <9> - print('taxi:', proc_id, proc_id * ' ', current_event) # <10> - active_proc = self.procs[proc_id] # <11> - next_time = sim_time + compute_duration(previous_action) # <12> - try: - next_event = active_proc.send(next_time) # <13> - except StopIteration: - del self.procs[proc_id] # <14> - else: - self.events.put(next_event) # <15> - else: # <16> - msg = '*** end of simulation time: {} events pending ***' - print(msg.format(self.events.qsize())) -# end::TAXI_SIMULATOR[] - - -def compute_duration(previous_action): - """Compute action duration using exponential distribution""" - if previous_action in ['leave garage', 'drop off passenger']: - # new state is prowling - interval = SEARCH_DURATION - elif previous_action == 'pick up passenger': - # new state is trip - interval = TRIP_DURATION - elif previous_action == 'going home': - interval = 1 - else: - raise ValueError(f'Unknown previous_action: {previous_action}') - return int(random.expovariate(1 / interval)) + 1 - - -def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS, - seed=None): - """Initialize random generator, build procs and run simulation""" - if seed is not None: - random.seed(seed) # get reproducible results - - taxis = {i: taxi_process(i, (i + 1) * 2, i * DEPARTURE_INTERVAL) - for i in range(num_taxis)} - sim = Simulator(taxis) - sim.run(end_time) - - -if __name__ == '__main__': - - parser = argparse.ArgumentParser( - description='Taxi fleet simulator.') - parser.add_argument('-e', '--end-time', type=int, - default=DEFAULT_END_TIME, - help='simulation end time; default = %s' - % DEFAULT_END_TIME) - parser.add_argument('-t', '--taxis', type=int, - default=DEFAULT_NUMBER_OF_TAXIS, - help='number of taxis running; default = %s' - % DEFAULT_NUMBER_OF_TAXIS) - parser.add_argument('-s', '--seed', type=int, default=None, - help='random generator seed (for testing)') - - args = parser.parse_args() - main(args.end_time, args.taxis, args.seed) - - -""" - -Sample run from the command line, seed=3, maximum elapsed time=120:: - -# tag::TAXI_SAMPLE_RUN[] -$ python3 taxi_sim.py -s 3 -e 120 -taxi: 0 Event(time=0, proc=0, action='leave garage') -taxi: 0 Event(time=2, proc=0, action='pick up passenger') -taxi: 1 Event(time=5, proc=1, action='leave garage') -taxi: 1 Event(time=8, proc=1, action='pick up passenger') -taxi: 2 Event(time=10, proc=2, action='leave garage') -taxi: 2 Event(time=15, proc=2, action='pick up passenger') -taxi: 2 Event(time=17, proc=2, action='drop off passenger') -taxi: 0 Event(time=18, proc=0, action='drop off passenger') -taxi: 2 Event(time=18, proc=2, action='pick up passenger') -taxi: 2 Event(time=25, proc=2, action='drop off passenger') -taxi: 1 Event(time=27, proc=1, action='drop off passenger') -taxi: 2 Event(time=27, proc=2, action='pick up passenger') -taxi: 0 Event(time=28, proc=0, action='pick up passenger') -taxi: 2 Event(time=40, proc=2, action='drop off passenger') -taxi: 2 Event(time=44, proc=2, action='pick up passenger') -taxi: 1 Event(time=55, proc=1, action='pick up passenger') -taxi: 1 Event(time=59, proc=1, action='drop off passenger') -taxi: 0 Event(time=65, proc=0, action='drop off passenger') -taxi: 1 Event(time=65, proc=1, action='pick up passenger') -taxi: 2 Event(time=65, proc=2, action='drop off passenger') -taxi: 2 Event(time=72, proc=2, action='pick up passenger') -taxi: 0 Event(time=76, proc=0, action='going home') -taxi: 1 Event(time=80, proc=1, action='drop off passenger') -taxi: 1 Event(time=88, proc=1, action='pick up passenger') -taxi: 2 Event(time=95, proc=2, action='drop off passenger') -taxi: 2 Event(time=97, proc=2, action='pick up passenger') -taxi: 2 Event(time=98, proc=2, action='drop off passenger') -taxi: 1 Event(time=106, proc=1, action='drop off passenger') -taxi: 2 Event(time=109, proc=2, action='going home') -taxi: 1 Event(time=110, proc=1, action='going home') -*** end of events *** -# end::TAXI_SAMPLE_RUN[] - -""" diff --git a/19-coroutine/taxi_sim0.py b/19-coroutine/taxi_sim0.py deleted file mode 100644 index ed988f5..0000000 --- a/19-coroutine/taxi_sim0.py +++ /dev/null @@ -1,255 +0,0 @@ - -""" -Taxi simulator - -Sample run with two cars, random seed 10. This is a valid doctest. - - >>> main(num_taxis=2, seed=10) - taxi: 0 Event(time=0, proc=0, action='leave garage') - taxi: 0 Event(time=4, proc=0, action='pick up passenger') - taxi: 1 Event(time=5, proc=1, action='leave garage') - taxi: 1 Event(time=9, proc=1, action='pick up passenger') - taxi: 0 Event(time=10, proc=0, action='drop off passenger') - taxi: 1 Event(time=12, proc=1, action='drop off passenger') - taxi: 0 Event(time=17, proc=0, action='pick up passenger') - taxi: 1 Event(time=19, proc=1, action='pick up passenger') - taxi: 1 Event(time=21, proc=1, action='drop off passenger') - taxi: 1 Event(time=24, proc=1, action='pick up passenger') - taxi: 0 Event(time=28, proc=0, action='drop off passenger') - taxi: 1 Event(time=28, proc=1, action='drop off passenger') - taxi: 0 Event(time=29, proc=0, action='going home') - taxi: 1 Event(time=30, proc=1, action='pick up passenger') - taxi: 1 Event(time=61, proc=1, action='drop off passenger') - taxi: 1 Event(time=62, proc=1, action='going home') - *** end of events *** - -See explanation and longer sample run at the end of this module. - -""" - -import argparse -import collections -import queue -import random - -DEFAULT_NUMBER_OF_TAXIS = 3 -DEFAULT_END_TIME = 80 -SEARCH_DURATION = 4 -TRIP_DURATION = 10 -DEPARTURE_INTERVAL = 5 - -Event = collections.namedtuple('Event', 'time proc action') - - -def compute_delay(interval): - """Compute action delay using exponential distribution""" - return int(random.expovariate(1 / interval)) + 1 - -# BEGIN TAXI_PROCESS -def taxi_process(ident, trips, start_time=0): # <1> - """Yield to simulator issuing event at each state change""" - time = yield Event(start_time, ident, 'leave garage') # <2> - for i in range(trips): # <3> - prowling_ends = time + compute_delay(SEARCH_DURATION) # <4> - time = yield Event(prowling_ends, ident, 'pick up passenger') # <5> - - trip_ends = time + compute_delay(TRIP_DURATION) # <6> - time = yield Event(trip_ends, ident, 'drop off passenger') # <7> - - yield Event(time + 1, ident, 'going home') # <8> - # end of taxi process # <9> -# END TAXI_PROCESS - -# BEGIN TAXI_SIMULATOR -class Simulator: - - def __init__(self, procs_map): - self.events = queue.PriorityQueue() - self.procs = dict(procs_map) - - def run(self, end_time): # <1> - """Schedule and display events until time is up""" - # schedule the first event for each cab - for _, proc in sorted(self.procs.items()): # <2> - first_event = next(proc) # <3> - self.events.put(first_event) # <4> - - # main loop of the simulation - time = 0 - while time < end_time: # <5> - if self.events.empty(): # <6> - print('*** end of events ***') - break - - # get and display current event - current_event = self.events.get() # <7> - print('taxi:', current_event.proc, # <8> - current_event.proc * ' ', current_event) - - # schedule next action for current proc - time = current_event.time # <9> - proc = self.procs[current_event.proc] # <10> - try: - next_event = proc.send(time) # <11> - except StopIteration: - del self.procs[current_event.proc] # <12> - else: - self.events.put(next_event) # <13> - else: # <14> - msg = '*** end of simulation time: {} events pending ***' - print(msg.format(self.events.qsize())) -# END TAXI_SIMULATOR - -def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS, - seed=None): - """Initialize random generator, build procs and run simulation""" - if seed is not None: - random.seed(seed) # get reproducible results - - taxis = {i: taxi_process(i, (i + 1) * 2, i * DEPARTURE_INTERVAL) - for i in range(num_taxis)} - sim = Simulator(taxis) - sim.run(end_time) - - -if __name__ == '__main__': - - parser = argparse.ArgumentParser( - description='Taxi fleet simulator.') - parser.add_argument('-e', '--end-time', type=int, - default=DEFAULT_END_TIME, - help='simulation end time; default = %s' - % DEFAULT_END_TIME) - parser.add_argument('-t', '--taxis', type=int, - default=DEFAULT_NUMBER_OF_TAXIS, - help='number of taxis running; default = %s' - % DEFAULT_NUMBER_OF_TAXIS) - parser.add_argument('-s', '--seed', type=int, default=None, - help='random generator seed (for testing)') - - args = parser.parse_args() - main(args.end_time, args.taxis, args.seed) - - -""" -Notes for the ``taxi_process`` coroutine:: - -<1> `taxi_process` will be called once per taxi, creating a generator - object to represent its operations. `ident` is the number of the taxi - (eg. 0, 1, 2 in the sample run); `trips` is the number of trips this - taxi will make before going home; `start_time` is when the taxi - leaves the garage. - -<2> The first `Event` yielded is `'leave garage'`. This suspends the - coroutine, and lets the simulation main loop proceed to the next - scheduled event. When it's time to reactivate this process, the main - loop will `send` the current simulation time, which is assigned to - `time`. - -<3> This block will be repeated once for each trip. - -<4> The ending time of the search for a passenger is computed. - -<5> An `Event` signaling passenger pick up is yielded. The coroutine - pauses here. When the time comes to reactivate this coroutine, - the main loop will again `send` the current time. - -<6> The ending time of the trip is computed, taking into account the - current `time`. - -<7> An `Event` signaling passenger drop off is yielded. Coroutine - suspended again, waiting for the main loop to send the time of when - it's time to continue. - -<8> The `for` loop ends after the given number of trips, and a final - `'going home'` event is yielded, to happen 1 minute after the current - time. The coroutine will suspend for the last time. When reactivated, - it will be sent the time from the simulation main loop, but here I - don't assign it to any variable because it will not be useful. - -<9> When the coroutine falls off the end, the coroutine object raises - `StopIteration`. - - -Notes for the ``Simulator.run`` method:: - -<1> The simulation `end_time` is the only required argument for `run`. - -<2> Use `sorted` to retrieve the `self.procs` items ordered by the - integer key; we don't care about the key, so assign it to `_`. - -<3> `next(proc)` primes each coroutine by advancing it to the first - yield, so it's ready to be sent data. An `Event` is yielded. - -<4> Add each event to the `self.events` `PriorityQueue`. The first - event for each taxi is `'leave garage'`, as seen in the sample run - (ex_taxi_process>>). - -<5> Main loop of the simulation: run until the current `time` equals - or exceeds the `end_time`. - -<6> The main loop may also exit if there are no pending events in the - queue. - -<7> Get `Event` with the smallest `time` in the queue; this is the - `current_event`. - -<8> Display the `Event`, identifying the taxi and adding indentation - according to the taxi id. - -<9> Update the simulation time with the time of the `current_event`. - -<10> Retrieve the coroutine for this taxi from the `self.procs` - dictionary. - -<11> Send the `time` to the coroutine. The coroutine will yield the - `next_event` or raise `StopIteration` it's finished. - -<12> If `StopIteration` was raised, delete the coroutine from the - `self.procs` dictionary. - -<13> Otherwise, put the `next_event` in the queue. - -<14> If the loop exits because the simulation time passed, display the - number of events pending (which may be zero by coincidence, - sometimes). - - -Sample run from the command line, seed=24, total elapsed time=160:: - -# BEGIN TAXI_SAMPLE_RUN -$ python3 taxi_sim.py -s 24 -e 160 -taxi: 0 Event(time=0, proc=0, action='leave garage') -taxi: 0 Event(time=5, proc=0, action='pick up passenger') -taxi: 1 Event(time=5, proc=1, action='leave garage') -taxi: 1 Event(time=6, proc=1, action='pick up passenger') -taxi: 2 Event(time=10, proc=2, action='leave garage') -taxi: 2 Event(time=11, proc=2, action='pick up passenger') -taxi: 2 Event(time=23, proc=2, action='drop off passenger') -taxi: 0 Event(time=24, proc=0, action='drop off passenger') -taxi: 2 Event(time=24, proc=2, action='pick up passenger') -taxi: 2 Event(time=26, proc=2, action='drop off passenger') -taxi: 0 Event(time=30, proc=0, action='pick up passenger') -taxi: 2 Event(time=31, proc=2, action='pick up passenger') -taxi: 0 Event(time=43, proc=0, action='drop off passenger') -taxi: 0 Event(time=44, proc=0, action='going home') -taxi: 2 Event(time=46, proc=2, action='drop off passenger') -taxi: 2 Event(time=49, proc=2, action='pick up passenger') -taxi: 1 Event(time=70, proc=1, action='drop off passenger') -taxi: 2 Event(time=70, proc=2, action='drop off passenger') -taxi: 2 Event(time=71, proc=2, action='pick up passenger') -taxi: 2 Event(time=79, proc=2, action='drop off passenger') -taxi: 1 Event(time=88, proc=1, action='pick up passenger') -taxi: 2 Event(time=92, proc=2, action='pick up passenger') -taxi: 2 Event(time=98, proc=2, action='drop off passenger') -taxi: 2 Event(time=99, proc=2, action='going home') -taxi: 1 Event(time=102, proc=1, action='drop off passenger') -taxi: 1 Event(time=104, proc=1, action='pick up passenger') -taxi: 1 Event(time=135, proc=1, action='drop off passenger') -taxi: 1 Event(time=136, proc=1, action='pick up passenger') -taxi: 1 Event(time=151, proc=1, action='drop off passenger') -taxi: 1 Event(time=152, proc=1, action='going home') -*** end of events *** -# END TAXI_SAMPLE_RUN - -""" diff --git a/19-coroutine/taxi_sim_delay.py b/19-coroutine/taxi_sim_delay.py deleted file mode 100644 index 8f38e4f..0000000 --- a/19-coroutine/taxi_sim_delay.py +++ /dev/null @@ -1,215 +0,0 @@ - -""" -Taxi simulator with delay on output -=================================== - -This is a variation of ``taxi_sim.py`` which adds a ``-d`` comand-line -option. When given, that option adds a delay in the main loop, pausing -the simulation for .5s for each "minute" of simulation time. - - -Driving a taxi from the console:: - - >>> from taxi_sim import taxi_process - >>> taxi = taxi_process(ident=13, trips=2, start_time=0) - >>> next(taxi) - Event(time=0, proc=13, action='leave garage') - >>> taxi.send(_.time + 7) - Event(time=7, proc=13, action='pick up passenger') - >>> taxi.send(_.time + 23) - Event(time=30, proc=13, action='drop off passenger') - >>> taxi.send(_.time + 5) - Event(time=35, proc=13, action='pick up passenger') - >>> taxi.send(_.time + 48) - Event(time=83, proc=13, action='drop off passenger') - >>> taxi.send(_.time + 1) - Event(time=84, proc=13, action='going home') - >>> taxi.send(_.time + 10) - Traceback (most recent call last): - File "", line 1, in - StopIteration - -Sample run with two cars, random seed 10. This is a valid doctest:: - - >>> main(num_taxis=2, seed=10) - taxi: 0 Event(time=0, proc=0, action='leave garage') - taxi: 0 Event(time=5, proc=0, action='pick up passenger') - taxi: 1 Event(time=5, proc=1, action='leave garage') - taxi: 1 Event(time=10, proc=1, action='pick up passenger') - taxi: 1 Event(time=15, proc=1, action='drop off passenger') - taxi: 0 Event(time=17, proc=0, action='drop off passenger') - taxi: 1 Event(time=24, proc=1, action='pick up passenger') - taxi: 0 Event(time=26, proc=0, action='pick up passenger') - taxi: 0 Event(time=30, proc=0, action='drop off passenger') - taxi: 0 Event(time=34, proc=0, action='going home') - taxi: 1 Event(time=46, proc=1, action='drop off passenger') - taxi: 1 Event(time=48, proc=1, action='pick up passenger') - taxi: 1 Event(time=110, proc=1, action='drop off passenger') - taxi: 1 Event(time=139, proc=1, action='pick up passenger') - taxi: 1 Event(time=140, proc=1, action='drop off passenger') - taxi: 1 Event(time=150, proc=1, action='going home') - *** end of events *** - -See longer sample run at the end of this module. - -""" - -import random -import collections -import queue -import argparse -import time - -DEFAULT_NUMBER_OF_TAXIS = 3 -DEFAULT_END_TIME = 180 -SEARCH_DURATION = 5 -TRIP_DURATION = 20 -DEPARTURE_INTERVAL = 5 - -Event = collections.namedtuple('Event', 'time proc action') - - -# BEGIN TAXI_PROCESS -def taxi_process(ident, trips, start_time=0): # <1> - """Yield to simulator issuing event at each state change""" - time = yield Event(start_time, ident, 'leave garage') # <2> - for i in range(trips): # <3> - time = yield Event(time, ident, 'pick up passenger') # <4> - time = yield Event(time, ident, 'drop off passenger') # <5> - - yield Event(time, ident, 'going home') # <6> - # end of taxi process # <7> -# END TAXI_PROCESS - - -# BEGIN TAXI_SIMULATOR -class Simulator: - - def __init__(self, procs_map): - self.events = queue.PriorityQueue() - self.procs = dict(procs_map) - - def run(self, end_time, delay=False): # <1> - """Schedule and display events until time is up""" - # schedule the first event for each cab - for _, proc in sorted(self.procs.items()): # <2> - first_event = next(proc) # <3> - self.events.put(first_event) # <4> - - # main loop of the simulation - sim_time = 0 # <5> - while sim_time < end_time: # <6> - if self.events.empty(): # <7> - print('*** end of events ***') - break - - # get and display current event - current_event = self.events.get() # <8> - if delay: - time.sleep((current_event.time - sim_time) / 2) - # update the simulation time - sim_time, proc_id, previous_action = current_event - print('taxi:', proc_id, proc_id * ' ', current_event) - active_proc = self.procs[proc_id] - # schedule next action for current proc - next_time = sim_time + compute_duration(previous_action) - try: - next_event = active_proc.send(next_time) # <12> - except StopIteration: - del self.procs[proc_id] # <13> - else: - self.events.put(next_event) # <14> - else: # <15> - msg = '*** end of simulation time: {} events pending ***' - print(msg.format(self.events.qsize())) -# END TAXI_SIMULATOR - - -def compute_duration(previous_action): - """Compute action duration using exponential distribution""" - if previous_action in ['leave garage', 'drop off passenger']: - # new state is prowling - interval = SEARCH_DURATION - elif previous_action == 'pick up passenger': - # new state is trip - interval = TRIP_DURATION - elif previous_action == 'going home': - interval = 1 - else: - raise ValueError('Unknown previous_action: %s' % previous_action) - return int(random.expovariate(1/interval)) + 1 - - -def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS, - seed=None, delay=False): - """Initialize random generator, build procs and run simulation""" - if seed is not None: - random.seed(seed) # get reproducible results - - taxis = {i: taxi_process(i, (i+1)*2, i*DEPARTURE_INTERVAL) - for i in range(num_taxis)} - sim = Simulator(taxis) - sim.run(end_time, delay) - - -if __name__ == '__main__': - - parser = argparse.ArgumentParser( - description='Taxi fleet simulator.') - parser.add_argument('-e', '--end-time', type=int, - default=DEFAULT_END_TIME, - help='simulation end time; default = %s' - % DEFAULT_END_TIME) - parser.add_argument('-t', '--taxis', type=int, - default=DEFAULT_NUMBER_OF_TAXIS, - help='number of taxis running; default = %s' - % DEFAULT_NUMBER_OF_TAXIS) - parser.add_argument('-s', '--seed', type=int, default=None, - help='random generator seed (for testing)') - parser.add_argument('-d', '--delay', action='store_true', - help='introduce delay proportional to simulation time') - - args = parser.parse_args() - main(args.end_time, args.taxis, args.seed, args.delay) - - -""" - -Sample run from the command line, seed=3, maximum elapsed time=120:: - -# BEGIN TAXI_SAMPLE_RUN -$ python3 taxi_sim.py -s 3 -e 120 -taxi: 0 Event(time=0, proc=0, action='leave garage') -taxi: 0 Event(time=2, proc=0, action='pick up passenger') -taxi: 1 Event(time=5, proc=1, action='leave garage') -taxi: 1 Event(time=8, proc=1, action='pick up passenger') -taxi: 2 Event(time=10, proc=2, action='leave garage') -taxi: 2 Event(time=15, proc=2, action='pick up passenger') -taxi: 2 Event(time=17, proc=2, action='drop off passenger') -taxi: 0 Event(time=18, proc=0, action='drop off passenger') -taxi: 2 Event(time=18, proc=2, action='pick up passenger') -taxi: 2 Event(time=25, proc=2, action='drop off passenger') -taxi: 1 Event(time=27, proc=1, action='drop off passenger') -taxi: 2 Event(time=27, proc=2, action='pick up passenger') -taxi: 0 Event(time=28, proc=0, action='pick up passenger') -taxi: 2 Event(time=40, proc=2, action='drop off passenger') -taxi: 2 Event(time=44, proc=2, action='pick up passenger') -taxi: 1 Event(time=55, proc=1, action='pick up passenger') -taxi: 1 Event(time=59, proc=1, action='drop off passenger') -taxi: 0 Event(time=65, proc=0, action='drop off passenger') -taxi: 1 Event(time=65, proc=1, action='pick up passenger') -taxi: 2 Event(time=65, proc=2, action='drop off passenger') -taxi: 2 Event(time=72, proc=2, action='pick up passenger') -taxi: 0 Event(time=76, proc=0, action='going home') -taxi: 1 Event(time=80, proc=1, action='drop off passenger') -taxi: 1 Event(time=88, proc=1, action='pick up passenger') -taxi: 2 Event(time=95, proc=2, action='drop off passenger') -taxi: 2 Event(time=97, proc=2, action='pick up passenger') -taxi: 2 Event(time=98, proc=2, action='drop off passenger') -taxi: 1 Event(time=106, proc=1, action='drop off passenger') -taxi: 2 Event(time=109, proc=2, action='going home') -taxi: 1 Event(time=110, proc=1, action='going home') -*** end of events *** -# END TAXI_SAMPLE_RUN - -""" diff --git a/19-coroutine/yield_from_expansion.py b/19-coroutine/yield_from_expansion.py deleted file mode 100644 index 837925f..0000000 --- a/19-coroutine/yield_from_expansion.py +++ /dev/null @@ -1,52 +0,0 @@ -# Code below is the expansion of the statement: -# -# RESULT = yield from EXPR -# -# Copied verbatim from the Formal Semantics section of -# PEP 380 -- Syntax for Delegating to a Subgenerator -# -# https://www.python.org/dev/peps/pep-0380/#formal-semantics - - -# tag::YIELD_FROM_EXPANSION[] -_i = iter(EXPR) # <1> -try: - _y = next(_i) # <2> -except StopIteration as _e: - _r = _e.value # <3> -else: - while 1: # <4> - try: - _s = yield _y # <5> - except GeneratorExit as _e: # <6> - try: - _m = _i.close - except AttributeError: - pass - else: - _m() - raise _e - except BaseException as _e: # <7> - _x = sys.exc_info() - try: - _m = _i.throw - except AttributeError: - raise _e - else: # <8> - try: - _y = _m(*_x) - except StopIteration as _e: - _r = _e.value - break - else: # <9> - try: # <10> - if _s is None: # <11> - _y = next(_i) - else: - _y = _i.send(_s) - except StopIteration as _e: # <12> - _r = _e.value - break - -RESULT = _r # <13> -# end::YIELD_FROM_EXPANSION[] diff --git a/19-coroutine/yield_from_expansion_simplified.py b/19-coroutine/yield_from_expansion_simplified.py deleted file mode 100644 index 7648da5..0000000 --- a/19-coroutine/yield_from_expansion_simplified.py +++ /dev/null @@ -1,32 +0,0 @@ -# Code below is a very simplified expansion of the statement: -# -# RESULT = yield from EXPR -# -# This code assumes that the subgenerator will run to completion, -# without the client ever calling ``.throw()`` or ``.close()``. -# Also, this code makes no distinction between the client -# calling ``next(subgen)`` or ``subgen.send(...)`` -# -# The full expansion is in: -# PEP 380 -- Syntax for Delegating to a Subgenerator -# -# https://www.python.org/dev/peps/pep-0380/#formal-semantics - - -# tag::YIELD_FROM_EXPANSION_SIMPLIFIED[] -_i = iter(EXPR) # <1> -try: - _y = next(_i) # <2> -except StopIteration as _e: - _r = _e.value # <3> -else: - while 1: # <4> - _s = yield _y # <5> - try: - _y = _i.send(_s) # <6> - except StopIteration as _e: # <7> - _r = _e.value - break - -RESULT = _r # <8> -# end::YIELD_FROM_EXPANSION_SIMPLIFIED[] diff --git a/20-concurrency/primes/threads.py b/20-concurrency/primes/threads.py deleted file mode 100644 index 0161dd7..0000000 --- a/20-concurrency/primes/threads.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 - -""" -threads.py: shows that Python threads are slower than -sequential code for CPU-intensive work. -""" - -import os -import sys -from queue import SimpleQueue -from time import perf_counter -from typing import NamedTuple -from threading import Thread - -from primes import is_prime, NUMBERS - -class PrimeResult(NamedTuple): # <3> - n: int - prime: bool - elapsed: float - -JobQueue = SimpleQueue[int] -ResultQueue = SimpleQueue[PrimeResult] - -def check(n: int) -> PrimeResult: - t0 = perf_counter() - res = is_prime(n) - return PrimeResult(n, res, perf_counter() - t0) - -def worker(jobs: JobQueue, results: ResultQueue) -> None: - while n := jobs.get(): - results.put(check(n)) - -def main() -> None: - if len(sys.argv) < 2: # <1> - workers = os.cpu_count() or 1 # make mypy happy - else: - workers = int(sys.argv[1]) - - print(f'Checking {len(NUMBERS)} numbers with {workers} threads:') - - jobs: JobQueue = SimpleQueue() # <2> - results: ResultQueue = SimpleQueue() - t0 = perf_counter() - - for n in NUMBERS: # <3> - jobs.put(n) - - for _ in range(workers): - proc = Thread(target=worker, args=(jobs, results)) # <4> - proc.start() # <5> - jobs.put(0) # <6> - - while True: - n, prime, elapsed = results.get() # <7> - label = 'P' if prime else ' ' - print(f'{n:16} {label} {elapsed:9.6f}s') - if jobs.empty(): # <8> - break - - elapsed = perf_counter() - t0 - print(f'Total time: {elapsed:.2f}s') - -if __name__ == '__main__': - main() diff --git a/21-futures/demo_executor_map.py b/20-executors/demo_executor_map.py similarity index 100% rename from 21-futures/demo_executor_map.py rename to 20-executors/demo_executor_map.py diff --git a/20-executors/getflags/.gitignore b/20-executors/getflags/.gitignore new file mode 100644 index 0000000..aad8463 --- /dev/null +++ b/20-executors/getflags/.gitignore @@ -0,0 +1,2 @@ +flags/ +downloaded/ diff --git a/20-executors/getflags/README.adoc b/20-executors/getflags/README.adoc new file mode 100644 index 0000000..a649994 --- /dev/null +++ b/20-executors/getflags/README.adoc @@ -0,0 +1,122 @@ += Experimenting with the `flags2*` examples + +The `flags2*` examples enhance the `flags*` examples with error handling and reporting. +Therefore, we need a server that generates errors and delays to experiment with them. + +The main reason for these instructions is to document how to configure one such server +in your machine, and how to tell the `flags2*` clients to access it. +The other reason is to alert of an installation step that MacOS users sometimes overlook. + +Contents: + +* <> +* <> +* <> + +[[server_setup]] +== Setting up test servers + +If you don't already have a local HTTP server for testing, +here are the steps to experiment with the `flags2*` examples +using just the Python ≥ 3.9 distribution: + +. Clone or download the https://github.com/fluentpython/example-code-2e[_Fluent Python 2e_ code repository] (this repo!). +. Open your shell and go to the _20-futures/getflags/_ directory of your local copy of the repository (this directory!) +. Unzip the _flags.zip_ file, creating a _flags_ directory at _20-futures/getflags/flags/_. +. Open a second shell, go to the _20-futures/getflags/_ directory and run `python3 -m http.server`. This will start a `ThreadingHTTPServer` listening to port 8000, serving the local files. If you open the URL http://localhost:8000/flags/[http://localhost:8000/flags/] with your browser, you'll see a long list of directories named with two-letter country codes from `ad/` to `zw/`. +. Now you can go back to the first shell and run the _flags2*.py_ examples with the default `--server LOCAL` option. +. To test with the `--server DELAY` option, go to _20-futures/getflags/_ and run `python3 slow_server.py`. This binds to port 8001 by default. It will add a random delay of .5s to 5s before each response. +. To test with the `--server ERROR` option, go to _20-futures/getflags/_ and run `python3 slow_server.py 8002 --error-rate .25`. +Each request will have a 25% probability of getting a +https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418[418 I'm a teapot] response, +and all responses will be delayed .5s. + +I wrote _slow_server.py_ reusing code from Python's +https://github.com/python/cpython/blob/917eca700aa341f8544ace43b75d41b477e98b72/Lib/http/server.py[`http.server`] standard library module, +which "is not recommended for production"—according to the +https://docs.python.org/3/library/http.server.html[documentation]. + +[NOTE] +==== +This is a simple testing environment that does not require any external libraries or +tools—apart from the libraries used in the `flags2*` scripts themselves, as discussed in the book. + +For a more robust testing environment, I recommend configuring +https://www.nginx.com/[NGINX] and +https://github.com/shopify/toxiproxy[Toxiproxy] with equivalent parameters. +==== + +[[client_setup]] +== Running a `flags2*` script + +The `flags2*` examples provide a command-line interface. +All three scripts accept the same options, +and you can see them by running any of the scripts with the `-h` option: + +[[flags2_help_demo]] +.Help screen for the scripts in the flags2 series +==== +[source, text] +---- +$ python3 flags2_threadpool.py -h +usage: flags2_threadpool.py [-h] [-a] [-e] [-l N] [-m CONCURRENT] [-s LABEL] + [-v] + [CC [CC ...]] + +Download flags for country codes. Default: top 20 countries by population. + +positional arguments: + CC country code or 1st letter (eg. B for BA...BZ) + +optional arguments: + -h, --help show this help message and exit + -a, --all get all available flags (AD to ZW) + -e, --every get flags for every possible code (AA...ZZ) + -l N, --limit N limit to N first codes + -m CONCURRENT, --max_req CONCURRENT + maximum concurrent requests (default=30) + -s LABEL, --server LABEL + Server to hit; one of DELAY, ERROR, LOCAL, REMOTE + (default=LOCAL) + -v, --verbose output detailed progress info + +---- +==== + +All arguments are optional. The most important arguments are discussed next. + +One option you can't ignore is `-s/--server`: it lets you choose which HTTP server and base URL will be used in the test. +You can pass one of four labels to determine where the script will look for the flags (the labels are case-insensitive): + +`LOCAL`:: Use `http://localhost:8000/flags`; this is the default. +You should configure a local HTTP server to answer at port 8000. See <> for instructions. +Feel free to hit this as hard as you can. It's your machine! + +`REMOTE`:: Use `http://fluentpython.com/data/flags`; that is a public website owned by me, hosted on a shared server. +Please do not hit it with too many concurrent requests. +The `fluentpython.com` domain is handled by the http://www.cloudflare.com/[Cloudflare] CDN (Content Delivery Network) +so you may notice that the first downloads are slower, but they get faster when the CDN cache warms +up.footnote:[Before configuring Cloudflare, I got HTTP 503 errors--Service Temporarily Unavailable--when +testing the scripts with a few dozen concurrent requests on my inexpensive shared host account. Now those errors are gone.] + +`DELAY`:: Use `http://localhost:8001/flags`; a server delaying HTTP responses should be listening to port 8001. +I wrote _slow_server.py_ to make it easier to experiment. See <> for instructions. + +`ERROR`:: Use `http://localhost:8002/flags`; a server introducing HTTP errors and delaying responses should be installed at port 8002. +Running _slow_server.py_ is an easy way to do it. See <>. + +[[macos_certificates]] +== Install SSL Certificates (for MacOS) + +On Macos, depending on how you installed Python you may need to manually run a command +after Python's installer finishes, to install the SSL certificates Python uses to make HTTPS connections. + +Using the Finder, open the `Python 3.X` folder inside `/Applications` folder +and double-click "Install Certificates" or "Install Certificates.command". + +Using the terminal, you can type for example: + +[source, text] +---- +$ open /Applications/Python 3.10/"Install Certificates.command" +---- diff --git a/21-futures/getflags/country_codes.txt b/20-executors/getflags/country_codes.txt similarity index 100% rename from 21-futures/getflags/country_codes.txt rename to 20-executors/getflags/country_codes.txt diff --git a/21-futures/getflags/flags.py b/20-executors/getflags/flags.py similarity index 64% rename from 21-futures/getflags/flags.py rename to 20-executors/getflags/flags.py index 3d6d36f..2311125 100755 --- a/21-futures/getflags/flags.py +++ b/20-executors/getflags/flags.py @@ -21,12 +21,12 @@ from pathlib import Path from typing import Callable -import requests # <1> +import httpx # <1> POP20_CC = ('CN IN US ID BR PK NG BD RU JP ' 'MX PH VN ET EG DE IR TR CD FR').split() # <2> -BASE_URL = 'http://fluentpython.com/data/flags' # <3> +BASE_URL = 'https://www.fluentpython.com/data/flags' # <3> DEST_DIR = Path('downloaded') # <4> def save_flag(img: bytes, filename: str) -> None: # <5> @@ -34,22 +34,25 @@ def save_flag(img: bytes, filename: str) -> None: # <5> def get_flag(cc: str) -> bytes: # <6> url = f'{BASE_URL}/{cc}/{cc}.gif'.lower() - resp = requests.get(url) + resp = httpx.get(url, timeout=6.1, # <7> + follow_redirects=True) # <8> + resp.raise_for_status() # <9> return resp.content -def download_many(cc_list: list[str]) -> int: # <7> - for cc in sorted(cc_list): # <8> +def download_many(cc_list: list[str]) -> int: # <10> + for cc in sorted(cc_list): # <11> image = get_flag(cc) save_flag(image, f'{cc}.gif') - print(cc, end=' ', flush=True) # <9> + print(cc, end=' ', flush=True) # <12> return len(cc_list) -def main(downloader: Callable[[list[str]], int]) -> None: # <10> - t0 = time.perf_counter() # <11> +def main(downloader: Callable[[list[str]], int]) -> None: # <13> + DEST_DIR.mkdir(exist_ok=True) # <14> + t0 = time.perf_counter() # <15> count = downloader(POP20_CC) elapsed = time.perf_counter() - t0 print(f'\n{count} downloads in {elapsed:.2f}s') if __name__ == '__main__': - main(download_many) # <12> + main(download_many) # <16> # end::FLAGS_PY[] diff --git a/21-futures/getflags/flags.zip b/20-executors/getflags/flags.zip similarity index 100% rename from 21-futures/getflags/flags.zip rename to 20-executors/getflags/flags.zip diff --git a/20-executors/getflags/flags2_asyncio.py b/20-executors/getflags/flags2_asyncio.py new file mode 100755 index 0000000..b697840 --- /dev/null +++ b/20-executors/getflags/flags2_asyncio.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +"""Download flags of countries (with error handling). + +asyncio async/await version + +""" +# tag::FLAGS2_ASYNCIO_TOP[] +import asyncio +from collections import Counter +from http import HTTPStatus +from pathlib import Path + +import httpx +import tqdm # type: ignore + +from flags2_common import main, DownloadStatus, save_flag + +# low concurrency default to avoid errors from remote site, +# such as 503 - Service Temporarily Unavailable +DEFAULT_CONCUR_REQ = 5 +MAX_CONCUR_REQ = 1000 + +async def get_flag(client: httpx.AsyncClient, # <1> + base_url: str, + cc: str) -> bytes: + url = f'{base_url}/{cc}/{cc}.gif'.lower() + resp = await client.get(url, timeout=3.1, follow_redirects=True) # <2> + resp.raise_for_status() + return resp.content + +async def download_one(client: httpx.AsyncClient, + cc: str, + base_url: str, + semaphore: asyncio.Semaphore, + verbose: bool) -> DownloadStatus: + try: + async with semaphore: # <3> + image = await get_flag(client, base_url, cc) + except httpx.HTTPStatusError as exc: # <4> + res = exc.response + if res.status_code == HTTPStatus.NOT_FOUND: + status = DownloadStatus.NOT_FOUND + msg = f'not found: {res.url}' + else: + raise + else: + await asyncio.to_thread(save_flag, image, f'{cc}.gif') # <5> + status = DownloadStatus.OK + msg = 'OK' + if verbose and msg: + print(cc, msg) + return status +# end::FLAGS2_ASYNCIO_TOP[] + +# tag::FLAGS2_ASYNCIO_START[] +async def supervisor(cc_list: list[str], + base_url: str, + verbose: bool, + concur_req: int) -> Counter[DownloadStatus]: # <1> + counter: Counter[DownloadStatus] = Counter() + semaphore = asyncio.Semaphore(concur_req) # <2> + async with httpx.AsyncClient() as client: + to_do = [download_one(client, cc, base_url, semaphore, verbose) + for cc in sorted(cc_list)] # <3> + to_do_iter = asyncio.as_completed(to_do) # <4> + if not verbose: + to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) # <5> + error: httpx.HTTPError | None = None # <6> + for coro in to_do_iter: # <7> + try: + status = await coro # <8> + except httpx.HTTPStatusError as exc: + error_msg = 'HTTP error {resp.status_code} - {resp.reason_phrase}' + error_msg = error_msg.format(resp=exc.response) + error = exc # <9> + except httpx.RequestError as exc: + error_msg = f'{exc} {type(exc)}'.strip() + error = exc # <10> + except KeyboardInterrupt: + break + else: + error = None + + if error: + status = DownloadStatus.ERROR # <11> + if verbose: + url = str(error.request.url) # <12> + cc = Path(url).stem.upper() # <13> + print(f'{cc} error: {error_msg}') + counter[status] += 1 + + return counter + +def download_many(cc_list: list[str], + base_url: str, + verbose: bool, + concur_req: int) -> Counter[DownloadStatus]: + coro = supervisor(cc_list, base_url, verbose, concur_req) + counts = asyncio.run(coro) # <14> + + return counts + +if __name__ == '__main__': + main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ) +# end::FLAGS2_ASYNCIO_START[] diff --git a/20-executors/getflags/flags2_asyncio_executor.py b/20-executors/getflags/flags2_asyncio_executor.py new file mode 100755 index 0000000..62516b3 --- /dev/null +++ b/20-executors/getflags/flags2_asyncio_executor.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +"""Download flags of countries (with error handling). + +asyncio async/await version + +""" +# tag::FLAGS2_ASYNCIO_TOP[] +import asyncio +from collections import Counter +from http import HTTPStatus +from pathlib import Path + +import httpx +import tqdm # type: ignore + +from flags2_common import main, DownloadStatus, save_flag + +# default set low to avoid errors from remote site, such as +# 503 - Service Temporarily Unavailable +DEFAULT_CONCUR_REQ = 5 +MAX_CONCUR_REQ = 1000 + + +async def get_flag(client: httpx.AsyncClient, # <2> + base_url: str, + cc: str) -> bytes: + url = f'{base_url}/{cc}/{cc}.gif'.lower() + resp = await client.get(url, timeout=3.1, follow_redirects=True) # <3> + resp.raise_for_status() + return resp.content + + +async def download_one(client: httpx.AsyncClient, + cc: str, + base_url: str, + semaphore: asyncio.Semaphore, + verbose: bool) -> DownloadStatus: + try: + async with semaphore: + image = await get_flag(client, base_url, cc) + except httpx.HTTPStatusError as exc: + res = exc.response + if res.status_code == HTTPStatus.NOT_FOUND: + status = DownloadStatus.NOT_FOUND + msg = f'not found: {res.url}' + else: + raise + else: +# tag::FLAGS2_ASYNCIO_EXECUTOR[] + loop = asyncio.get_running_loop() # <1> + loop.run_in_executor(None, save_flag, # <2> + image, f'{cc}.gif') # <3> +# end::FLAGS2_ASYNCIO_EXECUTOR[] + status = DownloadStatus.OK + msg = 'OK' + if verbose and msg: + print(cc, msg) + return status + +async def supervisor(cc_list: list[str], + base_url: str, + verbose: bool, + concur_req: int) -> Counter[DownloadStatus]: # <1> + counter: Counter[DownloadStatus] = Counter() + semaphore = asyncio.Semaphore(concur_req) # <2> + async with httpx.AsyncClient() as client: + to_do = [download_one(client, cc, base_url, semaphore, verbose) + for cc in sorted(cc_list)] # <3> + to_do_iter = asyncio.as_completed(to_do) # <4> + if not verbose: + to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) # <5> + error: httpx.HTTPError | None = None + for coro in to_do_iter: # <6> + try: + status = await coro # <7> + except httpx.HTTPStatusError as exc: # <13> + error_msg = 'HTTP error {resp.status_code} - {resp.reason_phrase}' + error_msg = error_msg.format(resp=exc.response) + error = exc + except httpx.RequestError as exc: # <15> + error_msg = f'{exc} {type(exc)}'.strip() + error = exc + except KeyboardInterrupt: # <7> + break + else: # <8> + error = None + + if error: + status = DownloadStatus.ERROR # <9> + if verbose: # <11> + cc = Path(str(error.request.url)).stem.upper() + print(f'{cc} error: {error_msg}') + counter[status] += 1 # <10> + + return counter # <12> + +def download_many(cc_list: list[str], + base_url: str, + verbose: bool, + concur_req: int) -> Counter[DownloadStatus]: + coro = supervisor(cc_list, base_url, verbose, concur_req) + counts = asyncio.run(coro) # <14> + + return counts + +if __name__ == '__main__': + main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ) +# end::FLAGS2_ASYNCIO_START[] diff --git a/21-futures/getflags/flags2_common.py b/20-executors/getflags/flags2_common.py similarity index 86% rename from 21-futures/getflags/flags2_common.py rename to 20-executors/getflags/flags2_common.py index 74cd888..1ad05d3 100644 --- a/21-futures/getflags/flags2_common.py +++ b/20-executors/getflags/flags2_common.py @@ -5,13 +5,11 @@ import string import sys import time -from collections import namedtuple, Counter +from collections import Counter from enum import Enum from pathlib import Path -Result = namedtuple('Result', 'status data') - -HTTPStatus = Enum('HTTPStatus', 'ok not_found error') +DownloadStatus = Enum('DownloadStatus', 'OK NOT_FOUND ERROR') POP20_CC = ('CN IN US ID BR PK NG BD RU JP ' 'MX PH VN ET EG DE IR TR CD FR').split() @@ -20,7 +18,7 @@ MAX_CONCUR_REQ = 1 SERVERS = { - 'REMOTE': 'http://fluentpython.com/data/flags', + 'REMOTE': 'https://www.fluentpython.com/data/flags', 'LOCAL': 'http://localhost:8000/flags', 'DELAY': 'http://localhost:8001/flags', 'ERROR': 'http://localhost:8002/flags', @@ -52,17 +50,17 @@ def initial_report(cc_list: list[str], def final_report(cc_list: list[str], - counter: Counter[HTTPStatus], + counter: Counter[DownloadStatus], start_time: float) -> None: elapsed = time.perf_counter() - start_time print('-' * 20) - plural = 's' if counter[HTTPStatus.ok] != 1 else '' - print(f'{counter[HTTPStatus.ok]} flag{plural} downloaded.') - if counter[HTTPStatus.not_found]: - print(f'{counter[HTTPStatus.not_found]} not found.') - if counter[HTTPStatus.error]: - plural = 's' if counter[HTTPStatus.error] != 1 else '' - print(f'{counter[HTTPStatus.error]} error{plural}.') + plural = 's' if counter[DownloadStatus.OK] != 1 else '' + print(f'{counter[DownloadStatus.OK]:3} flag{plural} downloaded.') + if counter[DownloadStatus.NOT_FOUND]: + print(f'{counter[DownloadStatus.NOT_FOUND]:3} not found.') + if counter[DownloadStatus.ERROR]: + plural = 's' if counter[DownloadStatus.ERROR] != 1 else '' + print(f'{counter[DownloadStatus.ERROR]:3} error{plural}.') print(f'Elapsed time: {elapsed:.2f}s') @@ -142,7 +140,7 @@ def process_args(default_concur_req): sys.exit(2) # command line usage error if not cc_list: - cc_list = sorted(POP20_CC) + cc_list = sorted(POP20_CC)[:args.limit] return args, cc_list @@ -151,9 +149,7 @@ def main(download_many, default_concur_req, max_concur_req): actual_req = min(args.max_req, max_concur_req, len(cc_list)) initial_report(cc_list, actual_req, args.server) base_url = SERVERS[args.server] + DEST_DIR.mkdir(exist_ok=True) t0 = time.perf_counter() counter = download_many(cc_list, base_url, args.verbose, actual_req) - assert sum(counter.values()) == len(cc_list), ( - 'some downloads are unaccounted for' - ) final_report(cc_list, counter, t0) diff --git a/21-futures/getflags/flags2_sequential.py b/20-executors/getflags/flags2_sequential.py similarity index 52% rename from 21-futures/getflags/flags2_sequential.py rename to 20-executors/getflags/flags2_sequential.py index 2e30856..e4358a3 100755 --- a/21-futures/getflags/flags2_sequential.py +++ b/20-executors/getflags/flags2_sequential.py @@ -17,71 +17,72 @@ """ +# tag::FLAGS2_BASIC_HTTP_FUNCTIONS[] from collections import Counter +from http import HTTPStatus -import requests -import tqdm # type: ignore +import httpx +import tqdm # type: ignore # <1> -from flags2_common import main, save_flag, HTTPStatus, Result +from flags2_common import main, save_flag, DownloadStatus # <2> DEFAULT_CONCUR_REQ = 1 MAX_CONCUR_REQ = 1 -# tag::FLAGS2_BASIC_HTTP_FUNCTIONS[] def get_flag(base_url: str, cc: str) -> bytes: url = f'{base_url}/{cc}/{cc}.gif'.lower() - resp = requests.get(url) - if resp.status_code != 200: # <1> - resp.raise_for_status() + resp = httpx.get(url, timeout=3.1, follow_redirects=True) + resp.raise_for_status() # <3> return resp.content -def download_one(cc: str, base_url: str, verbose: bool = False): +def download_one(cc: str, base_url: str, verbose: bool = False) -> DownloadStatus: try: image = get_flag(base_url, cc) - except requests.exceptions.HTTPError as exc: # <2> + except httpx.HTTPStatusError as exc: # <4> res = exc.response - if res.status_code == 404: - status = HTTPStatus.not_found # <3> - msg = 'not found' - else: # <4> - raise + if res.status_code == HTTPStatus.NOT_FOUND: + status = DownloadStatus.NOT_FOUND # <5> + msg = f'not found: {res.url}' + else: + raise # <6> else: save_flag(image, f'{cc}.gif') - status = HTTPStatus.ok + status = DownloadStatus.OK msg = 'OK' - if verbose: # <5> + if verbose: # <7> print(cc, msg) - return Result(status, cc) # <6> + return status # end::FLAGS2_BASIC_HTTP_FUNCTIONS[] # tag::FLAGS2_DOWNLOAD_MANY_SEQUENTIAL[] def download_many(cc_list: list[str], base_url: str, verbose: bool, - _unused_concur_req: int) -> Counter[int]: - counter: Counter[int] = Counter() # <1> + _unused_concur_req: int) -> Counter[DownloadStatus]: + counter: Counter[DownloadStatus] = Counter() # <1> cc_iter = sorted(cc_list) # <2> if not verbose: cc_iter = tqdm.tqdm(cc_iter) # <3> - for cc in cc_iter: # <4> + for cc in cc_iter: try: - res = download_one(cc, base_url, verbose) # <5> - except requests.exceptions.HTTPError as exc: # <6> - error_msg = 'HTTP error {res.status_code} - {res.reason}' - error_msg = error_msg.format(res=exc.response) - except requests.exceptions.ConnectionError: # <7> - error_msg = 'Connection error' + status = download_one(cc, base_url, verbose) # <4> + except httpx.HTTPStatusError as exc: # <5> + error_msg = 'HTTP error {resp.status_code} - {resp.reason_phrase}' + error_msg = error_msg.format(resp=exc.response) + except httpx.RequestError as exc: # <6> + error_msg = f'{exc} {type(exc)}'.strip() + except KeyboardInterrupt: # <7> + break else: # <8> error_msg = '' - status = res.status if error_msg: - status = HTTPStatus.error # <9> + status = DownloadStatus.ERROR # <9> counter[status] += 1 # <10> if verbose and error_msg: # <11> - print(f'*** Error for {cc}: {error_msg}') + print(f'{cc} error: {error_msg}') return counter # <12> # end::FLAGS2_DOWNLOAD_MANY_SEQUENTIAL[] diff --git a/20-executors/getflags/flags2_threadpool.py b/20-executors/getflags/flags2_threadpool.py new file mode 100755 index 0000000..62f6c3b --- /dev/null +++ b/20-executors/getflags/flags2_threadpool.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +"""Download flags of countries (with error handling). + +ThreadPool version + +Sample run:: + + $ python3 flags2_threadpool.py -s ERROR -e + ERROR site: http://localhost:8003/flags + Searching for 676 flags: from AA to ZZ + 30 concurrent connections will be used. + -------------------- + 150 flags downloaded. + 361 not found. + 165 errors. + Elapsed time: 7.46s + +""" + +# tag::FLAGS2_THREADPOOL[] +from collections import Counter +from concurrent.futures import ThreadPoolExecutor, as_completed + +import httpx +import tqdm # type: ignore + +from flags2_common import main, DownloadStatus +from flags2_sequential import download_one # <1> + +DEFAULT_CONCUR_REQ = 30 # <2> +MAX_CONCUR_REQ = 1000 # <3> + + +def download_many(cc_list: list[str], + base_url: str, + verbose: bool, + concur_req: int) -> Counter[DownloadStatus]: + counter: Counter[DownloadStatus] = Counter() + with ThreadPoolExecutor(max_workers=concur_req) as executor: # <4> + to_do_map = {} # <5> + for cc in sorted(cc_list): # <6> + future = executor.submit(download_one, cc, + base_url, verbose) # <7> + to_do_map[future] = cc # <8> + done_iter = as_completed(to_do_map) # <9> + if not verbose: + done_iter = tqdm.tqdm(done_iter, total=len(cc_list)) # <10> + for future in done_iter: # <11> + try: + status = future.result() # <12> + except httpx.HTTPStatusError as exc: # <13> + error_msg = 'HTTP error {resp.status_code} - {resp.reason_phrase}' + error_msg = error_msg.format(resp=exc.response) + except httpx.RequestError as exc: + error_msg = f'{exc} {type(exc)}'.strip() + except KeyboardInterrupt: + break + else: + error_msg = '' + + if error_msg: + status = DownloadStatus.ERROR + counter[status] += 1 + if verbose and error_msg: + cc = to_do_map[future] # <14> + print(f'{cc} error: {error_msg}') + + return counter + + +if __name__ == '__main__': + main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ) +# end::FLAGS2_THREADPOOL[] diff --git a/20-executors/getflags/flags3_asyncio.py b/20-executors/getflags/flags3_asyncio.py new file mode 100755 index 0000000..9554dd2 --- /dev/null +++ b/20-executors/getflags/flags3_asyncio.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +"""Download flags of countries (with error handling). + +asyncio async/await version + +""" +# tag::FLAGS2_ASYNCIO_TOP[] +import asyncio +from collections import Counter +from http import HTTPStatus +from pathlib import Path + +import httpx +import tqdm # type: ignore + +from flags2_common import main, DownloadStatus, save_flag + +# low concurrency default to avoid errors from remote site, +# such as 503 - Service Temporarily Unavailable +DEFAULT_CONCUR_REQ = 5 +MAX_CONCUR_REQ = 1000 + +async def get_flag(client: httpx.AsyncClient, # <1> + base_url: str, + cc: str) -> bytes: + url = f'{base_url}/{cc}/{cc}.gif'.lower() + resp = await client.get(url, timeout=3.1, follow_redirects=True) # <2> + resp.raise_for_status() + return resp.content + +# tag::FLAGS3_ASYNCIO_GET_COUNTRY[] +async def get_country(client: httpx.AsyncClient, + base_url: str, + cc: str) -> str: # <1> + url = f'{base_url}/{cc}/metadata.json'.lower() + resp = await client.get(url, timeout=3.1, follow_redirects=True) + resp.raise_for_status() + metadata = resp.json() # <2> + return metadata['country'] # <3> +# end::FLAGS3_ASYNCIO_GET_COUNTRY[] + +# tag::FLAGS3_ASYNCIO_DOWNLOAD_ONE[] +async def download_one(client: httpx.AsyncClient, + cc: str, + base_url: str, + semaphore: asyncio.Semaphore, + verbose: bool) -> DownloadStatus: + try: + async with semaphore: # <1> + image = await get_flag(client, base_url, cc) + async with semaphore: # <2> + country = await get_country(client, base_url, cc) + except httpx.HTTPStatusError as exc: + res = exc.response + if res.status_code == HTTPStatus.NOT_FOUND: + status = DownloadStatus.NOT_FOUND + msg = f'not found: {res.url}' + else: + raise + else: + filename = country.replace(' ', '_') # <3> + await asyncio.to_thread(save_flag, image, f'{filename}.gif') + status = DownloadStatus.OK + msg = 'OK' + if verbose and msg: + print(cc, msg) + return status +# end::FLAGS3_ASYNCIO_DOWNLOAD_ONE[] + +# tag::FLAGS2_ASYNCIO_START[] +async def supervisor(cc_list: list[str], + base_url: str, + verbose: bool, + concur_req: int) -> Counter[DownloadStatus]: # <1> + counter: Counter[DownloadStatus] = Counter() + semaphore = asyncio.Semaphore(concur_req) # <2> + async with httpx.AsyncClient() as client: + to_do = [download_one(client, cc, base_url, semaphore, verbose) + for cc in sorted(cc_list)] # <3> + to_do_iter = asyncio.as_completed(to_do) # <4> + if not verbose: + to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) # <5> + error: httpx.HTTPError | None = None # <6> + for coro in to_do_iter: # <7> + try: + status = await coro # <8> + except httpx.HTTPStatusError as exc: + error_msg = 'HTTP error {resp.status_code} - {resp.reason_phrase}' + error_msg = error_msg.format(resp=exc.response) + error = exc # <9> + except httpx.RequestError as exc: + error_msg = f'{exc} {type(exc)}'.strip() + error = exc # <10> + except KeyboardInterrupt: + break + + if error: + status = DownloadStatus.ERROR # <11> + if verbose: + url = str(error.request.url) # <12> + cc = Path(url).stem.upper() # <13> + print(f'{cc} error: {error_msg}') + counter[status] += 1 + + return counter + +def download_many(cc_list: list[str], + base_url: str, + verbose: bool, + concur_req: int) -> Counter[DownloadStatus]: + coro = supervisor(cc_list, base_url, verbose, concur_req) + counts = asyncio.run(coro) # <14> + + return counts + +if __name__ == '__main__': + main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ) +# end::FLAGS2_ASYNCIO_START[] diff --git a/20-executors/getflags/flags_asyncio.py b/20-executors/getflags/flags_asyncio.py new file mode 100755 index 0000000..588d166 --- /dev/null +++ b/20-executors/getflags/flags_asyncio.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +"""Download flags of top 20 countries by population + +asyncio + aiottp version + +Sample run:: + + $ python3 flags_asyncio.py + EG VN IN TR RU ID US DE CN MX JP BD NG ET FR BR PH PK CD IR + 20 flags downloaded in 1.07s +""" +# tag::FLAGS_ASYNCIO_TOP[] +import asyncio + +from httpx import AsyncClient # <1> + +from flags import BASE_URL, save_flag, main # <2> + +async def download_one(client: AsyncClient, cc: str): # <3> + image = await get_flag(client, cc) + save_flag(image, f'{cc}.gif') + print(cc, end=' ', flush=True) + return cc + +async def get_flag(client: AsyncClient, cc: str) -> bytes: # <4> + url = f'{BASE_URL}/{cc}/{cc}.gif'.lower() + resp = await client.get(url, timeout=6.1, + follow_redirects=True) # <5> + return resp.read() # <6> +# end::FLAGS_ASYNCIO_TOP[] + +# tag::FLAGS_ASYNCIO_START[] +def download_many(cc_list: list[str]) -> int: # <1> + return asyncio.run(supervisor(cc_list)) # <2> + +async def supervisor(cc_list: list[str]) -> int: + async with AsyncClient() as client: # <3> + to_do = [download_one(client, cc) + for cc in sorted(cc_list)] # <4> + res = await asyncio.gather(*to_do) # <5> + + return len(res) # <6> + +if __name__ == '__main__': + main(download_many) +# end::FLAGS_ASYNCIO_START[] diff --git a/21-futures/getflags/flags_threadpool.py b/20-executors/getflags/flags_threadpool.py similarity index 100% rename from 21-futures/getflags/flags_threadpool.py rename to 20-executors/getflags/flags_threadpool.py diff --git a/21-futures/getflags/flags_threadpool_futures.py b/20-executors/getflags/flags_threadpool_futures.py similarity index 100% rename from 21-futures/getflags/flags_threadpool_futures.py rename to 20-executors/getflags/flags_threadpool_futures.py diff --git a/20-executors/getflags/httpx-error-tree/drawtree.py b/20-executors/getflags/httpx-error-tree/drawtree.py new file mode 100755 index 0000000..f2d9fea --- /dev/null +++ b/20-executors/getflags/httpx-error-tree/drawtree.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +from tree import tree + + +SP = '\N{SPACE}' +HLIN = '\N{BOX DRAWINGS LIGHT HORIZONTAL}' # ─ +ELBOW = f'\N{BOX DRAWINGS LIGHT UP AND RIGHT}{HLIN*2}{SP}' # └── +TEE = f'\N{BOX DRAWINGS LIGHT VERTICAL AND RIGHT}{HLIN*2}{SP}' # ├── +PIPE = f'\N{BOX DRAWINGS LIGHT VERTICAL}{SP*3}' # │ + + +def cls_name(cls): + module = 'builtins.' if cls.__module__ == 'builtins' else '' + return module + cls.__name__ + +def render_lines(tree_iter): + cls, _, _ = next(tree_iter) + yield cls_name(cls) + prefix = '' + + for cls, level, last in tree_iter: + prefix = prefix[:4 * (level-1)] + prefix = prefix.replace(TEE, PIPE).replace(ELBOW, SP*4) + prefix += ELBOW if last else TEE + yield prefix + cls_name(cls) + + +def draw(cls): + for line in render_lines(tree(cls)): + print(line) + + +if __name__ == '__main__': + draw(Exception) diff --git a/20-executors/getflags/httpx-error-tree/tree.py b/20-executors/getflags/httpx-error-tree/tree.py new file mode 100755 index 0000000..950bbfe --- /dev/null +++ b/20-executors/getflags/httpx-error-tree/tree.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +import httpx # make httpx classes available to .__subclasses__() + + +def tree(cls, level=0, last_sibling=True): + yield cls, level, last_sibling + + # get RuntimeError and exceptions defined in httpx + subclasses = [sub for sub in cls.__subclasses__() + if sub is RuntimeError or sub.__module__ == 'httpx'] + if subclasses: + last = subclasses[-1] + for sub in subclasses: + yield from tree(sub, level+1, sub is last) + + +def display(cls): + for cls, level, _ in tree(cls): + indent = ' ' * 4 * level + module = 'builtins.' if cls.__module__ == 'builtins' else '' + print(f'{indent}{module}{cls.__name__}') + + +if __name__ == '__main__': + display(Exception) diff --git a/20-executors/getflags/requirements.txt b/20-executors/getflags/requirements.txt new file mode 100644 index 0000000..9b1faeb --- /dev/null +++ b/20-executors/getflags/requirements.txt @@ -0,0 +1,10 @@ +anyio==3.3.2 +certifi==2024.7.4 +charset-normalizer==2.0.6 +h11==0.16.0 +httpcore==0.13.7 +httpx==1.0.0b0 +idna==3.7 +rfc3986==1.5.0 +sniffio==1.2.0 +tqdm==4.66.3 diff --git a/21-futures/getflags/slow_server.py b/20-executors/getflags/slow_server.py similarity index 81% rename from 21-futures/getflags/slow_server.py rename to 20-executors/getflags/slow_server.py index 9d8142b..90e6672 100755 --- a/21-futures/getflags/slow_server.py +++ b/20-executors/getflags/slow_server.py @@ -15,8 +15,10 @@ from functools import partial from http import server, HTTPStatus from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler -from random import random +from random import random, uniform +MIN_DELAY = 0.5 # minimum delay for do_GET (seconds) +MAX_DELAY = 5.0 # maximum delay for do_GET (seconds) class SlowHTTPRequestHandler(SimpleHTTPRequestHandler): """SlowHTTPRequestHandler adds delays and errors to test HTTP clients. @@ -36,14 +38,23 @@ def __init__(self, *args, error_rate=0.0, **kwargs): def do_GET(self): """Serve a GET request.""" - time.sleep(.5) + delay = uniform(MIN_DELAY, MAX_DELAY) + cc = self.path[-6:-4].upper() + print(f'{cc} delay: {delay:0.2}s') + time.sleep(delay) if random() < self.error_rate: - self.send_error(HTTPStatus.IM_A_TEAPOT, "I'm a Teapot") + # HTTPStatus.IM_A_TEAPOT requires Python >= 3.9 + try: + self.send_error(HTTPStatus.IM_A_TEAPOT, "I'm a Teapot") + except BrokenPipeError as exc: + print(f'{cc} *** BrokenPipeError: client closed') else: f = self.send_head() if f: try: self.copyfile(f, self.wfile) + except BrokenPipeError as exc: + print(f'{cc} *** BrokenPipeError: client closed') finally: f.close() @@ -66,9 +77,9 @@ def do_GET(self): help='Error rate; e.g. use .25 for 25%% probability ' '[default:0.0]') parser.add_argument('port', action='store', - default=8000, type=int, + default=8001, type=int, nargs='?', - help='Specify alternate port [default: 8000]') + help='Specify alternate port [default: 8001]') args = parser.parse_args() handler_class = partial(SlowHTTPRequestHandler, directory=args.directory, diff --git a/21-futures/primes/primes.py b/20-executors/primes/primes.py similarity index 100% rename from 21-futures/primes/primes.py rename to 20-executors/primes/primes.py diff --git a/21-futures/primes/proc_pool.py b/20-executors/primes/proc_pool.py similarity index 100% rename from 21-futures/primes/proc_pool.py rename to 20-executors/primes/proc_pool.py diff --git a/22-async/README.rst b/21-async/README.rst similarity index 100% rename from 22-async/README.rst rename to 21-async/README.rst diff --git a/22-async/domains/README.rst b/21-async/domains/README.rst similarity index 100% rename from 22-async/domains/README.rst rename to 21-async/domains/README.rst diff --git a/22-async/domains/asyncio/blogdom.py b/21-async/domains/asyncio/blogdom.py similarity index 100% rename from 22-async/domains/asyncio/blogdom.py rename to 21-async/domains/asyncio/blogdom.py diff --git a/22-async/domains/asyncio/domaincheck.py b/21-async/domains/asyncio/domaincheck.py similarity index 100% rename from 22-async/domains/asyncio/domaincheck.py rename to 21-async/domains/asyncio/domaincheck.py diff --git a/22-async/domains/asyncio/domainlib.py b/21-async/domains/asyncio/domainlib.py similarity index 100% rename from 22-async/domains/asyncio/domainlib.py rename to 21-async/domains/asyncio/domainlib.py diff --git a/22-async/domains/curio/blogdom.py b/21-async/domains/curio/blogdom.py similarity index 100% rename from 22-async/domains/curio/blogdom.py rename to 21-async/domains/curio/blogdom.py diff --git a/22-async/domains/curio/domaincheck.py b/21-async/domains/curio/domaincheck.py similarity index 100% rename from 22-async/domains/curio/domaincheck.py rename to 21-async/domains/curio/domaincheck.py diff --git a/22-async/domains/curio/domainlib.py b/21-async/domains/curio/domainlib.py similarity index 100% rename from 22-async/domains/curio/domainlib.py rename to 21-async/domains/curio/domainlib.py diff --git a/22-async/domains/curio/requirements.txt b/21-async/domains/curio/requirements.txt similarity index 100% rename from 22-async/domains/curio/requirements.txt rename to 21-async/domains/curio/requirements.txt diff --git a/22-async/mojifinder/README.md b/21-async/mojifinder/README.md similarity index 100% rename from 22-async/mojifinder/README.md rename to 21-async/mojifinder/README.md diff --git a/22-async/mojifinder/bottle.py b/21-async/mojifinder/bottle.py similarity index 100% rename from 22-async/mojifinder/bottle.py rename to 21-async/mojifinder/bottle.py diff --git a/22-async/mojifinder/charindex.py b/21-async/mojifinder/charindex.py similarity index 100% rename from 22-async/mojifinder/charindex.py rename to 21-async/mojifinder/charindex.py diff --git a/22-async/mojifinder/requirements.txt b/21-async/mojifinder/requirements.txt similarity index 60% rename from 22-async/mojifinder/requirements.txt rename to 21-async/mojifinder/requirements.txt index 1f7c3d6..6831fee 100644 --- a/22-async/mojifinder/requirements.txt +++ b/21-async/mojifinder/requirements.txt @@ -1,7 +1,7 @@ click==7.1.2 fastapi==0.65.2 -h11==0.12.0 -pydantic==1.8.2 -starlette==0.13.6 +h11==0.16.0 +pydantic==1.10.13 +starlette==0.40.0 typing-extensions==3.7.4.3 uvicorn==0.13.4 diff --git a/22-async/mojifinder/static/form.html b/21-async/mojifinder/static/form.html similarity index 100% rename from 22-async/mojifinder/static/form.html rename to 21-async/mojifinder/static/form.html diff --git a/22-async/mojifinder/tcp_mojifinder.py b/21-async/mojifinder/tcp_mojifinder.py similarity index 79% rename from 22-async/mojifinder/tcp_mojifinder.py rename to 21-async/mojifinder/tcp_mojifinder.py index 8ff4e50..4da7188 100755 --- a/22-async/mojifinder/tcp_mojifinder.py +++ b/21-async/mojifinder/tcp_mojifinder.py @@ -14,26 +14,28 @@ async def finder(index: InvertedIndex, # <2> reader: asyncio.StreamReader, - writer: asyncio.StreamWriter): + writer: asyncio.StreamWriter) -> None: client = writer.get_extra_info('peername') # <3> while True: # <4> writer.write(PROMPT) # can't await! # <5> await writer.drain() # must await! # <6> data = await reader.readline() # <7> + if not data: # <8> + break try: - query = data.decode().strip() # <8> - except UnicodeDecodeError: # <9> + query = data.decode().strip() # <9> + except UnicodeDecodeError: # <10> query = '\x00' - print(f' From {client}: {query!r}') # <10> + print(f' From {client}: {query!r}') # <11> if query: - if ord(query[:1]) < 32: # <11> + if ord(query[:1]) < 32: # <12> break - results = await search(query, index, writer) # <12> - print(f' To {client}: {results} results.') # <13> + results = await search(query, index, writer) # <13> + print(f' To {client}: {results} results.') # <14> - writer.close() # <14> - await writer.wait_closed() # <15> - print(f'Close {client}.') # <16> + writer.close() # <15> + await writer.wait_closed() # <16> + print(f'Close {client}.') # <17> # end::TCP_MOJIFINDER_TOP[] # tag::TCP_MOJIFINDER_SEARCH[] @@ -52,7 +54,7 @@ async def search(query: str, # <1> # end::TCP_MOJIFINDER_SEARCH[] # tag::TCP_MOJIFINDER_MAIN[] -async def supervisor(index: InvertedIndex, host: str, port: int): +async def supervisor(index: InvertedIndex, host: str, port: int) -> None: server = await asyncio.start_server( # <1> functools.partial(finder, index), # <2> host, port) # <3> diff --git a/22-async/mojifinder/web_mojifinder.py b/21-async/mojifinder/web_mojifinder.py similarity index 72% rename from 22-async/mojifinder/web_mojifinder.py rename to 21-async/mojifinder/web_mojifinder.py index 7e0ff96..5f0cf5c 100644 --- a/22-async/mojifinder/web_mojifinder.py +++ b/21-async/mojifinder/web_mojifinder.py @@ -7,25 +7,26 @@ from charindex import InvertedIndex -app = FastAPI( # <1> +STATIC_PATH = Path(__file__).parent.absolute() / 'static' # <1> + +app = FastAPI( # <2> title='Mojifinder Web', description='Search for Unicode characters by name.', ) -class CharName(BaseModel): # <2> +class CharName(BaseModel): # <3> char: str name: str -def init(app): # <3> +def init(app): # <4> app.state.index = InvertedIndex() - static = Path(__file__).parent.absolute() / 'static' # <4> - app.state.form = (static / 'form.html').read_text() + app.state.form = (STATIC_PATH / 'form.html').read_text() init(app) # <5> @app.get('/search', response_model=list[CharName]) # <6> async def search(q: str): # <7> - chars = app.state.index.search(q) + chars = sorted(app.state.index.search(q)) return ({'char': c, 'name': name(c)} for c in chars) # <8> @app.get('/', response_class=HTMLResponse, include_in_schema=False) diff --git a/22-async/mojifinder/web_mojifinder_bottle.py b/21-async/mojifinder/web_mojifinder_bottle.py similarity index 100% rename from 22-async/mojifinder/web_mojifinder_bottle.py rename to 21-async/mojifinder/web_mojifinder_bottle.py diff --git a/21-futures/getflags/.gitignore b/21-futures/getflags/.gitignore deleted file mode 100644 index 8484300..0000000 --- a/21-futures/getflags/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flags/ \ No newline at end of file diff --git a/21-futures/getflags/downloaded/.gitignore b/21-futures/getflags/downloaded/.gitignore deleted file mode 100644 index d8c78e1..0000000 --- a/21-futures/getflags/downloaded/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.gif \ No newline at end of file diff --git a/21-futures/getflags/flags2_asyncio.py b/21-futures/getflags/flags2_asyncio.py deleted file mode 100755 index e7a73ac..0000000 --- a/21-futures/getflags/flags2_asyncio.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 - -"""Download flags of countries (with error handling). - -asyncio async/await version - -""" -# tag::FLAGS2_ASYNCIO_TOP[] -import asyncio -from collections import Counter - -import aiohttp -import tqdm # type: ignore - -from flags2_common import main, HTTPStatus, Result, save_flag - -# default set low to avoid errors from remote site, such as -# 503 - Service Temporarily Unavailable -DEFAULT_CONCUR_REQ = 5 -MAX_CONCUR_REQ = 1000 - - -class FetchError(Exception): # <1> - def __init__(self, country_code: str): - self.country_code = country_code - - -async def get_flag(session: aiohttp.ClientSession, # <2> - base_url: str, - cc: str) -> bytes: - url = f'{base_url}/{cc}/{cc}.gif'.lower() - async with session.get(url) as resp: - if resp.status == 200: - return await resp.read() - else: - resp.raise_for_status() # <3> - return bytes() - -async def download_one(session: aiohttp.ClientSession, # <4> - cc: str, - base_url: str, - semaphore: asyncio.Semaphore, - verbose: bool) -> Result: - try: - async with semaphore: # <5> - image = await get_flag(session, base_url, cc) - except aiohttp.ClientResponseError as exc: - if exc.status == 404: # <6> - status = HTTPStatus.not_found - msg = 'not found' - else: - raise FetchError(cc) from exc # <7> - else: - save_flag(image, f'{cc}.gif') - status = HTTPStatus.ok - msg = 'OK' - if verbose and msg: - print(cc, msg) - return Result(status, cc) -# end::FLAGS2_ASYNCIO_TOP[] - -# tag::FLAGS2_ASYNCIO_START[] -async def supervisor(cc_list: list[str], - base_url: str, - verbose: bool, - concur_req: int) -> Counter[HTTPStatus]: # <1> - counter: Counter[HTTPStatus] = Counter() - semaphore = asyncio.Semaphore(concur_req) # <2> - async with aiohttp.ClientSession() as session: - to_do = [download_one(session, cc, base_url, semaphore, verbose) - for cc in sorted(cc_list)] # <3> - to_do_iter = asyncio.as_completed(to_do) # <4> - if not verbose: - to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) # <5> - for coro in to_do_iter: # <6> - try: - res = await coro # <7> - except FetchError as exc: # <8> - country_code = exc.country_code # <9> - try: - error_msg = exc.__cause__.message # type: ignore # <10> - except AttributeError: - error_msg = 'Unknown cause' # <11> - if verbose and error_msg: - print(f'*** Error for {country_code}: {error_msg}') - status = HTTPStatus.error - else: - status = res.status - counter[status] += 1 # <12> - return counter # <13> - -def download_many(cc_list: list[str], - base_url: str, - verbose: bool, - concur_req: int) -> Counter[HTTPStatus]: - coro = supervisor(cc_list, base_url, verbose, concur_req) - counts = asyncio.run(coro) # <14> - - return counts - -if __name__ == '__main__': - main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ) -# end::FLAGS2_ASYNCIO_START[] diff --git a/21-futures/getflags/flags2_asyncio_executor.py b/21-futures/getflags/flags2_asyncio_executor.py deleted file mode 100755 index 5b75d59..0000000 --- a/21-futures/getflags/flags2_asyncio_executor.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 - -"""Download flags of countries (with error handling). - -asyncio async/await version using run_in_executor for save_flag. - -""" - -import asyncio -from collections import Counter - -import aiohttp -import tqdm # type: ignore - -from flags2_common import main, HTTPStatus, Result, save_flag - -# default set low to avoid errors from remote site, such as -# 503 - Service Temporarily Unavailable -DEFAULT_CONCUR_REQ = 5 -MAX_CONCUR_REQ = 1000 - - -class FetchError(Exception): - def __init__(self, country_code: str): - self.country_code = country_code - - -async def get_flag(session: aiohttp.ClientSession, - base_url: str, - cc: str) -> bytes: - url = f'{base_url}/{cc}/{cc}.gif'.lower() - async with session.get(url) as resp: - if resp.status == 200: - return await resp.read() - else: - resp.raise_for_status() - return bytes() - -# tag::FLAGS2_ASYNCIO_EXECUTOR[] -async def download_one(session: aiohttp.ClientSession, - cc: str, - base_url: str, - semaphore: asyncio.Semaphore, - verbose: bool) -> Result: - try: - async with semaphore: - image = await get_flag(session, base_url, cc) - except aiohttp.ClientResponseError as exc: - if exc.status == 404: - status = HTTPStatus.not_found - msg = 'not found' - else: - raise FetchError(cc) from exc - else: - loop = asyncio.get_running_loop() # <1> - loop.run_in_executor(None, # <2> - save_flag, image, f'{cc}.gif') # <3> - status = HTTPStatus.ok - msg = 'OK' - if verbose and msg: - print(cc, msg) - return Result(status, cc) -# end::FLAGS2_ASYNCIO_EXECUTOR[] - -async def supervisor(cc_list: list[str], - base_url: str, - verbose: bool, - concur_req: int) -> Counter[HTTPStatus]: - counter: Counter[HTTPStatus] = Counter() - semaphore = asyncio.Semaphore(concur_req) - async with aiohttp.ClientSession() as session: - to_do = [download_one(session, cc, base_url, semaphore, verbose) - for cc in sorted(cc_list)] - - to_do_iter = asyncio.as_completed(to_do) - if not verbose: - to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) - for coro in to_do_iter: - try: - res = await coro - except FetchError as exc: - country_code = exc.country_code - try: - error_msg = exc.__cause__.message # type: ignore - except AttributeError: - error_msg = 'Unknown cause' - if verbose and error_msg: - print(f'*** Error for {country_code}: {error_msg}') - status = HTTPStatus.error - else: - status = res.status - - counter[status] += 1 - - return counter - - -def download_many(cc_list: list[str], - base_url: str, - verbose: bool, - concur_req: int) -> Counter[HTTPStatus]: - coro = supervisor(cc_list, base_url, verbose, concur_req) - counts = asyncio.run(coro) # <14> - - return counts - - -if __name__ == '__main__': - main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ) diff --git a/21-futures/getflags/flags2_threadpool.py b/21-futures/getflags/flags2_threadpool.py deleted file mode 100755 index bbe71cb..0000000 --- a/21-futures/getflags/flags2_threadpool.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 - -"""Download flags of countries (with error handling). - -ThreadPool version - -Sample run:: - - $ python3 flags2_threadpool.py -s ERROR -e - ERROR site: http://localhost:8003/flags - Searching for 676 flags: from AA to ZZ - 30 concurrent connections will be used. - -------------------- - 150 flags downloaded. - 361 not found. - 165 errors. - Elapsed time: 7.46s - -""" - -# tag::FLAGS2_THREADPOOL[] -from collections import Counter -from concurrent import futures - -import requests -import tqdm # type: ignore # <1> - -from flags2_common import main, HTTPStatus # <2> -from flags2_sequential import download_one # <3> - -DEFAULT_CONCUR_REQ = 30 # <4> -MAX_CONCUR_REQ = 1000 # <5> - - -def download_many(cc_list: list[str], - base_url: str, - verbose: bool, - concur_req: int) -> Counter[int]: - counter: Counter[int] = Counter() - with futures.ThreadPoolExecutor(max_workers=concur_req) as executor: # <6> - to_do_map = {} # <7> - for cc in sorted(cc_list): # <8> - future = executor.submit(download_one, cc, - base_url, verbose) # <9> - to_do_map[future] = cc # <10> - done_iter = futures.as_completed(to_do_map) # <11> - if not verbose: - done_iter = tqdm.tqdm(done_iter, total=len(cc_list)) # <12> - for future in done_iter: # <13> - try: - res = future.result() # <14> - except requests.exceptions.HTTPError as exc: # <15> - error_fmt = 'HTTP {res.status_code} - {res.reason}' - error_msg = error_fmt.format(res=exc.response) - except requests.exceptions.ConnectionError: - error_msg = 'Connection error' - else: - error_msg = '' - status = res.status - - if error_msg: - status = HTTPStatus.error - counter[status] += 1 - if verbose and error_msg: - cc = to_do_map[future] # <16> - print(f'*** Error for {cc}: {error_msg}') - - return counter - - -if __name__ == '__main__': - main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ) -# end::FLAGS2_THREADPOOL[] diff --git a/21-futures/getflags/flags3_asyncio.py b/21-futures/getflags/flags3_asyncio.py deleted file mode 100755 index 37b5957..0000000 --- a/21-futures/getflags/flags3_asyncio.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 - -"""Download flags of countries (with error handling). - -asyncio async/await version using run_in_executor for save_flag. - -""" - -import asyncio -from collections import Counter - -import aiohttp -import tqdm # type: ignore - -from flags2_common import main, HTTPStatus, Result, save_flag - -# default set low to avoid errors from remote site, such as -# 503 - Service Temporarily Unavailable -DEFAULT_CONCUR_REQ = 5 -MAX_CONCUR_REQ = 1000 - - -class FetchError(Exception): - def __init__(self, country_code: str): - self.country_code = country_code - -async def get_flag(session: aiohttp.ClientSession, - base_url: str, - cc: str) -> bytes: - url = f'{base_url}/{cc}/{cc}.gif' - async with session.get(url) as resp: - if resp.status == 200: - return await resp.read() - else: - resp.raise_for_status() - return bytes() - -# tag::FLAGS3_ASYNCIO_GET_COUNTRY[] -async def get_country(session: aiohttp.ClientSession, # <1> - base_url: str, - cc: str) -> str: - url = f'{base_url}/{cc}/metadata.json' - async with session.get(url) as resp: - if resp.status == 200: - metadata = await resp.json() # <2> - return metadata.get('country', 'no name') # <3> - else: - resp.raise_for_status() - return '' -# end::FLAGS3_ASYNCIO_GET_COUNTRY[] - -# tag::FLAGS3_ASYNCIO_DOWNLOAD_ONE[] -async def download_one(session: aiohttp.ClientSession, - cc: str, - base_url: str, - semaphore: asyncio.Semaphore, - verbose: bool) -> Result: - try: - async with semaphore: - image = await get_flag(session, base_url, cc) # <1> - async with semaphore: - country = await get_country(session, base_url, cc) # <2> - except aiohttp.ClientResponseError as exc: - if exc.status == 404: - status = HTTPStatus.not_found - msg = 'not found' - else: - raise FetchError(cc) from exc - else: - filename = country.replace(' ', '_') # <3> - filename = f'{filename}.gif' - loop = asyncio.get_running_loop() - loop.run_in_executor(None, - save_flag, image, filename) - status = HTTPStatus.ok - msg = 'OK' - if verbose and msg: - print(cc, msg) - return Result(status, cc) -# end::FLAGS3_ASYNCIO_DOWNLOAD_ONE[] - -async def supervisor(cc_list: list[str], - base_url: str, - verbose: bool, - concur_req: int) -> Counter[HTTPStatus]: - counter: Counter[HTTPStatus] = Counter() - semaphore = asyncio.Semaphore(concur_req) - async with aiohttp.ClientSession() as session: - to_do = [download_one(session, cc, base_url, - semaphore, verbose) - for cc in sorted(cc_list)] - - to_do_iter = asyncio.as_completed(to_do) - if not verbose: - to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) - for coro in to_do_iter: - try: - res = await coro - except FetchError as exc: - country_code = exc.country_code - try: - error_msg = exc.__cause__.message # type: ignore - except AttributeError: - error_msg = 'Unknown cause' - if verbose and error_msg: - print(f'*** Error for {country_code}: {error_msg}') - status = HTTPStatus.error - else: - status = res.status - - counter[status] += 1 - - return counter - - -def download_many(cc_list: list[str], - base_url: str, - verbose: bool, - concur_req: int) -> Counter[HTTPStatus]: - coro = supervisor(cc_list, base_url, verbose, concur_req) - counts = asyncio.run(coro) # <14> - - return counts - - -if __name__ == '__main__': - main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ) diff --git a/21-futures/getflags/flags_asyncio.py b/21-futures/getflags/flags_asyncio.py deleted file mode 100755 index ee431d0..0000000 --- a/21-futures/getflags/flags_asyncio.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 - -"""Download flags of top 20 countries by population - -asyncio + aiottp version - -Sample run:: - - $ python3 flags_asyncio.py - EG VN IN TR RU ID US DE CN MX JP BD NG ET FR BR PH PK CD IR - 20 flags downloaded in 1.07s - -""" -# tag::FLAGS_ASYNCIO_TOP[] -import asyncio - -from aiohttp import ClientSession # <1> - -from flags import BASE_URL, save_flag, main # <2> - -async def download_one(session: ClientSession, cc: str): # <3> - image = await get_flag(session, cc) - save_flag(image, f'{cc}.gif') - print(cc, end=' ', flush=True) - return cc - -async def get_flag(session: ClientSession, cc: str) -> bytes: # <4> - url = f'{BASE_URL}/{cc}/{cc}.gif'.lower() - async with session.get(url) as resp: # <5> - return await resp.read() # <6> -# end::FLAGS_ASYNCIO_TOP[] - -# tag::FLAGS_ASYNCIO_START[] -def download_many(cc_list: list[str]) -> int: # <1> - return asyncio.run(supervisor(cc_list)) # <2> - -async def supervisor(cc_list: list[str]) -> int: - async with ClientSession() as session: # <3> - to_do = [download_one(session, cc) # <4> - for cc in sorted(cc_list)] - res = await asyncio.gather(*to_do) # <5> - - return len(res) # <6> - -if __name__ == '__main__': - main(download_many) -# end::FLAGS_ASYNCIO_START[] diff --git a/21-futures/getflags/requirements.txt b/21-futures/getflags/requirements.txt deleted file mode 100644 index 793679b..0000000 --- a/21-futures/getflags/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -aiohttp==3.7.4 -async-timeout==3.0.1 -attrs==20.3.0 -certifi==2020.12.5 -chardet==4.0.0 -idna==2.10 -requests==2.25.1 -urllib3==1.26.5 -tqdm==4.56.2 -multidict==5.1.0 -yarl==1.6.3 diff --git a/23-dyn-attr-prop/README.rst b/22-dyn-attr-prop/README.rst similarity index 100% rename from 23-dyn-attr-prop/README.rst rename to 22-dyn-attr-prop/README.rst diff --git a/23-dyn-attr-prop/blackknight.py b/22-dyn-attr-prop/blackknight.py similarity index 100% rename from 23-dyn-attr-prop/blackknight.py rename to 22-dyn-attr-prop/blackknight.py diff --git a/23-dyn-attr-prop/bulkfood/bulkfood_v1.py b/22-dyn-attr-prop/bulkfood/bulkfood_v1.py similarity index 100% rename from 23-dyn-attr-prop/bulkfood/bulkfood_v1.py rename to 22-dyn-attr-prop/bulkfood/bulkfood_v1.py diff --git a/23-dyn-attr-prop/bulkfood/bulkfood_v2.py b/22-dyn-attr-prop/bulkfood/bulkfood_v2.py similarity index 100% rename from 23-dyn-attr-prop/bulkfood/bulkfood_v2.py rename to 22-dyn-attr-prop/bulkfood/bulkfood_v2.py diff --git a/23-dyn-attr-prop/bulkfood/bulkfood_v2b.py b/22-dyn-attr-prop/bulkfood/bulkfood_v2b.py similarity index 100% rename from 23-dyn-attr-prop/bulkfood/bulkfood_v2b.py rename to 22-dyn-attr-prop/bulkfood/bulkfood_v2b.py diff --git a/23-dyn-attr-prop/bulkfood/bulkfood_v2prop.py b/22-dyn-attr-prop/bulkfood/bulkfood_v2prop.py similarity index 93% rename from 23-dyn-attr-prop/bulkfood/bulkfood_v2prop.py rename to 22-dyn-attr-prop/bulkfood/bulkfood_v2prop.py index e34a7c3..b2cffab 100644 --- a/23-dyn-attr-prop/bulkfood/bulkfood_v2prop.py +++ b/22-dyn-attr-prop/bulkfood/bulkfood_v2prop.py @@ -30,8 +30,8 @@ >>> nutmeg = LineItem('Moluccan nutmeg', 8, 13.95) >>> nutmeg.weight, nutmeg.price # <1> (8, 13.95) - >>> sorted(vars(nutmeg).items()) # <2> - [('description', 'Moluccan nutmeg'), ('price', 13.95), ('weight', 8)] + >>> nutmeg.__dict__ # <2> + {'description': 'Moluccan nutmeg', 'weight': 8, 'price': 13.95} # end::LINEITEM_V2_PROP_DEMO[] diff --git a/23-dyn-attr-prop/doc_property.py b/22-dyn-attr-prop/doc_property.py similarity index 91% rename from 23-dyn-attr-prop/doc_property.py rename to 22-dyn-attr-prop/doc_property.py index b1ba351..5b653ef 100644 --- a/23-dyn-attr-prop/doc_property.py +++ b/22-dyn-attr-prop/doc_property.py @@ -14,7 +14,7 @@ class Foo: @property def bar(self): - '''The bar attribute''' + """The bar attribute""" return self.__dict__['bar'] @bar.setter diff --git a/23-dyn-attr-prop/oscon/data/osconfeed.json b/22-dyn-attr-prop/oscon/data/osconfeed.json similarity index 99% rename from 23-dyn-attr-prop/oscon/data/osconfeed.json rename to 22-dyn-attr-prop/oscon/data/osconfeed.json index 50ed313..7bea08d 100644 --- a/23-dyn-attr-prop/oscon/data/osconfeed.json +++ b/22-dyn-attr-prop/oscon/data/osconfeed.json @@ -1243,7 +1243,7 @@ "time_start": "2014-07-21 13:30:00", "time_stop": "2014-07-21 17:00:00", "venue_serial": 1451, - "description": "Metaprograming in Python is fun and profitable thanks to its rich Data Model \u2013 APIs that let you handle functions, modules and even classes as objects that you can create, inspect and modify at runtime. The Data Model also enables your own objects to support infix operators, become iterable and emulate collections. This workshop shows how, through a diverse selection of examples and exercises.", + "description": "Metaprogramming in Python is fun and profitable thanks to its rich Data Model \u2013 APIs that let you handle functions, modules and even classes as objects that you can create, inspect and modify at runtime. The Data Model also enables your own objects to support infix operators, become iterable and emulate collections. This workshop shows how, through a diverse selection of examples and exercises.", "website_url": "http://oscon.com/oscon2014/public/schedule/detail/34107", "speakers": [150170], "categories": [ diff --git a/23-dyn-attr-prop/oscon/explore0.py b/22-dyn-attr-prop/oscon/explore0.py similarity index 87% rename from 23-dyn-attr-prop/oscon/explore0.py rename to 22-dyn-attr-prop/oscon/explore0.py index 1ea7843..c881d16 100644 --- a/23-dyn-attr-prop/oscon/explore0.py +++ b/22-dyn-attr-prop/oscon/explore0.py @@ -54,12 +54,15 @@ def __getattr__(self, name): # <2> except AttributeError: return FrozenJSON.build(self.__data[name]) # <4> + def __dir__(self): # <5> + return self.__data.keys() + @classmethod - def build(cls, obj): # <5> - if isinstance(obj, abc.Mapping): # <6> + def build(cls, obj): # <6> + if isinstance(obj, abc.Mapping): # <7> return cls(obj) - elif isinstance(obj, abc.MutableSequence): # <7> + elif isinstance(obj, abc.MutableSequence): # <8> return [cls.build(item) for item in obj] - else: # <8> + else: # <9> return obj # end::EXPLORE0[] diff --git a/23-dyn-attr-prop/oscon/explore1.py b/22-dyn-attr-prop/oscon/explore1.py similarity index 94% rename from 23-dyn-attr-prop/oscon/explore1.py rename to 22-dyn-attr-prop/oscon/explore1.py index ecb53b0..6942079 100644 --- a/23-dyn-attr-prop/oscon/explore1.py +++ b/22-dyn-attr-prop/oscon/explore1.py @@ -62,11 +62,14 @@ def __init__(self, mapping): # end::EXPLORE1[] def __getattr__(self, name): - if hasattr(self.__data, name): + try: return getattr(self.__data, name) - else: + except AttributeError: return FrozenJSON.build(self.__data[name]) + def __dir__(self): # <5> + return self.__data.keys() + @classmethod def build(cls, obj): if isinstance(obj, abc.Mapping): diff --git a/23-dyn-attr-prop/oscon/explore2.py b/22-dyn-attr-prop/oscon/explore2.py similarity index 93% rename from 23-dyn-attr-prop/oscon/explore2.py rename to 22-dyn-attr-prop/oscon/explore2.py index 8cb9348..b50d49f 100644 --- a/23-dyn-attr-prop/oscon/explore2.py +++ b/22-dyn-attr-prop/oscon/explore2.py @@ -47,8 +47,11 @@ def __init__(self, mapping): self.__data[key] = value def __getattr__(self, name): - if hasattr(self.__data, name): + try: return getattr(self.__data, name) - else: + except AttributeError: return FrozenJSON(self.__data[name]) # <4> + + def __dir__(self): + return self.__data.keys() # end::EXPLORE2[] diff --git a/23-dyn-attr-prop/oscon/osconfeed-sample.json b/22-dyn-attr-prop/oscon/osconfeed-sample.json similarity index 100% rename from 23-dyn-attr-prop/oscon/osconfeed-sample.json rename to 22-dyn-attr-prop/oscon/osconfeed-sample.json diff --git a/23-dyn-attr-prop/oscon/osconfeed_explore.rst b/22-dyn-attr-prop/oscon/osconfeed_explore.rst similarity index 100% rename from 23-dyn-attr-prop/oscon/osconfeed_explore.rst rename to 22-dyn-attr-prop/oscon/osconfeed_explore.rst diff --git a/23-dyn-attr-prop/oscon/runtests.sh b/22-dyn-attr-prop/oscon/runtests.sh similarity index 100% rename from 23-dyn-attr-prop/oscon/runtests.sh rename to 22-dyn-attr-prop/oscon/runtests.sh diff --git a/23-dyn-attr-prop/oscon/schedule_v1.py b/22-dyn-attr-prop/oscon/schedule_v1.py similarity index 90% rename from 23-dyn-attr-prop/oscon/schedule_v1.py rename to 22-dyn-attr-prop/oscon/schedule_v1.py index 1be6c41..12aab71 100644 --- a/23-dyn-attr-prop/oscon/schedule_v1.py +++ b/22-dyn-attr-prop/oscon/schedule_v1.py @@ -23,8 +23,7 @@ def __init__(self, **kwargs): self.__dict__.update(kwargs) # <1> def __repr__(self): - cls_name = self.__class__.__name__ - return f'<{cls_name} serial={self.serial!r}>' # <2> + return f'<{self.__class__.__name__} serial={self.serial!r}>' # <2> def load(path=JSON_PATH): records = {} # <3> diff --git a/23-dyn-attr-prop/oscon/schedule_v2.py b/22-dyn-attr-prop/oscon/schedule_v2.py similarity index 87% rename from 23-dyn-attr-prop/oscon/schedule_v2.py rename to 22-dyn-attr-prop/oscon/schedule_v2.py index 35827eb..5f264f7 100644 --- a/23-dyn-attr-prop/oscon/schedule_v2.py +++ b/22-dyn-attr-prop/oscon/schedule_v2.py @@ -29,8 +29,7 @@ def __init__(self, **kwargs): self.__dict__.update(kwargs) def __repr__(self): - cls_name = self.__class__.__name__ - return f'<{cls_name} serial={self.serial!r}>' + return f'<{self.__class__.__name__} serial={self.serial!r}>' @staticmethod # <3> def fetch(key): @@ -44,10 +43,9 @@ def fetch(key): class Event(Record): # <1> def __repr__(self): - if hasattr(self, 'name'): # <2> - cls_name = self.__class__.__name__ - return f'<{cls_name} {self.name!r}>' - else: + try: + return f'<{self.__class__.__name__} {self.name!r}>' # <2> + except AttributeError: return super().__repr__() @property diff --git a/23-dyn-attr-prop/oscon/schedule_v3.py b/22-dyn-attr-prop/oscon/schedule_v3.py similarity index 86% rename from 23-dyn-attr-prop/oscon/schedule_v3.py rename to 22-dyn-attr-prop/oscon/schedule_v3.py index 786a412..7683b2d 100644 --- a/23-dyn-attr-prop/oscon/schedule_v3.py +++ b/22-dyn-attr-prop/oscon/schedule_v3.py @@ -33,8 +33,7 @@ def __init__(self, **kwargs): self.__dict__.update(kwargs) def __repr__(self): - cls_name = self.__class__.__name__ - return f'<{cls_name} serial={self.serial!r}>' + return f'<{self.__class__.__name__} serial={self.serial!r}>' @staticmethod def fetch(key): @@ -46,11 +45,10 @@ def fetch(key): class Event(Record): def __repr__(self): - if hasattr(self, 'name'): # <3> - cls_name = self.__class__.__name__ - return f'<{cls_name} {self.name!r}>' - else: - return super().__repr__() # <4> + try: + return f'<{self.__class__.__name__} {self.name!r}>' + except AttributeError: + return super().__repr__() @property def venue(self): diff --git a/23-dyn-attr-prop/oscon/schedule_v4.py b/22-dyn-attr-prop/oscon/schedule_v4.py similarity index 87% rename from 23-dyn-attr-prop/oscon/schedule_v4.py rename to 22-dyn-attr-prop/oscon/schedule_v4.py index a609917..5b968e1 100644 --- a/23-dyn-attr-prop/oscon/schedule_v4.py +++ b/22-dyn-attr-prop/oscon/schedule_v4.py @@ -32,8 +32,7 @@ def __init__(self, **kwargs): self.__dict__.update(kwargs) def __repr__(self): - cls_name = self.__class__.__name__ - return f'<{cls_name} serial={self.serial!r}>' + return f'<{self.__class__.__name__} serial={self.serial!r}>' @staticmethod def fetch(key): @@ -52,11 +51,10 @@ def __init__(self, **kwargs): # end::SCHEDULE4_INIT[] def __repr__(self): - if hasattr(self, 'name'): - cls_name = self.__class__.__name__ - return f'<{cls_name} {self.name!r}>' - else: - return super().__repr__() # <4> + try: + return f'<{self.__class__.__name__} {self.name!r}>' + except AttributeError: + return super().__repr__() @property def venue(self): diff --git a/23-dyn-attr-prop/oscon/schedule_v4_hasattr.py b/22-dyn-attr-prop/oscon/schedule_v4_hasattr.py similarity index 87% rename from 23-dyn-attr-prop/oscon/schedule_v4_hasattr.py rename to 22-dyn-attr-prop/oscon/schedule_v4_hasattr.py index ac5d5de..abc72bf 100644 --- a/23-dyn-attr-prop/oscon/schedule_v4_hasattr.py +++ b/22-dyn-attr-prop/oscon/schedule_v4_hasattr.py @@ -31,8 +31,7 @@ def __init__(self, **kwargs): self.__dict__.update(kwargs) def __repr__(self): - cls_name = self.__class__.__name__ - return f'<{cls_name} serial={self.serial!r}>' + return f'<{self.__class__.__name__} serial={self.serial!r}>' @staticmethod def fetch(key): @@ -44,11 +43,10 @@ def fetch(key): class Event(Record): def __repr__(self): - if hasattr(self, 'name'): - cls_name = self.__class__.__name__ - return f'<{cls_name} {self.name!r}>' - else: - return super().__repr__() # <4> + try: + return f'<{self.__class__.__name__} {self.name!r}>' + except AttributeError: + return super().__repr__() @property def venue(self): diff --git a/23-dyn-attr-prop/oscon/schedule_v5.py b/22-dyn-attr-prop/oscon/schedule_v5.py similarity index 89% rename from 23-dyn-attr-prop/oscon/schedule_v5.py rename to 22-dyn-attr-prop/oscon/schedule_v5.py index 2517e57..7a9543b 100644 --- a/23-dyn-attr-prop/oscon/schedule_v5.py +++ b/22-dyn-attr-prop/oscon/schedule_v5.py @@ -36,8 +36,7 @@ def __init__(self, **kwargs): self.__dict__.update(kwargs) def __repr__(self): - cls_name = self.__class__.__name__ - return f'<{cls_name} serial={self.serial!r}>' + return f'<{self.__class__.__name__} serial={self.serial!r}>' @staticmethod def fetch(key): @@ -49,10 +48,9 @@ def fetch(key): class Event(Record): def __repr__(self): - if hasattr(self, 'name'): - cls_name = self.__class__.__name__ - return f'<{cls_name} {self.name!r}>' - else: + try: + return f'<{self.__class__.__name__} {self.name!r}>' + except AttributeError: return super().__repr__() # tag::SCHEDULE5_CACHED_PROPERTY[] diff --git a/23-dyn-attr-prop/oscon/test_schedule_v1.py b/22-dyn-attr-prop/oscon/test_schedule_v1.py similarity index 95% rename from 23-dyn-attr-prop/oscon/test_schedule_v1.py rename to 22-dyn-attr-prop/oscon/test_schedule_v1.py index 643f04f..cfd7dd0 100644 --- a/23-dyn-attr-prop/oscon/test_schedule_v1.py +++ b/22-dyn-attr-prop/oscon/test_schedule_v1.py @@ -3,7 +3,7 @@ import schedule_v1 as schedule -@pytest.yield_fixture +@pytest.fixture def records(): yield schedule.load(schedule.JSON_PATH) diff --git a/23-dyn-attr-prop/oscon/test_schedule_v2.py b/22-dyn-attr-prop/oscon/test_schedule_v2.py similarity index 98% rename from 23-dyn-attr-prop/oscon/test_schedule_v2.py rename to 22-dyn-attr-prop/oscon/test_schedule_v2.py index a11b24d..e15d640 100644 --- a/23-dyn-attr-prop/oscon/test_schedule_v2.py +++ b/22-dyn-attr-prop/oscon/test_schedule_v2.py @@ -2,7 +2,7 @@ import schedule_v2 as schedule -@pytest.yield_fixture +@pytest.fixture def records(): yield schedule.load(schedule.JSON_PATH) diff --git a/23-dyn-attr-prop/oscon/test_schedule_v3.py b/22-dyn-attr-prop/oscon/test_schedule_v3.py similarity index 96% rename from 23-dyn-attr-prop/oscon/test_schedule_v3.py rename to 22-dyn-attr-prop/oscon/test_schedule_v3.py index 1b7ea75..12332c4 100644 --- a/23-dyn-attr-prop/oscon/test_schedule_v3.py +++ b/22-dyn-attr-prop/oscon/test_schedule_v3.py @@ -2,7 +2,7 @@ import schedule_v3 as schedule -@pytest.yield_fixture +@pytest.fixture def records(): yield schedule.load(schedule.JSON_PATH) @@ -56,4 +56,4 @@ def test_event_speakers(): def test_event_no_speakers(): event = schedule.Record.fetch('event.36848') - assert event.speakers == [] \ No newline at end of file + assert event.speakers == [] diff --git a/23-dyn-attr-prop/oscon/test_schedule_v4.py b/22-dyn-attr-prop/oscon/test_schedule_v4.py similarity index 98% rename from 23-dyn-attr-prop/oscon/test_schedule_v4.py rename to 22-dyn-attr-prop/oscon/test_schedule_v4.py index 1e5560a..4980bbd 100644 --- a/23-dyn-attr-prop/oscon/test_schedule_v4.py +++ b/22-dyn-attr-prop/oscon/test_schedule_v4.py @@ -2,7 +2,7 @@ import schedule_v4 as schedule -@pytest.yield_fixture +@pytest.fixture def records(): yield schedule.load(schedule.JSON_PATH) diff --git a/23-dyn-attr-prop/oscon/test_schedule_v5.py b/22-dyn-attr-prop/oscon/test_schedule_v5.py similarity index 98% rename from 23-dyn-attr-prop/oscon/test_schedule_v5.py rename to 22-dyn-attr-prop/oscon/test_schedule_v5.py index 2151844..e3a5206 100644 --- a/23-dyn-attr-prop/oscon/test_schedule_v5.py +++ b/22-dyn-attr-prop/oscon/test_schedule_v5.py @@ -2,7 +2,7 @@ import schedule_v5 as schedule -@pytest.yield_fixture +@pytest.fixture def records(): yield schedule.load(schedule.JSON_PATH) diff --git a/23-dyn-attr-prop/pseudo_construction.py b/22-dyn-attr-prop/pseudo_construction.py similarity index 87% rename from 23-dyn-attr-prop/pseudo_construction.py rename to 22-dyn-attr-prop/pseudo_construction.py index 5656192..af651a2 100644 --- a/23-dyn-attr-prop/pseudo_construction.py +++ b/22-dyn-attr-prop/pseudo_construction.py @@ -1,4 +1,4 @@ -# pseudo-code for object construction +# pseudocode for object construction def make(the_class, some_arg): new_object = the_class.__new__(some_arg) if isinstance(new_object, the_class): diff --git a/24-descriptor/README.rst b/23-descriptor/README.rst similarity index 100% rename from 24-descriptor/README.rst rename to 23-descriptor/README.rst diff --git a/24-descriptor/bulkfood/bulkfood_v3.py b/23-descriptor/bulkfood/bulkfood_v3.py similarity index 100% rename from 24-descriptor/bulkfood/bulkfood_v3.py rename to 23-descriptor/bulkfood/bulkfood_v3.py diff --git a/24-descriptor/bulkfood/bulkfood_v4.py b/23-descriptor/bulkfood/bulkfood_v4.py similarity index 100% rename from 24-descriptor/bulkfood/bulkfood_v4.py rename to 23-descriptor/bulkfood/bulkfood_v4.py diff --git a/24-descriptor/bulkfood/bulkfood_v4c.py b/23-descriptor/bulkfood/bulkfood_v4c.py similarity index 100% rename from 24-descriptor/bulkfood/bulkfood_v4c.py rename to 23-descriptor/bulkfood/bulkfood_v4c.py diff --git a/24-descriptor/bulkfood/bulkfood_v5.py b/23-descriptor/bulkfood/bulkfood_v5.py similarity index 100% rename from 24-descriptor/bulkfood/bulkfood_v5.py rename to 23-descriptor/bulkfood/bulkfood_v5.py diff --git a/24-descriptor/bulkfood/model_v4c.py b/23-descriptor/bulkfood/model_v4c.py similarity index 100% rename from 24-descriptor/bulkfood/model_v4c.py rename to 23-descriptor/bulkfood/model_v4c.py diff --git a/24-descriptor/bulkfood/model_v5.py b/23-descriptor/bulkfood/model_v5.py similarity index 94% rename from 24-descriptor/bulkfood/model_v5.py rename to 23-descriptor/bulkfood/model_v5.py index 018afcd..4c35698 100644 --- a/24-descriptor/bulkfood/model_v5.py +++ b/23-descriptor/bulkfood/model_v5.py @@ -30,7 +30,7 @@ class NonBlank(Validated): def validate(self, name, value): value = value.strip() - if len(value) == 0: + if not value: # <2> raise ValueError(f'{name} cannot be blank') - return value # <2> + return value # <3> # end::MODEL_V5_VALIDATED_SUB[] diff --git a/24-descriptor/descriptorkinds.py b/23-descriptor/descriptorkinds.py similarity index 92% rename from 24-descriptor/descriptorkinds.py rename to 23-descriptor/descriptorkinds.py index 44f4018..13df709 100644 --- a/24-descriptor/descriptorkinds.py +++ b/23-descriptor/descriptorkinds.py @@ -5,21 +5,18 @@ >>> obj = Managed() # <1> >>> obj.over # <2> - -> Overriding.__get__(, , - ) + -> Overriding.__get__(, , ) >>> Managed.over # <3> -> Overriding.__get__(, None, ) >>> obj.over = 7 # <4> -> Overriding.__set__(, , 7) >>> obj.over # <5> - -> Overriding.__get__(, , - ) + -> Overriding.__get__(, , ) >>> obj.__dict__['over'] = 8 # <6> >>> vars(obj) # <7> {'over': 8} >>> obj.over # <8> - -> Overriding.__get__(, , - ) + -> Overriding.__get__(, , ) # end::DESCR_KINDS_DEMO1[] @@ -50,8 +47,7 @@ >>> obj = Managed() >>> obj.non_over # <1> - -> NonOverriding.__get__(, , - ) + -> NonOverriding.__get__(, , ) >>> obj.non_over = 7 # <2> >>> obj.non_over # <3> 7 @@ -59,8 +55,7 @@ -> NonOverriding.__get__(, None, ) >>> del obj.non_over # <5> >>> obj.non_over # <6> - -> NonOverriding.__get__(, , - ) + -> NonOverriding.__get__(, , ) # end::DESCR_KINDS_DEMO3[] @@ -88,7 +83,7 @@ >>> Managed.spam() Traceback (most recent call last): ... - TypeError: spam() missing 1 required positional argument: 'self' + TypeError: Managed.spam() missing 1 required positional argument: 'self' >>> Managed.spam(obj) -> Managed.spam() >>> Managed.spam.__get__(obj) # doctest: +ELLIPSIS @@ -156,15 +151,15 @@ def cls_name(obj_or_cls): def display(obj): cls = type(obj) if cls is type: - return ''.format(obj.__name__) + return f'' elif cls in [type(None), int]: return repr(obj) else: - return '<{} object>'.format(cls_name(obj)) + return f'<{cls_name(obj)} object>' def print_args(name, *args): pseudo_args = ', '.join(display(x) for x in args) - print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args)) + print(f'-> {cls_name(args[0])}.__{name}__({pseudo_args})') ### essential classes for this example ### @@ -199,6 +194,6 @@ class Managed: # <5> non_over = NonOverriding() def spam(self): # <6> - print('-> Managed.spam({})'.format(display(self))) + print(f'-> Managed.spam({display(self)})') # end::DESCR_KINDS[] diff --git a/24-descriptor/descriptorkinds_dump.py b/23-descriptor/descriptorkinds_dump.py similarity index 100% rename from 24-descriptor/descriptorkinds_dump.py rename to 23-descriptor/descriptorkinds_dump.py diff --git a/24-descriptor/method_is_descriptor.py b/23-descriptor/method_is_descriptor.py similarity index 100% rename from 24-descriptor/method_is_descriptor.py rename to 23-descriptor/method_is_descriptor.py diff --git a/23-dyn-attr-prop/oscon/demo_schedule2.py b/23-dyn-attr-prop/oscon/demo_schedule2.py deleted file mode 100755 index 12fd440..0000000 --- a/23-dyn-attr-prop/oscon/demo_schedule2.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 - -import shelve - -from schedule_v2 import DB_NAME, CONFERENCE, load_db -from schedule_v2 import DbRecord, Event - -with shelve.open(DB_NAME) as db: - if CONFERENCE not in db: - load_db(db) - - DbRecord.set_db(db) - event = DbRecord.fetch('event.33950') - print(event) - print(event.venue) - print(event.venue.name) - for spkr in event.speakers: - print(f'{spkr.serial}:', spkr.name) - - print(repr(Event.venue)) - - event2 = DbRecord.fetch('event.33451') - print(event2) - print(event2.fetch) - print(event2.venue) \ No newline at end of file diff --git a/25-class-metaprog/autoconst/autoconst.py b/24-class-metaprog/autoconst/autoconst.py similarity index 100% rename from 25-class-metaprog/autoconst/autoconst.py rename to 24-class-metaprog/autoconst/autoconst.py diff --git a/25-class-metaprog/autoconst/autoconst_demo.py b/24-class-metaprog/autoconst/autoconst_demo.py similarity index 100% rename from 25-class-metaprog/autoconst/autoconst_demo.py rename to 24-class-metaprog/autoconst/autoconst_demo.py diff --git a/25-class-metaprog/bulkfood/README.md b/24-class-metaprog/bulkfood/README.md similarity index 100% rename from 25-class-metaprog/bulkfood/README.md rename to 24-class-metaprog/bulkfood/README.md diff --git a/25-class-metaprog/bulkfood/bulkfood_v6.py b/24-class-metaprog/bulkfood/bulkfood_v6.py similarity index 100% rename from 25-class-metaprog/bulkfood/bulkfood_v6.py rename to 24-class-metaprog/bulkfood/bulkfood_v6.py diff --git a/25-class-metaprog/bulkfood/bulkfood_v7.py b/24-class-metaprog/bulkfood/bulkfood_v7.py similarity index 100% rename from 25-class-metaprog/bulkfood/bulkfood_v7.py rename to 24-class-metaprog/bulkfood/bulkfood_v7.py diff --git a/25-class-metaprog/bulkfood/bulkfood_v8.py b/24-class-metaprog/bulkfood/bulkfood_v8.py similarity index 100% rename from 25-class-metaprog/bulkfood/bulkfood_v8.py rename to 24-class-metaprog/bulkfood/bulkfood_v8.py diff --git a/25-class-metaprog/bulkfood/model_v6.py b/24-class-metaprog/bulkfood/model_v6.py similarity index 100% rename from 25-class-metaprog/bulkfood/model_v6.py rename to 24-class-metaprog/bulkfood/model_v6.py diff --git a/25-class-metaprog/bulkfood/model_v7.py b/24-class-metaprog/bulkfood/model_v7.py similarity index 100% rename from 25-class-metaprog/bulkfood/model_v7.py rename to 24-class-metaprog/bulkfood/model_v7.py diff --git a/25-class-metaprog/bulkfood/model_v8.py b/24-class-metaprog/bulkfood/model_v8.py similarity index 100% rename from 25-class-metaprog/bulkfood/model_v8.py rename to 24-class-metaprog/bulkfood/model_v8.py diff --git a/25-class-metaprog/checked/decorator/checkeddeco.py b/24-class-metaprog/checked/decorator/checkeddeco.py similarity index 100% rename from 25-class-metaprog/checked/decorator/checkeddeco.py rename to 24-class-metaprog/checked/decorator/checkeddeco.py diff --git a/25-class-metaprog/checked/decorator/checkeddeco_demo.py b/24-class-metaprog/checked/decorator/checkeddeco_demo.py similarity index 100% rename from 25-class-metaprog/checked/decorator/checkeddeco_demo.py rename to 24-class-metaprog/checked/decorator/checkeddeco_demo.py diff --git a/25-class-metaprog/checked/decorator/checkeddeco_test.py b/24-class-metaprog/checked/decorator/checkeddeco_test.py similarity index 100% rename from 25-class-metaprog/checked/decorator/checkeddeco_test.py rename to 24-class-metaprog/checked/decorator/checkeddeco_test.py diff --git a/25-class-metaprog/checked/initsub/checked_demo.py b/24-class-metaprog/checked/initsub/checked_demo.py similarity index 100% rename from 25-class-metaprog/checked/initsub/checked_demo.py rename to 24-class-metaprog/checked/initsub/checked_demo.py diff --git a/25-class-metaprog/checked/initsub/checkedlib.py b/24-class-metaprog/checked/initsub/checkedlib.py similarity index 100% rename from 25-class-metaprog/checked/initsub/checkedlib.py rename to 24-class-metaprog/checked/initsub/checkedlib.py diff --git a/25-class-metaprog/checked/initsub/checkedlib_test.py b/24-class-metaprog/checked/initsub/checkedlib_test.py similarity index 100% rename from 25-class-metaprog/checked/initsub/checkedlib_test.py rename to 24-class-metaprog/checked/initsub/checkedlib_test.py diff --git a/25-class-metaprog/checked/metaclass/checked_demo.py b/24-class-metaprog/checked/metaclass/checked_demo.py similarity index 100% rename from 25-class-metaprog/checked/metaclass/checked_demo.py rename to 24-class-metaprog/checked/metaclass/checked_demo.py diff --git a/25-class-metaprog/checked/metaclass/checkedlib.py b/24-class-metaprog/checked/metaclass/checkedlib.py similarity index 97% rename from 25-class-metaprog/checked/metaclass/checkedlib.py rename to 24-class-metaprog/checked/metaclass/checkedlib.py index 8207dbe..768a0f6 100644 --- a/25-class-metaprog/checked/metaclass/checkedlib.py +++ b/24-class-metaprog/checked/metaclass/checkedlib.py @@ -78,7 +78,9 @@ def __init__(self, name: str, constructor: Callable) -> None: self.storage_name = '_' + name # <1> self.constructor = constructor - def __get__(self, instance, owner=None): # <2> + def __get__(self, instance, owner=None): + if instance is None: # <2> + return self return getattr(instance, self.storage_name) # <3> def __set__(self, instance: Any, value: Any) -> None: diff --git a/25-class-metaprog/checked/metaclass/checkedlib_test.py b/24-class-metaprog/checked/metaclass/checkedlib_test.py similarity index 100% rename from 25-class-metaprog/checked/metaclass/checkedlib_test.py rename to 24-class-metaprog/checked/metaclass/checkedlib_test.py diff --git a/25-class-metaprog/evaltime/builderlib.py b/24-class-metaprog/evaltime/builderlib.py similarity index 100% rename from 25-class-metaprog/evaltime/builderlib.py rename to 24-class-metaprog/evaltime/builderlib.py diff --git a/25-class-metaprog/evaltime/evaldemo.py b/24-class-metaprog/evaltime/evaldemo.py similarity index 100% rename from 25-class-metaprog/evaltime/evaldemo.py rename to 24-class-metaprog/evaltime/evaldemo.py diff --git a/25-class-metaprog/evaltime/evaldemo_meta.py b/24-class-metaprog/evaltime/evaldemo_meta.py similarity index 100% rename from 25-class-metaprog/evaltime/evaldemo_meta.py rename to 24-class-metaprog/evaltime/evaldemo_meta.py diff --git a/25-class-metaprog/evaltime/metalib.py b/24-class-metaprog/evaltime/metalib.py similarity index 100% rename from 25-class-metaprog/evaltime/metalib.py rename to 24-class-metaprog/evaltime/metalib.py diff --git a/25-class-metaprog/factories.py b/24-class-metaprog/factories.py similarity index 95% rename from 25-class-metaprog/factories.py rename to 24-class-metaprog/factories.py index 38ec763..cf61d89 100644 --- a/25-class-metaprog/factories.py +++ b/24-class-metaprog/factories.py @@ -49,9 +49,8 @@ def __iter__(self) -> Iterator[Any]: # <5> yield getattr(self, name) def __repr__(self): # <6> - values = ', '.join( - '{}={!r}'.format(*i) for i in zip(self.__slots__, self) - ) + values = ', '.join(f'{name}={value!r}' + for name, value in zip(self.__slots__, self)) cls_name = self.__class__.__name__ return f'{cls_name}({values})' diff --git a/25-class-metaprog/factories_ducktyped.py b/24-class-metaprog/factories_ducktyped.py similarity index 100% rename from 25-class-metaprog/factories_ducktyped.py rename to 24-class-metaprog/factories_ducktyped.py diff --git a/25-class-metaprog/hours/hours.py b/24-class-metaprog/hours/hours.py similarity index 100% rename from 25-class-metaprog/hours/hours.py rename to 24-class-metaprog/hours/hours.py diff --git a/25-class-metaprog/hours/hours_test.py b/24-class-metaprog/hours/hours_test.py similarity index 100% rename from 25-class-metaprog/hours/hours_test.py rename to 24-class-metaprog/hours/hours_test.py diff --git a/25-class-metaprog/metabunch/README.md b/24-class-metaprog/metabunch/README.md similarity index 100% rename from 25-class-metaprog/metabunch/README.md rename to 24-class-metaprog/metabunch/README.md diff --git a/25-class-metaprog/metabunch/from3.6/bunch.py b/24-class-metaprog/metabunch/from3.6/bunch.py similarity index 93% rename from 25-class-metaprog/metabunch/from3.6/bunch.py rename to 24-class-metaprog/metabunch/from3.6/bunch.py index a8b3d5a..aa0a65e 100644 --- a/25-class-metaprog/metabunch/from3.6/bunch.py +++ b/24-class-metaprog/metabunch/from3.6/bunch.py @@ -28,7 +28,7 @@ >>> Point(x=1, y=2, z=3) Traceback (most recent call last): ... - AttributeError: 'Point' object has no attribute 'z' + AttributeError: No slots left for: 'z' >>> p = Point(x=21) >>> p.y = 42 >>> p @@ -51,7 +51,8 @@ def __init__(self, **kwargs): # <4> for name, default in defaults.items(): # <5> setattr(self, name, kwargs.pop(name, default)) if kwargs: # <6> - setattr(self, *kwargs.popitem()) + extra = ', '.join(kwargs) + raise AttributeError(f'No slots left for: {extra!r}') def __repr__(self): # <7> rep = ', '.join(f'{name}={value!r}' diff --git a/25-class-metaprog/metabunch/from3.6/bunch_test.py b/24-class-metaprog/metabunch/from3.6/bunch_test.py similarity index 96% rename from 25-class-metaprog/metabunch/from3.6/bunch_test.py rename to 24-class-metaprog/metabunch/from3.6/bunch_test.py index 322487a..166e600 100644 --- a/25-class-metaprog/metabunch/from3.6/bunch_test.py +++ b/24-class-metaprog/metabunch/from3.6/bunch_test.py @@ -26,7 +26,7 @@ def test_init(): def test_init_wrong_argument(): with pytest.raises(AttributeError) as exc: p = Point(x=1.2, y=3.4, flavor='coffee') - assert "no attribute 'flavor'" in str(exc.value) + assert 'flavor' in str(exc.value) def test_slots(): diff --git a/25-class-metaprog/metabunch/nutshell3e/bunch.py b/24-class-metaprog/metabunch/nutshell3e/bunch.py similarity index 100% rename from 25-class-metaprog/metabunch/nutshell3e/bunch.py rename to 24-class-metaprog/metabunch/nutshell3e/bunch.py diff --git a/25-class-metaprog/metabunch/nutshell3e/bunch_test.py b/24-class-metaprog/metabunch/nutshell3e/bunch_test.py similarity index 100% rename from 25-class-metaprog/metabunch/nutshell3e/bunch_test.py rename to 24-class-metaprog/metabunch/nutshell3e/bunch_test.py diff --git a/25-class-metaprog/metabunch/original/bunch.py b/24-class-metaprog/metabunch/original/bunch.py similarity index 100% rename from 25-class-metaprog/metabunch/original/bunch.py rename to 24-class-metaprog/metabunch/original/bunch.py diff --git a/25-class-metaprog/metabunch/original/bunch_test.py b/24-class-metaprog/metabunch/original/bunch_test.py similarity index 100% rename from 25-class-metaprog/metabunch/original/bunch_test.py rename to 24-class-metaprog/metabunch/original/bunch_test.py diff --git a/25-class-metaprog/metabunch/pre3.6/bunch.py b/24-class-metaprog/metabunch/pre3.6/bunch.py similarity index 100% rename from 25-class-metaprog/metabunch/pre3.6/bunch.py rename to 24-class-metaprog/metabunch/pre3.6/bunch.py diff --git a/25-class-metaprog/metabunch/pre3.6/bunch_test.py b/24-class-metaprog/metabunch/pre3.6/bunch_test.py similarity index 100% rename from 25-class-metaprog/metabunch/pre3.6/bunch_test.py rename to 24-class-metaprog/metabunch/pre3.6/bunch_test.py diff --git a/25-class-metaprog/persistent/.gitignore b/24-class-metaprog/persistent/.gitignore similarity index 100% rename from 25-class-metaprog/persistent/.gitignore rename to 24-class-metaprog/persistent/.gitignore diff --git a/25-class-metaprog/persistent/dblib.py b/24-class-metaprog/persistent/dblib.py similarity index 100% rename from 25-class-metaprog/persistent/dblib.py rename to 24-class-metaprog/persistent/dblib.py diff --git a/25-class-metaprog/persistent/dblib_test.py b/24-class-metaprog/persistent/dblib_test.py similarity index 100% rename from 25-class-metaprog/persistent/dblib_test.py rename to 24-class-metaprog/persistent/dblib_test.py diff --git a/25-class-metaprog/persistent/persistlib.py b/24-class-metaprog/persistent/persistlib.py similarity index 100% rename from 25-class-metaprog/persistent/persistlib.py rename to 24-class-metaprog/persistent/persistlib.py diff --git a/25-class-metaprog/persistent/persistlib_test.py b/24-class-metaprog/persistent/persistlib_test.py similarity index 100% rename from 25-class-metaprog/persistent/persistlib_test.py rename to 24-class-metaprog/persistent/persistlib_test.py diff --git a/25-class-metaprog/qualname/fakedjango.py b/24-class-metaprog/qualname/fakedjango.py similarity index 100% rename from 25-class-metaprog/qualname/fakedjango.py rename to 24-class-metaprog/qualname/fakedjango.py diff --git a/25-class-metaprog/qualname/models.py b/24-class-metaprog/qualname/models.py similarity index 100% rename from 25-class-metaprog/qualname/models.py rename to 24-class-metaprog/qualname/models.py diff --git a/25-class-metaprog/sentinel/sentinel.py b/24-class-metaprog/sentinel/sentinel.py similarity index 100% rename from 25-class-metaprog/sentinel/sentinel.py rename to 24-class-metaprog/sentinel/sentinel.py diff --git a/25-class-metaprog/sentinel/sentinel_test.py b/24-class-metaprog/sentinel/sentinel_test.py similarity index 100% rename from 25-class-metaprog/sentinel/sentinel_test.py rename to 24-class-metaprog/sentinel/sentinel_test.py diff --git a/25-class-metaprog/setattr/example_from_leo.py b/24-class-metaprog/setattr/example_from_leo.py similarity index 100% rename from 25-class-metaprog/setattr/example_from_leo.py rename to 24-class-metaprog/setattr/example_from_leo.py diff --git a/25-class-metaprog/slots/slots_timing.py b/24-class-metaprog/slots/slots_timing.py similarity index 100% rename from 25-class-metaprog/slots/slots_timing.py rename to 24-class-metaprog/slots/slots_timing.py diff --git a/24-class-metaprog/timeslice.py b/24-class-metaprog/timeslice.py new file mode 100644 index 0000000..5a52457 --- /dev/null +++ b/24-class-metaprog/timeslice.py @@ -0,0 +1,91 @@ + +""" +Could this be valid Python? + + if now >= T[4:20:PM]: chill() + + +>>> t = T[4:20] +>>> t +T[4:20] +>>> h, m, s = t +>>> h, m, s +(4, 20, 0) +>>> t[11:59:AM] +T[11:59:AM] +>>> start = t[9:O1:PM] +>>> start +T[9:O1:PM] +>>> start.h, start.m, start.s, start.pm +(9, 1, 0, True) +>>> now = T[7:O1:PM] +>>> T[4:OO:PM] +T[4:OO:PM] +>>> now > T[4:20:PM] +True +""" + +import functools + +AM = -2 +PM = -1 + +for n in range(10): + globals()[f'O{n}'] = n +OO = 0 + +@functools.total_ordering +class T(): + + def __init__(self, arg): + if isinstance(arg, slice): + h = arg.start or 0 + m = arg.stop or 0 + s = arg.step or 0 + else: + h, m, s = 0, 0, arg + if m in (AM, PM): + self.pm = m == PM + m = 0 + elif s in (AM, PM): + self.pm = s == PM + s = 0 + else: + self.pm = None + self.h, self.m, self.s = h, m, s + + def __class_getitem__(cls, arg): + return cls(arg) + + def __getitem__(self, arg): + return(type(self)(arg)) + + def __repr__(self): + h, m, s = self.h, self.m, self.s or None + if m == 0: + m = f'OO' + elif m < 10: + m = f'O{m}' + s = '' if s is None else s + if self.pm is None: + pm = '' + else: + pm = ':' + ('AM', 'PM')[self.pm] + return f'T[{h}:{m}{s}{pm}]' + + def __iter__(self): + yield from (self.h, self.m, self.s) + + def __eq__(self, other): + return tuple(self) == tuple(other) + + def __lt__(self, other): + return tuple(self) < tuple(other) + + def __add__(self, other): + """ + >>> T[11:O5:AM] + 15 # TODO: preserve pm field + T[11:20] + """ + if isinstance(other, int): + return self[self.h:self.m + other:self.pm] diff --git a/25-class-metaprog/tinyenums/microenum.py b/24-class-metaprog/tinyenums/microenum.py similarity index 100% rename from 25-class-metaprog/tinyenums/microenum.py rename to 24-class-metaprog/tinyenums/microenum.py diff --git a/25-class-metaprog/tinyenums/microenum_demo.py b/24-class-metaprog/tinyenums/microenum_demo.py similarity index 100% rename from 25-class-metaprog/tinyenums/microenum_demo.py rename to 24-class-metaprog/tinyenums/microenum_demo.py diff --git a/25-class-metaprog/tinyenums/nanoenum.py b/24-class-metaprog/tinyenums/nanoenum.py similarity index 100% rename from 25-class-metaprog/tinyenums/nanoenum.py rename to 24-class-metaprog/tinyenums/nanoenum.py diff --git a/25-class-metaprog/tinyenums/nanoenum_demo.py b/24-class-metaprog/tinyenums/nanoenum_demo.py similarity index 100% rename from 25-class-metaprog/tinyenums/nanoenum_demo.py rename to 24-class-metaprog/tinyenums/nanoenum_demo.py diff --git a/README.md b/README.md index 102993a..234c8da 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,7 @@ # Fluent Python 2e example code -Example code for the book **Fluent Python, 2nd edition** by Luciano Ramalho (O'Reilly, 2021). +Example code for the book **Fluent Python, Second Edition** by Luciano Ramalho (O'Reilly, 2022). -> **BEWARE**: This is a work in progress! -> -> * Code here may change and disappear without warning. -> -> * Major reorganizations may happen at any time. -> -> * No promises. No guarantees. Use at own risk. ## Table of Contents @@ -16,38 +9,37 @@ All chapters are undergoing review and updates, including significant rewrites i New chapters in **Fluent Python 2e** are marked with 🆕. -🚨 This table of contents is subject to change at any time until the book goes to the printer. +> 🚨  This table of contents is subject to change at any time until the book goes to the printer.
+Latest change: Old **Part I—Prologue** merged into new **Part I—Data Structures**; parts renumbered accordingly; chapter numbers unchanged. Part / Chapter #|Title|Directory|1st ed. Chapter # ---:|---|---|:---: -**I – Prologue**| +**I – Data Structures**| 1|The Python Data Model|[01-data-model](01-data-model)|1 -**II – Data Structures**| 2|An Array of Sequences|[02-array-seq](02-array-seq)|2 3|Dictionaries and Sets|[03-dict-set](03-dict-set)|3 4|Unicode Text versus Bytes|[04-text-byte](04-text-byte)|4 5|Data Class Builders|[05-data-classes](05-data-classes)|🆕 6|Object References, Mutability, and Recycling|[06-obj-ref](06-obj-ref)|8 -**III – Functions as Objects**| +**II – Functions as Objects**| 7|Funcions as First-Class Objects|[07-1class-func](07-1class-func)|5 -8|Type Hints in Function Definitions|[08-def-type-hints](08-def-type-hints)|🆕 -9|Function Decorators and Closures|[09-closure-deco](09-closure-deco)|7 +8|Type Hints in Functions|[08-def-type-hints](08-def-type-hints)|🆕 +9|Decorators and Closures|[09-closure-deco](09-closure-deco)|7 10|Design Patterns with First-Class Functions|[10-dp-1class-func](10-dp-1class-func)|6 -**IV – Object-Oriented Idioms**| +**III – Object-Oriented Idioms**| 11|A Pythonic Object|[11-pythonic-obj](11-pythonic-obj)|9 -12|Sequence Hacking, Hashing, and Slicing|[12-seq-hacking](12-seq-hacking)|10 +12|Special Methods for Sequences|[12-seq-hacking](12-seq-hacking)|10 13|Interfaces, Protocols, and ABCs|[13-protocl-abc](13-protocol-abc)|11 -14|Inheritance: For Good or For Worse|[14-inheritance](14-inheritance)|12 +14|Inheritance: For Better or For Worse|[14-inheritance](14-inheritance)|12 15|More About Type Hints|[15-more-types](15-more-types)|🆕 -16|Operator Overloading: Doing It Right|[16-op-overloading](16-op-overloading)|13 -**V – Control Flow**| -17|Iterables, Iterators, and Generators|[17-it-generator](17-it-generator)|14 -18|Context Managers and else Blocks|[18-with-match](18-with-match)|15 -19|Classic Coroutines|[19-coroutine](19-coroutine)|16 -20|Concurrency Models in Python|[20-concurrency](20-concurrency)|🆕 -21|Concurrency with Futures|[21-futures](21-futures)|17 -22|Asynchronous Programming|[22-async](22-async)|18 -**VI – Metaprogramming**| -23|Dynamic Attributes and Properties|[22-dyn-attr-prop](22-dyn-attr-prop)|19 -24|Attribute Descriptors|[23-descriptor](23-descriptor)|20 -25|Class Metaprogramming|[24-class-metaprog](24-class-metaprog)|21 +16|Operator Overloading|[16-op-overloading](16-op-overloading)|13 +**IV – Control Flow**| +17|Iterators, Generators, and Classic Coroutines|[17-it-generator](17-it-generator)|14 +18|with, match, and else Blocks|[18-with-match](18-with-match)|15 +19|Concurrency Models in Python|[19-concurrency](19-concurrency)|🆕 +20|Concurrent Executors|[20-executors](20-executors)|17 +21|Asynchronous Programming|[21-async](21-async)|18 +**V – Metaprogramming**| +22|Dynamic Attributes and Properties|[22-dyn-attr-prop](22-dyn-attr-prop)|19 +23|Attribute Descriptors|[23-descriptor](23-descriptor)|20 +24|Class Metaprogramming|[24-class-metaprog](24-class-metaprog)|21 diff --git a/links/FPY.LI.htaccess b/links/FPY.LI.htaccess new file mode 100644 index 0000000..5607fa4 --- /dev/null +++ b/links/FPY.LI.htaccess @@ -0,0 +1,1089 @@ +# to recreate or update this file: +# $ cat custom.htaccess short.htaccess > FPY.LI.htaccess + +ErrorDocument 404 /404.html + +# main resources +RedirectTemp /book https://www.oreilly.com/library/view/fluent-python-2nd/9781492056348/ +RedirectTemp /code https://github.com/fluentpython/example-code-2e +RedirectTemp /home https://www.fluentpython.com/ + +# URLs mentioned at least three times +RedirectTemp /bisect https://www.fluentpython.com/extra/ordered-sequences-with-bisect/ +RedirectTemp /cardxvi https://www.python.org/dev/peps/pep-0484/#the-numeric-tower +RedirectTemp /collec https://docs.python.org/3/library/collections.html +RedirectTemp /dask https://dask.org/ +RedirectTemp /dtmodel https://docs.python.org/3/reference/datamodel.html +RedirectTemp /descr101 https://www.python.org/download/releases/2.2.3/descrintro/ +RedirectTemp /descrhow https://docs.python.org/3/howto/descriptor.html +RedirectTemp /doctest https://docs.python.org/3/library/doctest.html +RedirectTemp /effectpy https://effectivepython.com/ +RedirectTemp /fmtspec https://docs.python.org/3/library/string.html#formatspec +RedirectTemp /gunicorn https://gunicorn.org/ +RedirectTemp /hashint https://www.fluentpython.com/extra/internals-of-sets-and-dicts/ +RedirectTemp /hattingh https://www.oreilly.com/library/view/using-asyncio-in/9781492075325/ +RedirectTemp /httpx https://www.python-httpx.org/ +RedirectTemp /initvar https://docs.python.org/3/library/dataclasses.html#init-only-variables +RedirectTemp /mypy https://mypy.readthedocs.io/en/stable/ +RedirectTemp /norvigdp http://norvig.com/design-patterns/ +RedirectTemp /nsphere https://en.wikipedia.org/wiki/N-sphere +RedirectTemp /oldcoro https://www.fluentpython.com/extra/classic-coroutines/ +RedirectTemp /pandas https://pandas.pydata.org/ +RedirectTemp /pycook3 https://www.oreilly.com/library/view/python-cookbook-3rd/9781449357337/ +RedirectTemp /pynut3 https://www.oreilly.com/library/view/python-in-a/9781491913833/ +RedirectTemp /pypydif https://doc.pypy.org/en/latest/cpython_differences.html#subclasses-of-built-in-types +RedirectTemp /shed4051 https://github.com/python/typeshed/issues/4051 +RedirectTemp /specattr https://docs.python.org/3/library/stdtypes.html#special-attributes +RedirectTemp /typecoro https://docs.python.org/3.10/library/typing.html#typing.Coroutine +RedirectTemp /typing https://docs.python.org/3/library/typing.html +RedirectTemp /weakref https://www.fluentpython.com/extra/weak-references/ + +# URL added during QA of the Second Edition +RedirectTemp /bdfl https://www.artima.com/weblogs/viewpost.jsp?thread=235725 + +# Python Enhancement Proposals +RedirectTemp /pep218 https://www.python.org/dev/peps/pep-0218/ +RedirectTemp /pep227 https://www.python.org/dev/peps/pep-0227/ +RedirectTemp /pep255 https://www.python.org/dev/peps/pep-0255/ +RedirectTemp /pep342 https://www.python.org/dev/peps/pep-0342/ +RedirectTemp /pep343 https://www.python.org/dev/peps/pep-0343/ +RedirectTemp /pep357 https://www.python.org/dev/peps/pep-0357/ +RedirectTemp /pep362 https://www.python.org/dev/peps/pep-0362/ +RedirectTemp /pep371 https://www.python.org/dev/peps/pep-0371/ +RedirectTemp /pep380 https://www.python.org/dev/peps/pep-0380/ +RedirectTemp /pep393 https://www.python.org/dev/peps/pep-0393/ +RedirectTemp /pep412 https://www.python.org/dev/peps/pep-0412/ +RedirectTemp /pep442 https://www.python.org/dev/peps/pep-0442/ +RedirectTemp /pep443 https://www.python.org/dev/peps/pep-0443/ +RedirectTemp /pep448 https://www.python.org/dev/peps/pep-0448/ +RedirectTemp /pep455 https://www.python.org/dev/peps/pep-0455/ +RedirectTemp /pep456 https://www.python.org/dev/peps/pep-0456/ +RedirectTemp /pep461 https://www.python.org/dev/peps/pep-0461/ +RedirectTemp /pep465 https://www.python.org/dev/peps/pep-0465/ +RedirectTemp /pep467 https://www.python.org/dev/peps/pep-0467/ +RedirectTemp /pep482 https://www.python.org/dev/peps/pep-0482/ +RedirectTemp /pep483 https://www.python.org/dev/peps/pep-0483/ +RedirectTemp /pep484 https://www.python.org/dev/peps/pep-0484/ +RedirectTemp /pep487 https://www.python.org/dev/peps/pep-0487/ +RedirectTemp /pep492 https://www.python.org/dev/peps/pep-0492/ +RedirectTemp /pep519 https://www.python.org/dev/peps/pep-0519/ +RedirectTemp /pep525 https://www.python.org/dev/peps/pep-0525/ +RedirectTemp /pep526 https://www.python.org/dev/peps/pep-0526/ +RedirectTemp /pep528 https://www.python.org/dev/peps/pep-0528/ +RedirectTemp /pep529 https://www.python.org/dev/peps/pep-0529/ +RedirectTemp /pep530 https://www.python.org/dev/peps/pep-0530/ +RedirectTemp /pep544 https://www.python.org/dev/peps/pep-0544/ +RedirectTemp /pep554 https://www.python.org/dev/peps/pep-0554/ +RedirectTemp /pep557 https://www.python.org/dev/peps/pep-0557/ +RedirectTemp /pep560 https://www.python.org/dev/peps/pep-0560/ +RedirectTemp /pep561 https://www.python.org/dev/peps/pep-0561/ +RedirectTemp /pep563 https://www.python.org/dev/peps/pep-0563/ +RedirectTemp /pep570 https://www.python.org/dev/peps/pep-0570/ +RedirectTemp /pep572 https://www.python.org/dev/peps/pep-0572/ +RedirectTemp /pep584 https://www.python.org/dev/peps/pep-0584/ +RedirectTemp /pep585 https://www.python.org/dev/peps/pep-0585/ +RedirectTemp /pep586 https://www.python.org/dev/peps/pep-0586/ +RedirectTemp /pep589 https://www.python.org/dev/peps/pep-0589/ +RedirectTemp /pep591 https://www.python.org/dev/peps/pep-0591/ +RedirectTemp /pep593 https://www.python.org/dev/peps/pep-0593/ +RedirectTemp /pep604 https://www.python.org/dev/peps/pep-0604/ +RedirectTemp /pep612 https://www.python.org/dev/peps/pep-0612/ +RedirectTemp /pep613 https://www.python.org/dev/peps/pep-0613/ +RedirectTemp /pep616 https://www.python.org/dev/peps/pep-0616/ +RedirectTemp /pep617 https://www.python.org/dev/peps/pep-0617/ +RedirectTemp /pep618 https://www.python.org/dev/peps/pep-0618/ +RedirectTemp /pep634 https://www.python.org/dev/peps/pep-0634/ +RedirectTemp /pep635 https://www.python.org/dev/peps/pep-0635/ +RedirectTemp /pep636 https://www.python.org/dev/peps/pep-0636/ +RedirectTemp /pep638 https://www.python.org/dev/peps/pep-0638/ +RedirectTemp /pep645 https://www.python.org/dev/peps/pep-0645/ +RedirectTemp /pep646 https://www.python.org/dev/peps/pep-0646/ +RedirectTemp /pep647 https://www.python.org/dev/peps/pep-0647/ +RedirectTemp /pep649 https://www.python.org/dev/peps/pep-0649/ +RedirectTemp /pep654 https://www.python.org/dev/peps/pep-0654/ +RedirectTemp /pep655 https://www.python.org/dev/peps/pep-0655/ +RedirectTemp /pep661 https://www.python.org/dev/peps/pep-0661/ +RedirectTemp /pep3099 https://www.python.org/dev/peps/pep-3099/ +RedirectTemp /pep3102 https://www.python.org/dev/peps/pep-3102/ +RedirectTemp /pep3104 https://www.python.org/dev/peps/pep-3104/ +RedirectTemp /pep3106 https://www.python.org/dev/peps/pep-3106/ +RedirectTemp /pep3107 https://www.python.org/dev/peps/pep-3107/ +RedirectTemp /pep3115 https://www.python.org/dev/peps/pep-3115/ +RedirectTemp /pep3118 https://www.python.org/dev/peps/pep-3118/ +RedirectTemp /pep3119 https://www.python.org/dev/peps/pep-3119/ +RedirectTemp /pep3129 https://www.python.org/dev/peps/pep-3129/ +RedirectTemp /pep3132 https://www.python.org/dev/peps/pep-3132/ +RedirectTemp /pep3141 https://www.python.org/dev/peps/pep-3141/ +RedirectTemp /pep3148 https://www.python.org/dev/peps/pep-3148/ +RedirectTemp /pep3155 https://www.python.org/dev/peps/pep-3155/ +RedirectTemp /pep3333 https://www.python.org/dev/peps/pep-3333/ + +# Remaining URLs by chapter + +############################################################ p +RedirectTemp /p-1 https://mail.python.org/pipermail/python-list/2002-December/134521.html +RedirectTemp /p-2 https://docs.python.org/3.10/tutorial/ +RedirectTemp /p-3 https://docs.python.org/3/tutorial/ +RedirectTemp /p-4 https://www.oreilly.com/library/view/fluent-python-2nd/9781492056348/ +RedirectTemp /p-5 https://www.oreilly.com/online-learning/try-now.html +RedirectTemp /p-6 https://www.oreilly.com/library/view/fluent-python-2nd/9781492056348/ +RedirectTemp /p-7 https://stackoverflow.com/users/95810/alex-martelli +RedirectTemp /p-8 https://pythonpro.com.br +RedirectTemp /p-9 https://groups.google.com/g/python-brasil +RedirectTemp /p-10 https://www.coffeelab.com.br/ +RedirectTemp /p-11 https://garoa.net.br/wiki/P%C3%A1gina_principal +############################################################ a +RedirectTemp /a-1 https://groups.google.com/forum/#!topic/python-tulip/Y4bhLNbKs74 +RedirectTemp /a-2 https://docs.python.org/3/library/asyncio-eventloop.html#executor +RedirectTemp /a-3 https://www.youtube.com/watch?v=x-kB2o8sd5c +RedirectTemp /a-4 https://www.youtube.com/watch?v=OSGv2VnC0go +RedirectTemp /a-5 https://mail.python.org/pipermail/python-ideas/2015-March/032557.html +RedirectTemp /a-6 https://pypi.org/project/pep8/ +RedirectTemp /a-7 https://pypi.org/project/flake8/ +RedirectTemp /a-8 https://pypi.org/project/pyflakes/ +RedirectTemp /a-9 https://pypi.org/project/mccabe/ +RedirectTemp /a-10 https://google.github.io/styleguide/pyguide.html +RedirectTemp /a-11 https://flask.palletsprojects.com/en/1.1.x/styleguide/ +RedirectTemp /a-12 https://docs.python-guide.org/ +RedirectTemp /a-13 https://david.goodger.org/projects/pycon/2007/idiomatic/handout.html +RedirectTemp /a-14 https://docs.mongodb.com/manual/about/#about-the-documentation-process +RedirectTemp /a-15 https://blog.startifact.com/posts/older/what-is-pythonic.html +RedirectTemp /a-16 https://mail.python.org/pipermail/tutor/2003-October/thread.html#25930 +RedirectTemp /a-17 https://mail.python.org/pipermail/python-list/2003-April/192027.html +RedirectTemp /a-18 https://www.python.org/doc/essays/ +############################################################ 01 +RedirectTemp /1-1 http://hugunin.net/story_of_jython.html +RedirectTemp /1-2 https://www.oreilly.com/library/view/jython-essentials/9781449397364/ +RedirectTemp /1-3 https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers +RedirectTemp /1-4 https://docs.python.org/3.10/library/string.html#format-string-syntax +RedirectTemp /1-5 https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr +RedirectTemp /1-6 https://docs.python.org/3/library/stdtypes.html#truth +RedirectTemp /1-7 https://docs.python.org/3/tutorial/controlflow.html#unpacking-argument-lists +RedirectTemp /1-8 https://www.python.org/doc/humor/#the-zen-of-python +RedirectTemp /1-9 https://stackoverflow.com/users/95810/alex-martelli +RedirectTemp /1-10 https://en.wikipedia.org/wiki/Object_model +RedirectTemp /1-11 https://www.dourish.com/goodies/jargon.html +RedirectTemp /1-12 https://zopeinterface.readthedocs.io/en/latest/ +RedirectTemp /1-13 https://plone.org/ +############################################################ 02 +RedirectTemp /2-1 https://github.com/fluentpython/example-code-2e/blob/master/02-array-seq/listcomp_speed.py +RedirectTemp /2-2 https://www.python.org/dev/peps/pep-3132/ +RedirectTemp /2-3 https://stackoverflow.com/questions/68630/are-tuples-more-efficient-than-lists-in-python/22140115#22140115 +RedirectTemp /2-4 https://docs.python.org/3/whatsnew/3.5.html#pep-448-additional-unpacking-generalizations +RedirectTemp /2-5 https://docs.python.org/3/whatsnew/3.5.html#pep-448-additional-unpacking-generalizations +RedirectTemp /2-6 https://docs.python.org/3.10/whatsnew/3.10.html#pep-634-structural-pattern-matching +RedirectTemp /2-7 https://docs.python.org/3.10/whatsnew/3.10.html +RedirectTemp /2-8 https://en.wikipedia.org/wiki/Switch_statement#Fallthrough +RedirectTemp /2-9 https://en.wikipedia.org/wiki/Dangling_else +RedirectTemp /2-10 https://github.com/gvanrossum/patma/blob/3ece6444ef70122876fd9f0099eb9490a2d630df/EXAMPLES.md#case-6-a-very-deep-iterable-and-type-match-with-extraction +RedirectTemp /2-11 https://github.com/fluentpython/lispy/blob/main/original/norvig/lis.py +RedirectTemp /2-12 https://norvig.com/lispy.html +RedirectTemp /2-13 https://numpy.org/doc/stable/user/quickstart.html#indexing-slicing-and-iterating +RedirectTemp /2-14 https://pythontutor.com/ +RedirectTemp /2-15 https://en.wikipedia.org/wiki/Fluent_interface +RedirectTemp /2-16 https://docs.python.org/3/library/bisect.html#bisect.insort +RedirectTemp /2-17 https://stackoverflow.com/questions/4845418/when-should-a-memoryview-be-used/ +RedirectTemp /2-18 https://www.fluentpython.com/extra/parsing-binary-struct/ +RedirectTemp /2-19 http://www.netlib.org +RedirectTemp /2-20 https://pandas.pydata.org/ +RedirectTemp /2-21 https://scikit-learn.org/stable/ +RedirectTemp /2-22 https://docs.python.org/3/howto/sorting.html +RedirectTemp /2-23 https://www.python.org/dev/peps/pep-3132/ +RedirectTemp /2-24 https://bugs.python.org/issue2292 +RedirectTemp /2-25 https://docs.python.org/3.10/whatsnew/3.10.html#pep-634-structural-pattern-matching +RedirectTemp /2-26 https://docs.python.org/3.10/whatsnew/3.10.html +RedirectTemp /2-27 https://www.python.org/dev/peps/pep-0636/#appendix-a-quick-intro +RedirectTemp /2-28 https://eli.thegreenplace.net/2011/11/28/less-copies-in-python-with-the-buffer-protocol-and-memoryviews/ +RedirectTemp /2-29 https://jakevdp.github.io/PythonDataScienceHandbook/ +RedirectTemp /2-30 https://www.oreilly.com/library/view/python-for-data/9781491957653/ +RedirectTemp /2-31 https://www.labri.fr/perso/nrougier/from-python-to-numpy/ +RedirectTemp /2-32 https://www.cs.utexas.edu/users/EWD/transcriptions/EWD08xx/EWD831.html +RedirectTemp /2-33 http://www.fonts101.com/fonts/view/Uncategorized/34398/Dijkstra +RedirectTemp /2-34 https://docs.python.org/3/reference/datamodel.html#objects-values-and-types +RedirectTemp /2-35 https://en.wikipedia.org/wiki/Timsort +RedirectTemp /2-36 http://www.groklaw.net/pdf3/OraGoogle-1202.pdf +RedirectTemp /2-37 https://www.python.org/doc/humor/#id9 +############################################################ 03 +RedirectTemp /3-1 https://www.python.org/dev/peps/pep-0584/#motivation +RedirectTemp /3-2 https://docs.python.org/3.10/c-api/typeobj.html#Py_TPFLAGS_MAPPING +RedirectTemp /3-3 https://docs.python.org/3/glossary.html#term-hashable +RedirectTemp /3-4 https://docs.python.org/3/glossary.html#term-hashable +RedirectTemp /3-5 http://www.aleax.it/Python/accu04_Relearn_Python_alex.pdf +RedirectTemp /3-6 https://github.com/pingo-io/pingo-py +RedirectTemp /3-7 https://github.com/fluentpython/example-code-2e/blob/master/03-dict-set/missing.py +RedirectTemp /3-8 https://docs.python.org/3/library/collections.html#collections.ChainMap +RedirectTemp /3-9 https://docs.python.org/3/library/collections.html#collections.Counter +RedirectTemp /3-10 https://docs.python.org/3/library/shelve.html +RedirectTemp /3-11 https://docs.python.org/3/library/dbm.html +RedirectTemp /3-12 https://docs.python.org/3/library/pickle.html +RedirectTemp /3-13 https://nedbatchelder.com/blog/202006/pickles_nine_flaws.html +RedirectTemp /3-14 https://github.com/python/cpython/blob/0bbf30e2b910bc9c5899134ae9d73a8df968da35/Lib/_collections_abc.py#L813 +RedirectTemp /3-15 https://mail.python.org/pipermail/python-dev/2015-May/140003.html +RedirectTemp /3-16 https://bugs.python.org/issue18986 +RedirectTemp /3-17 https://github.com/fluentpython/example-code-2e/blob/master/03-dict-set/transformdict.py +RedirectTemp /3-18 http://gandenberger.org/2018/03/10/ordered-dicts-vs-ordereddict/ +RedirectTemp /3-19 https://www.pypy.org/ +RedirectTemp /3-20 https://morepypy.blogspot.com/2015/01/faster-more-memory-efficient-and-more.html +RedirectTemp /3-21 https://www.npopov.com/2014/12/22/PHPs-new-hashtable-implementation.html +RedirectTemp /3-22 https://www.youtube.com/watch?v=66P5FMkWoVU +RedirectTemp /3-23 https://pyvideo.org/video/276/the-mighty-dictionary-55/ +RedirectTemp /3-24 https://www.youtube.com/watch?v=p33CVV29OG8 +RedirectTemp /3-25 https://docs.python.org/3/whatsnew/3.6.html#new-dict-implementation +RedirectTemp /3-26 https://github.com/python/cpython/blob/cf7eaa4617295747ee5646c4e2b7e7a16d7c64ab/Objects/dictobject.c +RedirectTemp /3-27 https://github.com/python/cpython/blob/cf7eaa4617295747ee5646c4e2b7e7a16d7c64ab/Objects/dictnotes.txt +RedirectTemp /3-28 https://www.youtube.com/watch?v=tGAngdU_8D8 +RedirectTemp /3-29 https://speakerdeck.com/ramalho/python-set-practice-at-pycon +RedirectTemp /3-30 https://github.com/standupdev/uintset +RedirectTemp /3-31 https://spectrum.ieee.org/hans-peter-luhn-and-the-birth-of-the-hashing-algorithm +RedirectTemp /3-32 http://www.json.org/fatfree.html +RedirectTemp /3-33 https://twitter.com/mitsuhiko/status/1229385843585974272 +############################################################ 04 +RedirectTemp /4-1 https://www.slideshare.net/fischertrav/character-encoding-unicode-how-to-with-dignity-33352863 +RedirectTemp /4-2 https://pyvideo.org/video/2625/character-encoding-and-unicode-in-python/ +RedirectTemp /4-3 https://www.fluentpython.com/extra/parsing-binary-struct/ +RedirectTemp /4-4 https://www.fluentpython.com/extra/multi-character-emojis/ +RedirectTemp /4-5 https://w3techs.com/technologies/overview/character_encoding +RedirectTemp /4-6 https://docs.python.org/3/library/codecs.html#codecs.register_error +RedirectTemp /4-7 https://docs.python.org/3/library/stdtypes.html#str.isascii +RedirectTemp /4-8 https://pypi.org/project/chardet/ +RedirectTemp /4-9 https://docs.python.org/3/library/codecs.html#encodings-and-unicode +RedirectTemp /4-10 https://nedbatchelder.com/text/unipain/unipain.html +RedirectTemp /4-11 https://devblogs.microsoft.com/commandline/windows-command-line-unicode-and-utf-8-output-text-buffer/ +RedirectTemp /4-12 https://docs.python.org/3/using/cmdline.html#envvar-PYTHONIOENCODING +RedirectTemp /4-13 https://docs.python.org/3/using/cmdline.html#envvar-PYTHONLEGACYWINDOWSSTDIO +RedirectTemp /4-14 https://docs.python.org/3/library/locale.html#locale.getpreferredencoding +RedirectTemp /4-15 http://www.w3.org/TR/charmod-norm/ +RedirectTemp /4-16 https://docs.python.org/3/library/locale.html?highlight=strxfrm#locale.strxfrm +RedirectTemp /4-17 https://pypi.org/project/pyuca/ +RedirectTemp /4-18 https://github.com/jtauber/pyuca +RedirectTemp /4-19 http://www.unicode.org/Public/UCA/6.3.0/allkeys.txt +RedirectTemp /4-20 https://pypi.org/project/PyICU/ +RedirectTemp /4-21 https://docs.python.org/3.10/library/stdtypes.html#str.isalpha +RedirectTemp /4-22 https://en.wikipedia.org/wiki/Unicode_character_property#General_Category +RedirectTemp /4-23 https://en.wikipedia.org/wiki/Unicode_character_property +RedirectTemp /4-24 https://github.com/microsoft/terminal +RedirectTemp /4-25 https://docs.python.org/3/library/unicodedata.html +RedirectTemp /4-26 https://docs.python.org/3/reference/lexical_analysis.html#string-literal-concatenation +RedirectTemp /4-27 https://docs.python.org/3/library/re.html +RedirectTemp /4-28 https://nedbatchelder.com/text/unipain.html +RedirectTemp /4-29 https://www.slideshare.net/fischertrav/character-encoding-unicode-how-to-with-dignity-33352863 +RedirectTemp /4-30 https://pyvideo.org/video/2625/character-encoding-and-unicode-in-python/ +RedirectTemp /4-31 https://regebro.wordpress.com/2011/03/23/unconfusing-unicode-what-is-unicode/ +RedirectTemp /4-32 https://docs.python.org/3/howto/unicode.html +RedirectTemp /4-33 https://diveintopython3.net/strings.html +RedirectTemp /4-34 https://diveintopython3.net/ +RedirectTemp /4-35 https://finderiko.com/python-book +RedirectTemp /4-36 https://docs.python.org/3.0/whatsnew/3.0.html#text-vs-data-instead-of-unicode-vs-8-bit +RedirectTemp /4-37 https://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/ +RedirectTemp /4-38 http://python-notes.curiousefficiency.org/en/latest/python3/binary_protocols.html +RedirectTemp /4-39 http://python-notes.curiousefficiency.org/en/latest/python3/text_file_processing.html +RedirectTemp /4-40 https://docs.python.org/3/library/codecs.html#standard-encodings +RedirectTemp /4-41 https://github.com/python/cpython/blob/0bbf30e2b910bc9c5899134ae9d73a8df968da35/Tools/unicode/listcodecs.py +RedirectTemp /4-42 https://www.oreilly.com/library/view/unicode-explained/059610121X/ +RedirectTemp /4-43 https://www.informit.com/store/unicode-demystified-a-practical-programmers-guide-to-9780201700527 +RedirectTemp /4-44 https://unicodebook.readthedocs.io/index.html +RedirectTemp /4-45 https://www.w3.org/International/wiki/Case_folding +RedirectTemp /4-46 http://www.w3.org/TR/charmod-norm/ +RedirectTemp /4-47 http://unicode.org/reports/tr15/ +RedirectTemp /4-48 http://www.unicode.org/faq/normalization.html +RedirectTemp /4-49 http://www.unicode.org/ +RedirectTemp /4-50 http://www.macchiato.com/unicode/nfc-faq +RedirectTemp /4-51 https://stories.moma.org/the-original-emoji-set-has-been-added-to-the-museum-of-modern-arts-collection-c6060e141f61?gi=c403f5a840a5 +RedirectTemp /4-52 https://emojipedia.org/ +RedirectTemp /4-53 https://blog.emojipedia.org/correcting-the-record-on-the-first-emoji-set/ +RedirectTemp /4-54 http://emojitracker.com/ +RedirectTemp /4-55 http://www.unicode.org/glossary/#plain_text +RedirectTemp /4-56 http://www.methods.co.nz/asciidoc/ +RedirectTemp /4-57 https://atlas.oreilly.com/ +############################################################ 05 +RedirectTemp /5-1 https://docs.python.org/3/library/typing.html#typing.TypedDict +RedirectTemp /5-2 https://docs.python.org/3.10/library/inspect.html#inspect.get_annotations +RedirectTemp /5-3 https://docs.python.org/3/library/typing.html#typing.get_type_hints +RedirectTemp /5-4 https://docs.python.org/3.8/library/collections.html#collections.somenamedtuple._asdict +RedirectTemp /5-5 https://www.jetbrains.com/pycharm/ +RedirectTemp /5-6 https://www.python.org/dev/peps/pep-0484/#acceptable-type-hints +RedirectTemp /5-7 https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass +RedirectTemp /5-8 https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass +RedirectTemp /5-9 https://docs.python.org/3/library/dataclasses.html +RedirectTemp /5-10 https://docs.python.org/3/library/dataclasses.html#inheritance +RedirectTemp /5-11 https://www.python.org/dev/peps/pep-0526/#class-and-instance-variable-annotations +RedirectTemp /5-12 https://dublincore.org/specifications/dublin-core/ +RedirectTemp /5-13 https://en.wikipedia.org/wiki/Dublin_Core +RedirectTemp /5-14 https://martinfowler.com/bliki/CodeSmell.html +RedirectTemp /5-15 https://martinfowler.com/books/refactoring.html +RedirectTemp /5-16 https://www.python.org/dev/peps/pep-0634/#class-patterns +RedirectTemp /5-17 https://docs.python.org/3/library/dataclasses.html +RedirectTemp /5-18 https://www.python.org/dev/peps/pep-0557/#id47 +RedirectTemp /5-19 https://www.python.org/dev/peps/pep-0557/#id48 +RedirectTemp /5-20 https://www.python.org/dev/peps/pep-0557/#id33 +RedirectTemp /5-21 https://realpython.com +RedirectTemp /5-22 https://realpython.com/python-data-classes/ +RedirectTemp /5-23 https://www.youtube.com/watch?v=T-TwcmT6Rcw +RedirectTemp /5-24 https://www.attrs.org/en/stable/ +RedirectTemp /5-25 https://glyph.twistedmatrix.com/2016/08/attrs.html +RedirectTemp /5-26 https://www.attrs.org/en/stable/why.html +RedirectTemp /5-27 https://github.com/dabeaz/cluegen +RedirectTemp /5-28 https://refactoring.guru/ +RedirectTemp /5-29 https://refactoring.guru/smells/data-class +RedirectTemp /5-30 https://web.archive.org/web/20190204130328/http://catb.org/esr/jargon/html/G/Guido.html +RedirectTemp /5-31 https://web.archive.org/web/20190211161610/http://catb.org/esr/jargon/html/index.html +RedirectTemp /5-32 https://www.attrs.org/en/stable/ +############################################################ 06 +RedirectTemp /6-1 https://www.olin.edu/faculty/profile/lynn-andrea-stein/ +RedirectTemp /6-2 https://docs.python.org/3/reference/datamodel.html#objects-values-and-types +RedirectTemp /6-3 https://pythontutor.com/ +RedirectTemp /6-4 https://docs.python.org/3/library/copy.html +RedirectTemp /6-5 https://en.wikipedia.org/wiki/Principle_of_least_astonishment +RedirectTemp /6-6 https://docs.python.org/3/reference/datamodel.html#object.%5C_%5C_del__ +RedirectTemp /6-7 https://emptysqua.re/blog/pypy-garbage-collection-and-a-deadlock/ +RedirectTemp /6-8 https://www.youtube.com/watch?v=HHFCFJSPWrI&feature=youtu.be +RedirectTemp /6-9 http://pymotw.com/3/copy/ +RedirectTemp /6-10 http://pymotw.com/3/weakref/ +RedirectTemp /6-11 https://docs.python.org/3/library/gc.html +RedirectTemp /6-12 https://devguide.python.org/garbage_collector/ +RedirectTemp /6-13 https://devguide.python.org/ +RedirectTemp /6-14 https://www.python.org/dev/peps/pep-0442/ +RedirectTemp /6-15 https://en.wikipedia.org/wiki/String_interning +RedirectTemp /6-16 https://en.wikipedia.org/wiki/Haddocks%27_Eyes +RedirectTemp /6-17 https://thp.io/2012/python-gc/python_gc_final_2012-01-22.pdf +############################################################ 07 +RedirectTemp /7-1 http://python-history.blogspot.com/2009/04/origins-of-pythons-functional-features.html +RedirectTemp /7-2 https://www.fluentpython.com/extra/function-introspection/ +RedirectTemp /7-3 https://docs.python.org/3/library/functions.html#map +RedirectTemp /7-4 https://en.wikipedia.org/wiki/Functional_programming +RedirectTemp /7-5 https://docs.python.org/3/howto/functional.html +RedirectTemp /7-6 https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy +RedirectTemp /7-7 https://docs.python.org/3/whatsnew/3.8.html#positional-only-parameters +RedirectTemp /7-8 https://docs.python.org/3/whatsnew/3.8.html#positional-only-parameters +RedirectTemp /7-9 https://github.com/python/cpython/blob/0bbf30e2b910bc9c5899134ae9d73a8df968da35/Lib/functools.py#L341 +RedirectTemp /7-10 https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy +RedirectTemp /7-11 https://docs.python.org/3/howto/functional.html +RedirectTemp /7-12 https://stackoverflow.com/questions/3252228/python-why-is-functools-partial-necessary +RedirectTemp /7-13 https://speakerdeck.com/ramalho/beyond-paradigms-berlin-edition +RedirectTemp /7-14 https://www.youtube.com/watch?v=bF3a2VYXxa0 +RedirectTemp /7-15 http://cs.brown.edu/~sk/Publications/Papers/Published/sk-teach-pl-post-linnaean/ +RedirectTemp /7-16 http://python-history.blogspot.com/2009/04/origins-of-pythons-functional-features.html +RedirectTemp /7-17 https://raw.githubusercontent.com/python/cpython/main/Misc/HISTORY +RedirectTemp /7-18 http://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html +############################################################ 08 +RedirectTemp /8-1 https://www.python.org/dev/peps/pep-0484/#non-goals +RedirectTemp /8-2 https://github.com/python/typing/issues/182 +RedirectTemp /8-3 https://github.com/python/mypy/issues/731 +RedirectTemp /8-4 https://github.com/google/pytype +RedirectTemp /8-5 https://github.com/Microsoft/pyright +RedirectTemp /8-6 https://pyre-check.org/ +RedirectTemp /8-7 https://mypy.readthedocs.io/en/stable/introduction.html +RedirectTemp /8-8 https://mypy.readthedocs.io/en/stable/config_file.html +RedirectTemp /8-9 https://pypi.org/project/flake8/ +RedirectTemp /8-10 https://pypi.org/project/blue/ +RedirectTemp /8-11 https://pypi.org/project/black/ +RedirectTemp /8-12 https://wefearchange.org/2020/11/steeringcouncil.rst.html +RedirectTemp /8-13 https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes +RedirectTemp /8-14 https://en.wikipedia.org/wiki/Barbara_Liskov +RedirectTemp /8-15 https://en.wikipedia.org/wiki/Behavioral_subtyping +RedirectTemp /8-16 https://www.python.org/dev/peps/pep-0585/#implementation +RedirectTemp /8-17 https://docs.python.org/3/library/typing.html#module-contents +RedirectTemp /8-18 https://en.wikipedia.org/wiki/Geohash +RedirectTemp /8-19 https://en.wikipedia.org/wiki/Inverted_index +RedirectTemp /8-20 https://docs.python.org/3/library/typing.html#typing.List +RedirectTemp /8-21 https://docs.python.org/3/library/typing.html#typing.Dict +RedirectTemp /8-22 https://docs.python.org/3/library/typing.html#typing.Set +RedirectTemp /8-23 https://www.python.org/dev/peps/pep-0585/#implementation +RedirectTemp /8-24 https://docs.python.org/3/library/numbers.html +RedirectTemp /8-25 https://docs.python.org/3/library/typing.html#typing.List +RedirectTemp /8-26 https://github.com/python/typeshed +RedirectTemp /8-27 https://github.com/python/typeshed/blob/66cd36268a6a667714efaa27198a41d0d7f89477/stdlib/2and3/math.pyi#L45 +RedirectTemp /8-28 https://docs.python.org/3/library/statistics.html#statistics.mode +RedirectTemp /8-29 https://github.com/python/cpython/blob/822efa5695b5ba6c2316c1400e4e9ec2546f7ea5/Lib/statistics.py#L534 +RedirectTemp /8-30 https://github.com/python/typeshed/blob/e1e99245bb46223928eba68d4fc74962240ba5b4/stdlib/3/statistics.pyi +RedirectTemp /8-31 https://docs.python.org/3/library/statistics.html#statistics.mode +RedirectTemp /8-32 https://github.com/python/typeshed/blob/a8834fcd46339e17fc8add82b5803a1ce53d3d60/stdlib/3/statistics.pyi#L32 +RedirectTemp /8-33 https://mail.python.org/archives/list/python-dev@python.org/thread/CLVXXPQ2T2LQ5MP2Y53VVQFCXYWQJHKZ/ +RedirectTemp /8-34 https://docs.python.org/3/library/typing.html#typing.Callable +RedirectTemp /8-35 https://pypi.org/project/blue/ +RedirectTemp /8-36 https://www.python.org/dev/peps/pep-0484/#id38 +RedirectTemp /8-37 https://docs.google.com/document/d/1aXs1tpwzPjW9MdsG5dI7clNFyYayFBkcXwRDo-qvbIk/preview +RedirectTemp /8-38 https://www.oreilly.com/library/view/the-best-software/9781590595008/ +RedirectTemp /8-39 https://www.youtube.com/watch?v=YFexUDjHO6w +RedirectTemp /8-40 https://www.youtube.com/watch?v=YFexUDjHO6w&t=13m40s +RedirectTemp /8-41 https://bernat.tech/posts/the-state-of-type-hints-in-python/ +RedirectTemp /8-42 https://realpython.com/python-type-checking/ +RedirectTemp /8-43 https://cjolowicz.github.io/posts/hypermodern-python-04-typing/ +RedirectTemp /8-44 https://mypy.readthedocs.io/en/stable/index.html +RedirectTemp /8-45 https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html +RedirectTemp /8-46 https://mypy.readthedocs.io/en/stable/common_issues.html +RedirectTemp /8-47 https://github.com/typeddjango/awesome-python-typing +RedirectTemp /8-48 https://docs.python.org/3/library/functions.html#max +RedirectTemp /8-49 https://en.wikipedia.org/wiki/Linguistic_relativity +RedirectTemp /8-50 https://pypistats.org/top +RedirectTemp /8-51 https://github.com/psf/requests/issues/3855 +RedirectTemp /8-52 https://lwn.net/Articles/643399/ +RedirectTemp /8-53 https://docs.python-requests.org/en/master/api/#requests.request +RedirectTemp /8-54 https://queue.acm.org/detail.cfm?id=1039523 +############################################################ 09 +RedirectTemp /9-1 https://docs.python.org/3/library/dis.html +RedirectTemp /9-2 https://en.wikipedia.org/wiki/Memoization +RedirectTemp /9-3 https://numpy.org/doc/stable/user/basics.types.html +RedirectTemp /9-4 https://docs.python.org/3/library/functools.html#functools.singledispatch +RedirectTemp /9-5 https://github.com/GrahamDumpleton/wrapt/blob/develop/blog/README.md +RedirectTemp /9-6 https://github.com/GrahamDumpleton/wrapt/blob/develop/blog/01-how-you-implemented-your-python-decorator-is-wrong.md +RedirectTemp /9-7 https://wrapt.readthedocs.io/en/latest/ +RedirectTemp /9-8 https://www.oreilly.com/library/view/python-cookbook-3rd/9781449357337/ch09.html +RedirectTemp /9-9 https://pypi.org/project/decorator/ +RedirectTemp /9-10 https://wiki.python.org/moin/PythonDecoratorLibrary +RedirectTemp /9-11 http://web.archive.org/web/20201109032203/http://effbot.org/zone/closure.htm +RedirectTemp /9-12 https://www.python.org/dev/peps/pep-3104/ +RedirectTemp /9-13 https://www.python.org/dev/peps/pep-0227/ +RedirectTemp /9-14 https://www.python.org/dev/peps/pep-0443/ +RedirectTemp /9-15 https://www.artima.com/weblogs/viewpost.jsp?thread=101605 +RedirectTemp /9-16 https://reg.readthedocs.io/en/latest/ +RedirectTemp /9-17 https://morepath.readthedocs.io/en/latest/ +RedirectTemp /9-18 https://www.gnu.org/software/emacs/manual/html_node/elisp/Dynamic-Binding.html +RedirectTemp /9-19 http://www.paulgraham.com/rootsoflisp.html +RedirectTemp /9-20 http://www-formal.stanford.edu/jmc/recursive/recursive.html +RedirectTemp /9-21 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this +############################################################ 10 +RedirectTemp /10-1 https://en.wikipedia.org/wiki/Software_design_pattern +RedirectTemp /10-2 https://en.wikipedia.org/wiki/Iterator_pattern +RedirectTemp /10-3 https://github.com/python/mypy/issues/9397 +RedirectTemp /10-4 http://www.norvig.com/design-patterns/index.htm +RedirectTemp /10-5 https://pyvideo.org/video/1110/python-design-patterns/ +RedirectTemp /10-6 http://www.aleax.it/gdd_pydp.pdf +RedirectTemp /10-7 https://perl.plover.com/yak/design/ +RedirectTemp /10-8 https://en.wikipedia.org/wiki/Turtles_all_the_way_down +############################################################ 11 +RedirectTemp /11-1 https://blog.startifact.com/posts/older/what-is-pythonic.html +RedirectTemp /11-2 https://julien.danjou.info/guide-python-static-class-abstract-methods/ +RedirectTemp /11-3 https://docs.python.org/3/library/string.html#formatspec +RedirectTemp /11-4 https://docs.python.org/3/reference/lexical_analysis.html#f-strings +RedirectTemp /11-5 https://docs.python.org/3/library/string.html#format-string-syntax +RedirectTemp /11-6 https://docs.python.org/3/library/string.html#formatspec +RedirectTemp /11-7 https://docs.python.org/3/reference/datamodel.html#object.__hash__ +RedirectTemp /11-8 https://web.archive.org/web/20161025185040/http://pythonpaste.org/StyleGuide.html +RedirectTemp /11-9 https://docs.python.org/3/tutorial/modules.html#more-on-modules +RedirectTemp /11-10 https://docs.python.org/3/library/gettext.html#gettext.NullTranslations +RedirectTemp /11-11 https://github.com/fluentpython/example-code-2e/blob/master/11-pythonic-obj/mem_test.py +RedirectTemp /11-12 https://docs.python.org/3/reference/datamodel.html#basic-customization +RedirectTemp /11-13 http://esug.org/data/HistoricalDocuments/TheSmalltalkReport/ST07/04wo.pdf +RedirectTemp /11-14 https://www.artima.com/articles/the-simplest-thing-that-could-possibly-work#part3 +RedirectTemp /11-15 https://docs.oracle.com/javase/tutorial/essential/environment/security.html +RedirectTemp /11-16 https://github.com/fluentpython/example-code-2e/blob/master/11-pythonic-obj/private/Expose.java +RedirectTemp /11-17 https://docs.oracle.com/javase/tutorial/essential/environment/security.html +############################################################ 12 +RedirectTemp /12-1 https://en.wikipedia.org/wiki/Vector_space_model +RedirectTemp /12-2 https://pypi.org/project/gensim/ +RedirectTemp /12-3 https://docs.python.org/3/library/functions.html#enumerate +RedirectTemp /12-4 https://mathworld.wolfram.com/Hypersphere.html +RedirectTemp /12-5 https://en.wikipedia.org/wiki/Fold_(higher-order_function) +RedirectTemp /12-6 https://docs.python.org/2.5/whatsnew/pep-357.html +RedirectTemp /12-7 https://docs.python.org/3/reference/datamodel.html#special-method-names +RedirectTemp /12-8 https://en.wikipedia.org/wiki/KISS_principle +RedirectTemp /12-9 https://mail.python.org/pipermail/python-list/2000-July/046184.html +RedirectTemp /12-10 https://en.wikipedia.org/wiki/Duck_typing +RedirectTemp /12-11 https://mail.python.org/mailman/listinfo/python-list +RedirectTemp /12-12 https://mail.python.org/pipermail/python-list/2003-April/218568.html +############################################################ 13 +RedirectTemp /13-1 https://docs.python.org/3/c-api/index.html +RedirectTemp /13-2 https://docs.python.org/3/c-api/sequence.html +RedirectTemp /13-3 https://github.com/python/cpython/blob/31ceccb2c77854893f3a754aca04bedd74bedb10/Lib/_collections_abc.py#L870 +RedirectTemp /13-4 https://en.wikipedia.org/wiki/Monkey_patch +RedirectTemp /13-5 https://www.gevent.org/api/gevent.monkey.html +RedirectTemp /13-6 https://docs.python.org/3/library/random.html#random.shuffle +RedirectTemp /13-7 https://docs.python.org/3/reference/datamodel.html#emulating-container-types +RedirectTemp /13-8 https://docs.python.org/3/library/collections.html#collections.namedtuple +RedirectTemp /13-9 https://github.com/python/typeshed/blob/24afb531ffd07083d6a74be917342195062f7277/stdlib/collections/__init__.pyi +RedirectTemp /13-10 https://docs.python.org/3/glossary.html#term-abstract-base-class +RedirectTemp /13-11 https://en.wikipedia.org/wiki/Duck_typing#History +RedirectTemp /13-12 http://ptgmedia.pearsoncmg.com/images/020163371x/items/item33.html +RedirectTemp /13-13 https://docs.python.org/3/library/bisect.html#bisect.bisect +RedirectTemp /13-14 https://github.com/python/cpython/blob/main/Lib/_collections_abc.py +RedirectTemp /13-15 https://github.com/python/cpython/blob/main/Lib/abc.py +RedirectTemp /13-16 https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes +RedirectTemp /13-17 https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable +RedirectTemp /13-18 https://docs.python.org/3/library/abc.html +RedirectTemp /13-19 https://docs.python.org/dev/library/abc.html#abc.abstractmethod +RedirectTemp /13-20 https://docs.python.org/dev/library/abc.html +RedirectTemp /13-21 https://docs.python.org/3/library/os.html#os.urandom +RedirectTemp /13-22 https://github.com/python/mypy/issues/2922 +RedirectTemp /13-23 https://docs.python.org/3/library/stdtypes.html#truth +RedirectTemp /13-24 https://github.com/python/cpython/blob/0bbf30e2b910bc9c5899134ae9d73a8df968da35/Lib/_collections_abc.py +RedirectTemp /13-25 https://github.com/python/cpython/blob/0fbddb14dc03f61738af01af88e7d8aa8df07336/Lib/_collections_abc.py#L369 +RedirectTemp /13-26 https://github.com/python/cpython/blob/c0a9afe2ac1820409e6173bd1893ebee2cf50270/Lib/abc.py#L196 +RedirectTemp /13-27 https://bugs.python.org/issue31333 +RedirectTemp /13-28 https://github.com/python/cpython/blob/3635388f52b42e5280229104747962117104c453/Modules/_abc.c#L605 +RedirectTemp /13-29 https://github.com/python/cpython/blob/0fbddb14dc03f61738af01af88e7d8aa8df07336/Lib/_collections_abc.py#L881 +RedirectTemp /13-30 https://docs.python.org/3/library/typing.html#protocols +RedirectTemp /13-31 https://github.com/python/cpython/blob/3635388f52b42e5280229104747962117104c453/Lib/typing.py#L1751 +RedirectTemp /13-32 https://mail.python.org/archives/list/python-dev@python.org/thread/CLVXXPQ2T2LQ5MP2Y53VVQFCXYWQJHKZ/ +RedirectTemp /13-33 https://martinfowler.com/bliki/RoleInterface.html +RedirectTemp /13-34 https://en.wikipedia.org/wiki/Interface_segregation_principle +RedirectTemp /13-35 https://github.com/python/typeshed/blob/master/CONTRIBUTING.md +RedirectTemp /13-36 https://gist.github.com/asukakenji/ac8a05644a2e98f1d5ea8c299541fce9 +RedirectTemp /13-37 https://www.python.org/dev/peps/pep-0544/#runtime-checkable-decorator-and-narrowing-types-by-isinstance +RedirectTemp /13-38 https://www.python.org/dev/peps/pep-0544/#merging-and-extending-protocols +RedirectTemp /13-39 https://numpy.org/devdocs/user/basics.types.html +RedirectTemp /13-40 https://github.com/python/typeshed/blob/master/stdlib/statistics.pyi +RedirectTemp /13-41 https://bugs.python.org/issue41974 +RedirectTemp /13-42 https://glyph.twistedmatrix.com/2020/07/new-duck.html +RedirectTemp /13-43 https://glyph.twistedmatrix.com/2021/03/interfaces-and-protocols.html +RedirectTemp /13-44 https://plone.org/ +RedirectTemp /13-45 https://trypyramid.com/ +RedirectTemp /13-46 https://twistedmatrix.com/trac/ +RedirectTemp /13-47 https://www.artima.com/articles/contracts-in-python +RedirectTemp /13-48 https://martinfowler.com/bliki/DynamicTyping.html +RedirectTemp /13-49 https://martinfowler.com/bliki/RoleInterface.html +RedirectTemp /13-50 https://mypy.readthedocs.io/en/stable/protocols.html +RedirectTemp /13-51 https://pymotw.com/3/abc/index.html +RedirectTemp /13-52 https://www.python.org/dev/peps/pep-3119/ +RedirectTemp /13-53 https://www.python.org/dev/peps/pep-3141/ +RedirectTemp /13-54 https://docs.python.org/3/library/numbers.html +RedirectTemp /13-55 https://github.com/python/mypy/issues/3186 +RedirectTemp /13-56 https://stackoverflow.com/questions/69334475/how-to-hint-at-number-types-i-e-subclasses-of-number-not-numbers-themselv/69383462#69383462 +RedirectTemp /13-57 https://github.com/python/mypy/issues/3186 +RedirectTemp /13-58 https://martinfowler.com/articles/lean-inception/ +RedirectTemp /13-59 https://martinfowler.com +RedirectTemp /13-60 https://www.jetbrains.com/pycharm/ +RedirectTemp /13-61 https://wingware.com/ +RedirectTemp /13-62 https://code.visualstudio.com/ +############################################################ 14 +RedirectTemp /14-1 http://worrydream.com/EarlyHistoryOfSmalltalk/ +RedirectTemp /14-2 https://docs.python.org/3/tutorial/classes.html +RedirectTemp /14-3 https://docs.python.org/3/library/collections.html#ordereddict-examples-and-recipes +RedirectTemp /14-4 https://discuss.python.org/t/is-it-time-to-deprecate-unbound-super-methods/1833 +RedirectTemp /14-5 https://doc.pypy.org/en/latest/cpython_differences.html#subclasses-of-built-in-types +RedirectTemp /14-6 https://docs.python.org/3/library/collections.html +RedirectTemp /14-7 https://github.com/fluentpython/example-code-2e/blob/master/14-inheritance/strkeydict_dictsub.py +RedirectTemp /14-8 https://doc.pypy.org/en/latest/cpython_differences.html#subclasses-of-built-in-types +RedirectTemp /14-9 https://en.wikipedia.org/wiki/Breadth-first_search +RedirectTemp /14-10 https://www.python.org/download/releases/2.3/mro/ +RedirectTemp /14-11 https://github.com/fluentpython/example-code-2e/blob/master/14-inheritance/uppermixin.py +RedirectTemp /14-12 https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html +RedirectTemp /14-13 https://docs.python.org/3/library/collections.abc.html +RedirectTemp /14-14 https://github.com/python/cpython/blob/8ece98a7e418c3c68a4c61bc47a2d0931b59a889/Lib/collections/__init__.py#L1084 +RedirectTemp /14-15 https://docs.python.org/3/library/http.server.html +RedirectTemp /14-16 https://github.com/python/cpython/blob/17c23167942498296f0bdfffe52e72d53d66d693/Lib/http/server.py#L144 +RedirectTemp /14-17 https://github.com/python/cpython/blob/699ee016af5736ffc80f68359617611a22b72943/Lib/socketserver.py#L664 +RedirectTemp /14-18 https://docs.python.org/3/library/socketserver.html#socketserver.ForkingMixIn +RedirectTemp /14-19 https://docs.python.org/3/library/os.html#os.fork +RedirectTemp /14-20 https://en.wikipedia.org/wiki/POSIX +RedirectTemp /14-21 http://ccbv.co.uk/ +RedirectTemp /14-22 https://github.com/django/django/tree/main/django/views/generic +RedirectTemp /14-23 https://en.wikipedia.org/wiki/Template_method_pattern +RedirectTemp /14-24 https://docs.python.org/3/library/tkinter.html +RedirectTemp /14-25 https://docs.python.org/3/library/tkinter.ttk.html +RedirectTemp /14-26 https://docs.oracle.com/javase/10/docs/api/java/awt/package-tree.html +RedirectTemp /14-27 https://docs.oracle.com/javase/10/docs/api/javax/swing/package-tree.html +RedirectTemp /14-28 https://squeak.org/ +RedirectTemp /14-29 https://github.com/django/django/blob/b64db05b9cedd96905d637a2d824cbbf428e40e7/django/views/generic/list.py#L194 +RedirectTemp /14-30 https://github.com/python/cpython/blob/8ed183391241f0c73e7ba7f42b1d49fc02985f7b/Lib/tkinter/__init__.py#L2618 +RedirectTemp /14-31 https://docs.python.org/3/library/socketserver.html +RedirectTemp /14-32 https://docs.python.org/3/library/socketserver.html#socketserver.BaseServer +RedirectTemp /14-33 https://github.com/python/cpython/blob/699ee016af5736ffc80f68359617611a22b72943/Lib/socketserver.py#L153 +RedirectTemp /14-34 https://docs.python.org/3/library/typing.html#typing.final +RedirectTemp /14-35 https://docs.python.org/3/library/typing.html#typing.Final +RedirectTemp /14-36 https://docs.python.org/3/library/collections.abc.html +RedirectTemp /14-37 https://hynek.me/articles/python-subclassing-redux/ +RedirectTemp /14-38 https://www.oreilly.com/library/view/python-cookbook-3rd/9781449357337/ch08.html#super +RedirectTemp /14-39 https://rhettinger.wordpress.com/2011/05/26/super-considered-super/ +RedirectTemp /14-40 https://fuhm.net/super-harmful/ +RedirectTemp /14-41 https://stackoverflow.com/questions/30190185/how-to-use-super-with-one-argument/30190341#30190341 +RedirectTemp /14-42 https://www.artima.com/weblogs/viewpost.jsp?thread=246488 +RedirectTemp /14-43 https://www.artima.com/weblogs/viewpost.jsp?thread=281127 +RedirectTemp /14-44 https://www.artima.com/weblogs/viewpost.jsp?thread=246341 +RedirectTemp /14-45 https://www.artima.com/weblogs/viewpost.jsp?thread=246483 +RedirectTemp /14-46 https://www.artima.com/weblogs/viewpost.jsp?thread=236275 +RedirectTemp /14-47 https://www.artima.com/weblogs/viewpost.jsp?thread=236278 +RedirectTemp /14-48 https://www.artima.com/weblogs/viewpost.jsp?thread=237121 +RedirectTemp /14-49 https://python-patterns.guide/gang-of-four/composition-over-inheritance/ +RedirectTemp /14-50 https://python-patterns.guide/ +RedirectTemp /14-51 https://www.youtube.com/watch?v=3MNVP9-hglc +RedirectTemp /14-52 http://worrydream.com/EarlyHistoryOfSmalltalk/ +RedirectTemp /14-53 https://en.wikipedia.org/wiki/Polymorphism_(computer_science) +############################################################ 15 +RedirectTemp /15-1 https://www.youtube.com/watch?v=csL8DLXGNlU&t=92m5s +RedirectTemp /15-2 https://github.com/python/typeshed/blob/a8834fcd46339e17fc8add82b5803a1ce53d3d60/stdlib/2and3/builtins.pyi#L1434 +RedirectTemp /15-3 https://github.com/python/typeshed/blob/a8834fcd46339e17fc8add82b5803a1ce53d3d60/stdlib/2and3/builtins.pyi +RedirectTemp /15-4 https://twitter.com/gwidion/status/1265384692464967680 +RedirectTemp /15-5 https://pypi.org/project/pydantic/ +RedirectTemp /15-6 https://google.github.io/pytype/faq.html +RedirectTemp /15-7 https://google.github.io/pytype/faq.html +RedirectTemp /15-8 https://lxml.de/ +RedirectTemp /15-9 https://docs.python.org/3/library/xml.etree.elementtree.html +RedirectTemp /15-10 https://mypy.readthedocs.io/en/stable/common_issues.html +RedirectTemp /15-11 https://mypy.readthedocs.io/en/stable/common_issues.html#types-of-empty-collections +RedirectTemp /15-12 https://github.com/python/typing/issues/182 +RedirectTemp /15-13 https://pypi.org/project/pydantic/ +RedirectTemp /15-14 https://mypy.readthedocs.io/en/stable/type_narrowing.html#casts +RedirectTemp /15-15 https://github.com/python/cpython/blob/bee66d3cb98e740f9d8057eb7f503122052ca5d8/Lib/typing.py#L1340 +RedirectTemp /15-16 https://www.python.org/dev/peps/pep-0484/#casts +RedirectTemp /15-17 https://github.com/python/typeshed/issues/5535 +RedirectTemp /15-18 https://docs.python.org/3/library/asyncio-stream.html#tcp-echo-server-using-streams +RedirectTemp /15-19 https://github.com/python/cpython/blob/b798ab06937f8bb24b444a49dd42e11fff15e654/Lib/test/test_asyncio/test_server.py#L55 +RedirectTemp /15-20 https://en.wikipedia.org/wiki/Code_smell +RedirectTemp /15-21 https://mail.python.org/archives/list/typing-sig@python.org/message/5LCWMN2UY2UQNLC5Z47GHBZKSPZW4I63/ +RedirectTemp /15-22 https://mypy.readthedocs.io/en/stable/error_codes.html#error-codes +RedirectTemp /15-23 https://github.com/fluentpython/example-code-2e/blob/master/15-more-types/clip_annot.py +RedirectTemp /15-24 https://docs.python.org/3/library/typing.html#introspection-helpers +RedirectTemp /15-25 https://docs.python.org/3.10/library/inspect.html#inspect.get_annotations +RedirectTemp /15-26 https://www.python.org/dev/peps/pep-0563/#abstract +RedirectTemp /15-27 https://mail.python.org/archives/list/python-dev@python.org/message/ZBJ7MD6CSGM6LZAOTET7GXAVBZB7O77O/ +RedirectTemp /15-28 https://docs.python.org/3.10/howto/annotations.html +RedirectTemp /15-29 https://docs.python.org/3/library/typing.html#user-defined-generic-types +RedirectTemp /15-30 https://github.com/python/typeshed/blob/bfc83c365a0b26ab16586beac77ff16729d0e473/stdlib/builtins.pyi#L743 +RedirectTemp /15-31 https://docs.python.org/3.10/library/typing.html#typing.FrozenSet +RedirectTemp /15-32 https://docs.python.org/3.10/library/typing.html#typing.Generator +RedirectTemp /15-33 https://docs.python.org/3.10/library/typing.html#typing.AsyncGenerator +RedirectTemp /15-34 https://github.com/python/cpython/blob/46b16d0bdbb1722daed10389e27226a2370f1635/Lib/typing.py#L1786 +RedirectTemp /15-35 https://github.com/python/typeshed/blob/2a9f081abbf01134e4e04ced6a750107db904d70/stdlib/builtins.pyi#L239 +RedirectTemp /15-36 https://www.oreilly.com/library/view/robust-python/9781098100650/ +RedirectTemp /15-37 https://www.python.org/dev/peps/pep-0484/#covariance-and-contravariance +RedirectTemp /15-38 https://mypy.readthedocs.io/en/stable/generics.html#variance-of-generic-types +RedirectTemp /15-39 https://mypy.readthedocs.io/en/stable/common_issues.html#variance +RedirectTemp /15-40 https://www.artima.com/weblogs/viewpost.jsp?thread=85551 +RedirectTemp /15-41 https://dl.acm.org/action/cookieAbsent +RedirectTemp /15-42 http://bracha.org/pluggableTypesPosition.pdf +RedirectTemp /15-43 https://www.researchgate.net/publication/213886116_Static_Typing_Where_Possible_Dynamic_Typing_When_Needed_The_End_of_the_Cold_War_Between_Programming_Languages +RedirectTemp /15-44 https://www.atomickotlin.com/atomickotlin/ +RedirectTemp /15-45 https://www.informit.com/store/effective-java-9780134685991 +RedirectTemp /15-46 https://www.manning.com/books/programming-with-types +RedirectTemp /15-47 https://www.oreilly.com/library/view/programming-typescript/9781492037644/ +RedirectTemp /15-48 https://www.informit.com/store/dart-programming-language-9780321927705 +RedirectTemp /15-49 https://www.yodaiken.com/2017/09/15/bad-ideas-in-type-theory/ +RedirectTemp /15-50 https://www.yodaiken.com/2017/11/30/types-considered-harmful-ii/ +RedirectTemp /15-51 https://web.archive.org/web/20071010002142/http://weblogs.java.net/blog/arnold/archive/2005/06/generics_consid_1.html +RedirectTemp /15-52 https://github.com/python/cpython/blob/3e7ee02327db13e4337374597cdc4458ecb9e3ad/Lib/asyncio/trsock.py#L5 +RedirectTemp /15-53 https://www.python.org/dev/peps/pep-0484/#covariance-and-contravariance +############################################################ 16 +RedirectTemp /16-1 http://www.gotw.ca/publications/c_family_interview.htm +RedirectTemp /16-2 https://docs.python.org/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations +RedirectTemp /16-3 https://docs.python.org/3/reference/datamodel.html#object.__neg__ +RedirectTemp /16-4 https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#boolean-indexing +RedirectTemp /16-5 https://docs.python.org/3/library/collections.html#collections.Counter +RedirectTemp /16-6 https://docs.python.org/3/reference/datamodel.html#emulating-container-types +RedirectTemp /16-7 https://docs.python.org/3/library/numbers.html#implementing-the-arithmetic-operations +RedirectTemp /16-8 https://www.fluentpython.com/lingo/#fail-fast +RedirectTemp /16-9 https://github.com/python/cpython/blob/0bbf30e2b910bc9c5899134ae9d73a8df968da35/Objects/typeobject.c#L4598 +RedirectTemp /16-10 https://neopythonic.blogspot.com/2019/03/why-operators-are-useful.html +RedirectTemp /16-11 https://treyhunner.com/2019/03/python-deep-comparisons-and-code-readability/ +RedirectTemp /16-12 https://docs.python.org/3/library/numbers.html#implementing-the-arithmetic-operations +RedirectTemp /16-13 https://docs.python.org/3/library/pathlib.html +RedirectTemp /16-14 https://pypi.org/project/scapy/ +RedirectTemp /16-15 https://scapy.readthedocs.io/en/latest/usage.html#stacking-layers +RedirectTemp /16-16 https://docs.python.org/3/library/functools.html#functools.total_ordering +RedirectTemp /16-17 https://wiki.illinois.edu//wiki/download/attachments/273416327/ingalls.pdf +RedirectTemp /16-18 https://wiki.illinois.edu//wiki/download/attachments/273416327/double-dispatch.pdf +RedirectTemp /16-19 http://www.gotw.ca/publications/c_family_interview.htm +RedirectTemp /16-20 http://www.gotw.ca/publications/c_family_interview.htm +RedirectTemp /16-21 https://doc.rust-lang.org/std/ops/index.html +RedirectTemp /16-22 https://www.fluentpython.com/lingo/#lazy +############################################################ 17 +RedirectTemp /17-1 http://www.paulgraham.com/icad.html +RedirectTemp /17-2 https://en.wikipedia.org/wiki/Sentinel_value +RedirectTemp /17-3 https://docs.python.org/3.10/library/functions.html#iter +RedirectTemp /17-4 https://docs.python.org/3.10/library/functions.html#iter +RedirectTemp /17-5 https://github.com/python/cpython/blob/b1930bf75f276cd7ca08c4455298128d89adf7d1/Lib/_collections_abc.py#L271 +RedirectTemp /17-6 https://github.com/python/cpython/blob/main/Lib/types.py#L6 +RedirectTemp /17-7 https://en.wikipedia.org/wiki/CLU_(programming_language) +RedirectTemp /17-8 https://docs.python.org/3/glossary.html +RedirectTemp /17-9 https://docs.python.org/3/glossary.html#term-generator-iterator +RedirectTemp /17-10 https://docs.python.org/3/glossary.html#term-generator-expression +RedirectTemp /17-11 https://marc.info/?l=python-list&m=141826925106951&w=2 +RedirectTemp /17-12 https://docs.python.org/3/library/os.html#os.walk +RedirectTemp /17-13 https://docs.python.org/3/library/itertools.html +RedirectTemp /17-14 https://docs.python.org/3/library/exceptions.html#exception-hierarchy +RedirectTemp /17-15 https://en.wikipedia.org/wiki/Depth-first_search +RedirectTemp /17-16 https://docs.python.org/3.10/library/typing.html#typing.TypeAlias +RedirectTemp /17-17 https://docs.python.org/3/library/typing.html#typing.Generator +RedirectTemp /17-18 http://www.dabeaz.com/coroutines/Coroutines.pdf +RedirectTemp /17-19 http://www.dabeaz.com/coroutines/Coroutines.pdf +RedirectTemp /17-20 https://mail.python.org/pipermail/python-ideas/2009-April/003841.html +RedirectTemp /17-21 https://mail.python.org/pipermail/python-ideas/2009-April/003912.html +RedirectTemp /17-22 https://docs.python.org/3/library/exceptions.html#StopIteration +RedirectTemp /17-23 https://docs.python.org/3/reference/expressions.html#yield-expressions +RedirectTemp /17-24 https://docs.python.org/3/reference/index.html +RedirectTemp /17-25 https://github.com/python/cpython/blob/6f743e7a4da904f61dfa84cc7d7385e4dcc79ac5/Lib/typing.py#L2060 +RedirectTemp /17-26 http://catb.org/~esr/jargon/html/G/grok.html +RedirectTemp /17-27 https://docs.python.org/3/reference/expressions.html#yieldexpr +RedirectTemp /17-28 https://docs.python.org/3/library/itertools.html +RedirectTemp /17-29 https://docs.python.org/3/library/itertools.html#itertools-recipes +RedirectTemp /17-30 https://more-itertools.readthedocs.io/en/stable/index.html +RedirectTemp /17-31 https://rittau.org/2006/11/java-iterators-are-not-iterable/ +RedirectTemp /17-32 https://docs.python.org/3/whatsnew/3.3.html#pep-380-syntax-for-delegating-to-a-subgenerator +RedirectTemp /17-33 http://www.dabeaz.com/generators/ +RedirectTemp /17-34 http://www.dabeaz.com/coroutines/ +RedirectTemp /17-35 https://archive.org/details/pyvideo_213___pycon-2009-a-curious-course-on-coroutines-and-concurrency-part-1-of-3 +RedirectTemp /17-36 https://archive.org/details/pyvideo_215___pycon-2009-a-curious-course-on-coroutines-and-concurrency-part-2-of-3 +RedirectTemp /17-37 https://archive.org/details/pyvideo_214___pycon-2009-a-curious-course-on-coroutines-and-concurrency-part-3-of-3 +RedirectTemp /17-38 http://www.dabeaz.com/finalgenerator/ +RedirectTemp /17-39 https://web.archive.org/web/20200218150637/http://seriously.dontusethiscode.com/2013/05/01/greedy-coroutine.html +RedirectTemp /17-40 https://effectivepython.com/ +RedirectTemp /17-41 https://effectivepython.com/2015/03/10/consider-coroutines-to-run-many-functions-concurrently +RedirectTemp /17-42 https://en.wikipedia.org/wiki/Conway's_Game_of_Life +RedirectTemp /17-43 https://gist.github.com/ramalho/da5590bc38c973408839 +RedirectTemp /17-44 https://gist.github.com/ramalho/da5590bc38c973408839 +RedirectTemp /17-45 https://journal.code4lib.org/articles/4893 +RedirectTemp /17-46 https://github.com/fluentpython/isis2json +RedirectTemp /17-47 https://github.com/fluentpython/isis2json/blob/master/README.rst +############################################################ 18 +RedirectTemp /18-1 https://pyvideo.org/video/1669/keynote-3/ +RedirectTemp /18-2 https://docs.python.org/3/library/sqlite3.html#using-the-connection-as-a-context-manager +RedirectTemp /18-3 https://docs.python.org/3/library/threading.html#using-locks-conditions-and-semaphores-in-the-with-statement +RedirectTemp /18-4 https://docs.python.org/3/library/decimal.html#decimal.localcontext +RedirectTemp /18-5 https://docs.python.org/3/library/unittest.mock.html#patch +RedirectTemp /18-6 https://docs.python.org/3/library/contextlib.html#contextlib.redirect_stdout +RedirectTemp /18-7 https://docs.python.org/3/library/sys.html#sys.exc_info +RedirectTemp /18-8 https://en.wikipedia.org/wiki/LL_parser +RedirectTemp /18-9 https://docs.python.org/3/library/contextlib.html +RedirectTemp /18-10 https://github.com/python/cpython/blob/8afab2ebbc1b343cd88d058914cf622fe687a2be/Lib/contextlib.py#L123 +RedirectTemp /18-11 https://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/ +RedirectTemp /18-12 https://docs.python.org/3/library/fileinput.html#fileinput.input +RedirectTemp /18-13 https://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/ +RedirectTemp /18-14 https://en.wikipedia.org/wiki/Euclidean_algorithm +RedirectTemp /18-15 https://github.com/fluentpython/example-code-2e/tree/master/18-with-match/lispy/py3.10/ +RedirectTemp /18-16 https://github.com/fluentpython/example-code-2e/blob/master/18-with-match/lispy/original/lispy.py +RedirectTemp /18-17 https://github.com/fluentpython/example-code-2e/blob/6527037ae7319ba370a1ee2d9fe79214d0ed9452/18-with-match/lispy/py3.10/lis.py#L35 +RedirectTemp /18-18 https://github.com/fluentpython/example-code-2e/blob/master/18-with-match/lispy/py3.10/examples_test.py +RedirectTemp /18-19 https://github.com/python/typeshed/issues/6042 +RedirectTemp /18-20 https://github.com/fluentpython/lispy/tree/main/mylis +RedirectTemp /18-21 https://github.com/fluentpython/example-code-2e/blob/00e4741926e1b771ee7c753148b1415c0bd12e39/02-array-seq/lispy/py3.10/examples_test.py +RedirectTemp /18-22 https://mitpress.mit.edu/sites/default/files/sicp/index.html +RedirectTemp /18-23 https://github.com/fluentpython/example-code-2e/blob/master/18-with-match/lispy/py3.10/examples_test.py +RedirectTemp /18-24 https://github.com/fluentpython/example-code-2e/blob/master/18-with-match/lispy/py3.10/lis.py +RedirectTemp /18-25 https://www.python.org/dev/peps/pep-0634/#or-patterns +RedirectTemp /18-26 https://en.wikipedia.org/wiki/Lambda#Character_encodings +RedirectTemp /18-27 https://docs.python.org/3/reference/compound_stmts.html +RedirectTemp /18-28 https://docs.python.org/3/glossary.html#term-eafp +RedirectTemp /18-29 https://speakerdeck.com/pyconslides/pycon-keynote-python-is-awesome-by-raymond-hettinger?slide=21 +RedirectTemp /18-30 https://docs.python.org/3/reference/compound_stmts.html +RedirectTemp /18-31 https://stackoverflow.com/questions/16138232/is-it-a-good-practice-to-use-try-except-else-in-python +RedirectTemp /18-32 https://docs.python.org/3/library/stdtypes.html#typecontextmanager +RedirectTemp /18-33 https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers +RedirectTemp /18-34 https://speakerdeck.com/pyconslides/pycon-keynote-python-is-awesome-by-raymond-hettinger?slide=21 +RedirectTemp /18-35 https://speakerdeck.com/pyconslides/transforming-code-into-beautiful-idiomatic-python-by-raymond-hettinger-1?slide=34 +RedirectTemp /18-36 https://preshing.com/20110920/the-python-with-statement-by-example/ +RedirectTemp /18-37 https://www.rath.org/on-the-beauty-of-pythons-exitstack.html +RedirectTemp /18-38 https://norvig.com/lispy.html +RedirectTemp /18-39 https://norvig.com/lispy2.html +RedirectTemp /18-40 https://github.com/norvig/pytudes +RedirectTemp /18-41 https://github.com/fluentpython/lispy +RedirectTemp /18-42 https://racket-lang.org/ +RedirectTemp /18-43 https://pyvideo.org/video/1669/keynote-3/ +RedirectTemp /18-44 https://en.wikipedia.org/wiki/Tail_call +RedirectTemp /18-45 https://2ality.com/2015/06/tail-call-optimization.html +RedirectTemp /18-46 https://github.com/fluentpython/example-code-2e/blob/master/18-with-match/lispy/original/lis.py +RedirectTemp /18-47 https://github.com/fluentpython/example-code-2e/blob/master/18-with-match/lispy/original/lispy.py +RedirectTemp /18-48 http://neopythonic.blogspot.com/2009/04/final-words-on-tail-calls.html +RedirectTemp /18-49 https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit/ +RedirectTemp /18-50 http://kangax.github.io/compat-table/es6/ +RedirectTemp /18-51 https://world.hey.com/mgmarlow/what-happened-to-proper-tail-calls-in-javascript-5494c256 +RedirectTemp /18-52 http://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html +RedirectTemp /18-53 https://github.com/fluentpython/lispy/blob/main/mylis/mylis_2/lis.py +RedirectTemp /18-54 https://github.com/fluentpython/lispy/tree/main/mylis +############################################################ 19 +RedirectTemp /19-1 https://go.dev/blog/waza-talk +RedirectTemp /19-2 https://en.wikipedia.org/wiki/Graphics_processing_unit +RedirectTemp /19-3 https://docs.python.org/3/library/sys.html#sys.getswitchinterval +RedirectTemp /19-4 https://docs.python.org/3/library/sys.html#sys.setswitchinterval +RedirectTemp /19-5 https://en.wikipedia.org/wiki/System_call +RedirectTemp /19-6 https://mail.python.org/pipermail/python-dev/2009-October/093356.html +RedirectTemp /19-7 http://www.dabeaz.com/finalgenerator/ +RedirectTemp /19-8 https://docs.python.org/3/library/threading.html#thread-objects +RedirectTemp /19-9 https://www.pypy.org/ +RedirectTemp /19-10 https://mail.python.org/pipermail/python-list/2009-February/675659.html +RedirectTemp /19-11 https://en.wikipedia.org/wiki/Braille_Patterns +RedirectTemp /19-12 https://docs.python.org/3/library/multiprocessing.shared_memory.html +RedirectTemp /19-13 https://docs.python.org/3/library/multiprocessing.shared_memory.html#multiprocessing.shared_memory.ShareableList +RedirectTemp /19-14 https://greenlet.readthedocs.io/en/latest/ +RedirectTemp /19-15 https://docs.sqlalchemy.org/en/14/changelog/migration_14.html#asynchronous-io-support-for-core-and-orm +RedirectTemp /19-16 https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html +RedirectTemp /19-17 http://www.gevent.org/ +RedirectTemp /19-18 https://github.com/gevent/gevent/wiki/Projects +RedirectTemp /19-19 https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor-example +RedirectTemp /19-20 https://github.com/python/asyncio/issues/284 +RedirectTemp /19-21 https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor +RedirectTemp /19-22 https://mail.python.org/archives/list/python-dev@python.org/message/JBYXQH3NV3YBF7P2HLHB5CD6V3GVTY55/ +RedirectTemp /19-23 https://docs.python.org/3/library/queue.html#queue.SimpleQueue.get +RedirectTemp /19-24 https://en.wikipedia.org/wiki/Race_condition +RedirectTemp /19-25 https://github.com/fluentpython/example-code-2e/commit/2c1230579db99738a5e5e6802063bda585f6476d +RedirectTemp /19-26 https://github.com/fluentpython/example-code-2e/blob/master/19-concurrency/primes/README.md +RedirectTemp /19-27 https://github.com/fluentpython/example-code-2e/blob/master/19-concurrency/primes/threads.py +RedirectTemp /19-28 https://en.wikipedia.org/wiki/Context_switch +RedirectTemp /19-29 http://www.gotw.ca/publications/concurrency-ddj.htm +RedirectTemp /19-30 https://www.ansible.com/ +RedirectTemp /19-31 https://saltproject.io/ +RedirectTemp /19-32 https://www.fabfile.org/ +RedirectTemp /19-33 https://engineering.fb.com/2016/05/27/production-engineering/python-in-production-engineering/ +RedirectTemp /19-34 https://jupyter.org/ +RedirectTemp /19-35 https://docs.bokeh.org/en/latest/index.html +RedirectTemp /19-36 https://www.tensorflow.org/ +RedirectTemp /19-37 https://pytorch.org/ +RedirectTemp /19-38 https://www.oreilly.com/radar/where-programming-ops-ai-and-the-cloud-are-headed-in-2021/ +RedirectTemp /19-39 https://www.youtube.com/watch?v=ods97a5Pzw0 +RedirectTemp /19-40 https://www.thoughtworks.com/radar/techniques/high-performance-envy-web-scale-envy +RedirectTemp /19-41 https://modwsgi.readthedocs.io/en/master/ +RedirectTemp /19-42 https://uwsgi-docs.readthedocs.io/en/latest/ +RedirectTemp /19-43 https://unit.nginx.org/ +RedirectTemp /19-44 https://www.techatbloomberg.com/blog/configuring-uwsgi-production-deployment/ +RedirectTemp /19-45 https://www.youtube.com/watch?v=p6R1h2Nn468 +RedirectTemp /19-46 https://asgi.readthedocs.io/en/latest/index.html +RedirectTemp /19-47 https://docs.celeryproject.org/en/stable/getting-started/introduction.html +RedirectTemp /19-48 https://python-rq.org/ +RedirectTemp /19-49 https://docs.celeryproject.org/en/stable/faq.html#what-kinds-of-things-should-i-use-celery-for +RedirectTemp /19-50 https://redis.io/ +RedirectTemp /19-51 https://realpython.com/intro-to-python-threading/ +RedirectTemp /19-52 https://pymotw.com/3/concurrency.html +RedirectTemp /19-53 https://www.pearson.com/us/higher-education/program/Hellmann-Python-3-Standard-Library-by-Example-The/PGM328871.html +RedirectTemp /19-54 https://docs.python.org/3/library/multiprocessing.html#programming-guidelines +RedirectTemp /19-55 https://docs.python.org/3/library/multiprocessing.html +RedirectTemp /19-56 https://www.oreilly.com/library/view/high-performance-python/9781492055013/ +RedirectTemp /19-57 https://link.springer.com/book/10.1007/978-1-4842-5793-7?error=cookies_not_supported&code=2ed5d61d-ae9f-4f3d-94ac-0f68cf45ea4f +RedirectTemp /19-58 https://www.packtpub.com/product/parallel-programming-with-python/9781783288397 +RedirectTemp /19-59 https://greenteapress.com/wp/semaphores/ +RedirectTemp /19-60 https://docs.python.org/3/c-api/init.html#thread-state-and-the-global-interpreter-lock +RedirectTemp /19-61 https://docs.python.org/3/faq/library.html#can-t-we-get-rid-of-the-global-interpreter-lock +RedirectTemp /19-62 https://www.artima.com/weblogs/viewpost.jsp?thread=214235 +RedirectTemp /19-63 http://jessenoller.com/blog/2009/02/01/python-threads-and-the-global-interpreter-lock +RedirectTemp /19-64 https://realpython.com/products/cpython-internals-book/ +RedirectTemp /19-65 http://www.dabeaz.com/GIL/ +RedirectTemp /19-66 http://www.dabeaz.com/python/UnderstandingGIL.pdf +RedirectTemp /19-67 https://bugs.python.org/issue7946#msg223110 +RedirectTemp /19-68 https://bugs.python.org/issue7946 +RedirectTemp /19-69 https://www.fullstackpython.com/ +RedirectTemp /19-70 https://www.oreilly.com/library/view/high-performance-python/9781492055013/ +RedirectTemp /19-71 https://www.packtpub.com/product/parallel-programming-with-python/9781783288397 +RedirectTemp /19-72 https://www.packtpub.com/product/distributed-computing-with-python/9781785889691 +RedirectTemp /19-73 https://towardsdatascience.com/python-performance-and-gpus-1be860ffd58d?gi=a7d537cc2fb4 +RedirectTemp /19-74 https://instagram-engineering.com/web-service-efficiency-at-instagram-with-python-4976d078e366?gi=12a441991c88 +RedirectTemp /19-75 https://www.oreilly.com/library/view/architecture-patterns-with/9781492052197/ +RedirectTemp /19-76 https://www.cosmicpython.com/ +RedirectTemp /19-77 https://pypi.org/project/lelo/ +RedirectTemp /19-78 https://github.com/npryce/python-parallelize +RedirectTemp /19-79 https://github.com/ericsnowcurrently/multi-core-python/wiki +RedirectTemp /19-80 https://gist.github.com/markshannon/79cace3656b40e21b7021504daee950c +RedirectTemp /19-81 https://mail.python.org/archives/list/python-dev@python.org/message/YOOQZCFOKEPQ24YHWWLQSJ3RCXFMS7D7/ +RedirectTemp /19-82 https://en.wikipedia.org/wiki/Communicating_sequential_processes +RedirectTemp /19-83 https://github.com/stackless-dev/stackless/wiki +RedirectTemp /19-84 https://www.eveonline.com +RedirectTemp /19-85 https://www.ccpgames.com/ +RedirectTemp /19-86 https://stackless.readthedocs.io/en/3.6-slp/stackless-python.html#history +RedirectTemp /19-87 https://doc.pypy.org/en/latest/stackless.html +RedirectTemp /19-88 https://greenlet.readthedocs.io/en/latest/ +RedirectTemp /19-89 http://www.gevent.org/ +RedirectTemp /19-90 http://thespianpy.com/doc/ +RedirectTemp /19-91 https://pykka.readthedocs.io/en/latest/ +RedirectTemp /19-92 https://www.manning.com/books/rabbitmq-in-action +RedirectTemp /19-93 https://pragprog.com/titles/pb7con/seven-concurrency-models-in-seven-weeks/ +RedirectTemp /19-94 https://en.wikipedia.org/wiki/OpenCL +RedirectTemp /19-95 https://media.pragprog.com/titles/pb7con/Bonus_Chapter.pdf +RedirectTemp /19-96 https://martinfowler.com/ +RedirectTemp /19-97 https://martinfowler.com/articles/patterns-of-distributed-systems/ +RedirectTemp /19-98 https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/ +RedirectTemp /19-99 https://www.oreilly.com/library/view/oscon-2016-video/9781491965153/video247021.html +RedirectTemp /19-100 https://www.oreilly.com/library/view/designing-for-scalability/9781449361556/ +RedirectTemp /19-101 https://www.thoughtworks.com/radar/techniques/high-performance-envy-web-scale-envy +RedirectTemp /19-102 https://en.wikipedia.org/wiki/KISS_principle +RedirectTemp /19-103 https://www.usenix.org/conference/hotos15/workshop-program/presentation/mcsherry +############################################################ 20 +RedirectTemp /20-1 https://www.artima.com/weblogs/viewpost.jsp?thread=299551 +RedirectTemp /20-2 https://docs.python.org/3/library/http.server.html +RedirectTemp /20-3 https://www.youtube.com/watch?v=A9e9Cy1UkME +RedirectTemp /20-4 https://www.cia.gov/the-world-factbook/ +RedirectTemp /20-5 https://docs.python-requests.org/en/latest/ +RedirectTemp /20-6 https://docs.python.org/3.10/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor +RedirectTemp /20-7 https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.as_completed +RedirectTemp /20-8 https://docs.python.org/3/library/concurrent.futures.html +RedirectTemp /20-9 https://docs.python.org/3.10/library/concurrent.futures.html#concurrent.futures.Executor +RedirectTemp /20-10 https://github.com/fluentpython/example-code-2e/blob/master/20-executors/getflags/flags2_common.py +RedirectTemp /20-11 https://github.com/noamraph/tqdm +RedirectTemp /20-12 https://www.youtube.com/watch?v=M8Z65tAl5l4 +RedirectTemp /20-13 https://github.com/noamraph/tqdm/blob/master/README.md +RedirectTemp /20-14 https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.as_completed +RedirectTemp /20-15 https://docs.python.org/3/library/asyncio-task.html#asyncio.as_completed +RedirectTemp /20-16 https://www.cloudflare.com/ +RedirectTemp /20-17 https://github.com/fluentpython/example-code-2e/tree/master/20-executors/getflags +RedirectTemp /20-18 https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418 +RedirectTemp /20-19 https://en.wikipedia.org/wiki/Embarrassingly_parallel +RedirectTemp /20-20 https://pyvideo.org/video/480/pyconau-2010--the-future-is-soon/ +RedirectTemp /20-21 http://www.dabeaz.com/coroutines/ +RedirectTemp /20-22 https://en.wikipedia.org/wiki/POSIX_Threads +RedirectTemp /20-23 https://en.wikipedia.org/wiki/C_dynamic_memory_allocation +RedirectTemp /20-24 https://pragprog.com/titles/pb7con/seven-concurrency-models-in-seven-weeks/ +RedirectTemp /20-25 https://hexdocs.pm/ecto/getting-started.html +############################################################ 21 +RedirectTemp /21-1 https://docs.python.org/3/library/asyncio.html +RedirectTemp /21-2 https://bugs.python.org/issue43216 +RedirectTemp /21-3 https://bugs.python.org/issue36921 +RedirectTemp /21-4 https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.getaddrinfo +RedirectTemp /21-5 https://docs.python.org/3/library/socket.html#socket.getaddrinfo +RedirectTemp /21-6 https://docs.python.org/3.10/library/asyncio-eventloop.html#asyncio.get_event_loop +RedirectTemp /21-7 https://www.python.org/dev/peps/pep-0492/#await-expression +RedirectTemp /21-8 https://www.fluentpython.com/extra/classic-coroutines/#yield_from_meaning_sec +RedirectTemp /21-9 https://github.com/fluentpython/example-code-2e/tree/master/20-executors/getflags +RedirectTemp /21-10 https://magicstack.github.io/asyncpg/current/ +RedirectTemp /21-11 https://magicstack.github.io/asyncpg/current/api/index.html#transactions +RedirectTemp /21-12 https://magicstack.github.io/asyncpg/current/api/index.html#transactions +RedirectTemp /21-13 https://github.com/MagicStack/asyncpg/blob/4d39a05268ce4cc01b00458223a767542da048b8/asyncpg/transaction.py#L57 +RedirectTemp /21-14 https://magicstack.github.io/asyncpg/current/usage.html#connection-pools +RedirectTemp /21-15 https://gist.github.com/jboner/2841832 +RedirectTemp /21-16 https://en.wikipedia.org/wiki/Network-attached_storage +RedirectTemp /21-17 https://en.wikipedia.org/wiki/Semaphore_(programming) +RedirectTemp /21-18 https://en.wikipedia.org/wiki/Semaphore_(programming) +RedirectTemp /21-19 https://groups.google.com/forum/#!msg/python-tulip/PdAEtwpaJHs/7fqb-Qj2zJoJ +RedirectTemp /21-20 https://web.archive.org/web/20151209151711/http://tritarget.org/blog/2012/11/28/the-pyramid-of-doom-a-javascript-style-trap +RedirectTemp /21-21 https://stackoverflow.com/questions/53701841/what-is-the-use-case-for-future-add-done-callback/53710563 +RedirectTemp /21-22 https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor +RedirectTemp /21-23 https://motor.readthedocs.io/en/stable/ +RedirectTemp /21-24 https://emptysqua.re/blog/response-to-asynchronous-python-and-databases/ +RedirectTemp /21-25 https://docs.python.org/3/library/asyncio-stream.html#tcp-echo-server-using-streams +RedirectTemp /21-26 https://en.wikipedia.org/wiki/Phaistos_Disc +RedirectTemp /21-27 https://en.wikipedia.org/wiki/Inverted_index +RedirectTemp /21-28 https://fastapi.tiangolo.com/ +RedirectTemp /21-29 https://swagger.io/specification/ +RedirectTemp /21-30 https://asgi.readthedocs.io/en/latest/implementations.html +RedirectTemp /21-31 https://pydantic-docs.helpmanual.io/ +RedirectTemp /21-32 https://doc.traefik.io/traefik/ +RedirectTemp /21-33 https://fastapi.tiangolo.com/project-generation/ +RedirectTemp /21-34 https://fastapi.tiangolo.com/tutorial/response-model/ +RedirectTemp /21-35 https://docs.python.org/3/library/asyncio-stream.html#asyncio.start_server +RedirectTemp /21-36 https://github.com/python/typeshed/issues/5535 +RedirectTemp /21-37 https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.Server.serve_forever +RedirectTemp /21-38 https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamWriter.close +RedirectTemp /21-39 https://docs.python.org/3/library/asyncio-stream.html#streamwriter +RedirectTemp /21-40 https://docs.python.org/3/library/asyncio-stream.html +RedirectTemp /21-41 https://docs.python.org/3/library/asyncio-protocol.html +RedirectTemp /21-42 https://docs.python.org/3/library/asyncio-protocol.html#tcp-echo-server +RedirectTemp /21-43 https://github.com/aio-libs/aiopg +RedirectTemp /21-44 https://docs.python.org/3/whatsnew/3.8.html#asyncio +RedirectTemp /21-45 https://datatracker.ietf.org/doc/html/rfc6761 +RedirectTemp /21-46 https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager +RedirectTemp /21-47 https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager +RedirectTemp /21-48 https://docs.python.org/3/library/asyncio-task.html#asyncio.gather +RedirectTemp /21-49 https://curio.readthedocs.io/en/latest/index.html +RedirectTemp /21-50 https://curio.readthedocs.io/en/latest/reference.html#task-groups +RedirectTemp /21-51 https://en.wikipedia.org/wiki/Structured_concurrency +RedirectTemp /21-52 https://mail.python.org/archives/list/python-dev@python.org/thread/2ORDAW74LGE3ZI2QETPJRT2ZL7MCCPG2/ +RedirectTemp /21-53 https://www.python.org/dev/peps/pep-0654/#motivation +RedirectTemp /21-54 https://curio.readthedocs.io/en/latest/reference.html#AWAIT +RedirectTemp /21-55 https://www.python-httpx.org/async/#curio +RedirectTemp /21-56 https://github.com/dabeaz/curio/tree/78bca8a6ad677ef51e1568ac7b3e51441ab49c42/examples +RedirectTemp /21-57 https://datatracker.ietf.org/doc/html/rfc8305 +RedirectTemp /21-58 https://trio.readthedocs.io/en/stable/ +RedirectTemp /21-59 https://www.youtube.com/watch?v=M-sc73Y-zQA +RedirectTemp /21-60 https://en.wikipedia.org/wiki/Technical_debt +RedirectTemp /21-61 https://www.youtube.com/watch?v=E-1Y4kSsAFc +RedirectTemp /21-62 https://github.com/dabeaz/curio +RedirectTemp /21-63 https://trio.readthedocs.io/en/stable/ +RedirectTemp /21-64 https://curio.readthedocs.io/en/latest/#curio-university +RedirectTemp /21-65 https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/ +RedirectTemp /21-66 https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ +RedirectTemp /21-67 https://stackoverflow.com/questions/49482969/what-is-the-core-difference-between-asyncio-and-trio +RedirectTemp /21-68 https://docs.python.org/3/library/asyncio.html +RedirectTemp /21-69 https://bugs.python.org/issue33649 +RedirectTemp /21-70 https://docs.python.org/3/library/asyncio-dev.html +RedirectTemp /21-71 https://www.youtube.com/watch?v=iG6fr81xHKA +RedirectTemp /21-72 https://www.youtube.com/watch?v=F19R_M4Nay4 +RedirectTemp /21-73 https://asherman.io/projects/unsync.html +RedirectTemp /21-74 https://pyladies.com/ +RedirectTemp /21-75 https://www.youtube.com/watch?v=sW76-pRkZk8 +RedirectTemp /21-76 https://www.youtube.com/watch?v=Xbl7XjFYsN4 +RedirectTemp /21-77 https://www.youtube.com/watch?v=02CLD-42VdI +RedirectTemp /21-78 https://micropython.org/ +RedirectTemp /21-79 https://docs.micropython.org/en/latest/library/uasyncio.html +RedirectTemp /21-80 https://www.encode.io/articles/python-async-frameworks-beyond-developer-tribalism +RedirectTemp /21-81 https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ +RedirectTemp /21-82 https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ +RedirectTemp /21-83 https://github.com/MagicStack/uvloop +RedirectTemp /21-84 http://magic.io/blog/uvloop-blazing-fast-python-networking/ +RedirectTemp /21-85 https://github.com/MagicStack/httptools +RedirectTemp /21-86 https://docs.aiohttp.org/en/stable/ +RedirectTemp /21-87 https://github.com/wg/wrk +RedirectTemp /21-88 https://twistedmatrix.com/trac/ +############################################################ 22 +RedirectTemp /22-1 https://github.com/fluentpython/example-code-2e/blob/master/22-dyn-attr-prop/oscon/data/osconfeed.json +RedirectTemp /22-2 https://pypi.org/project/attrdict/ +RedirectTemp /22-3 https://pypi.org/project/addict/ +RedirectTemp /22-4 https://github.com/ActiveState/code/tree/master/recipes/Python/52308_simple_but_handy_collector_bunch_named_stuff +RedirectTemp /22-5 https://docs.python.org/3/library/types.html#types.SimpleNamespace +RedirectTemp /22-6 https://docs.python.org/3/library/argparse.html#argparse.Namespace +RedirectTemp /22-7 https://docs.python.org/3/library/multiprocessing.html#multiprocessing.managers.Namespace +RedirectTemp /22-8 https://github.com/fluentpython/example-code-2e/blob/master/22-dyn-attr-prop/oscon/schedule_v2.py +RedirectTemp /22-9 https://docs.python.org/3/library/functools.html#functools.cached_property +RedirectTemp /22-10 https://docs.python.org/3/library/functools.html#functools.cached_property +RedirectTemp /22-11 https://bugs.python.org/issue42781 +RedirectTemp /22-12 https://docs.python.org/3/howto/descriptor.html +RedirectTemp /22-13 https://github.com/python/cpython/blob/e6d0107e13ed957109e79b796984d3d026a8660d/Lib/functools.py#L926 +RedirectTemp /22-14 https://docs.python.org/3/library/threading.html#rlock-objects +RedirectTemp /22-15 https://docs.python.org/3.10/library/functools.html#functools.cached_property +RedirectTemp /22-16 https://www.wsj.com/articles/SB10001424052970203914304576627102996831200 +RedirectTemp /22-17 https://www.youtube.com/watch?v=s35rVw1zskA&feature=youtu.be +RedirectTemp /22-18 https://docs.python.org/3/library/functions.html#dir +RedirectTemp /22-19 https://github.com/python/cpython/blob/19903085c3ad7a17c8047e1556c700f2eb109931/Lib/cmd.py#L214 +RedirectTemp /22-20 https://docs.python.org/3/library/functions.html#hasattr +RedirectTemp /22-21 https://docs.python.org/3.10/reference/datamodel.html#special-method-lookup +RedirectTemp /22-22 https://docs.python.org/3/library/functions.html +RedirectTemp /22-23 https://docs.python.org/3/reference/datamodel.html#customizing-attribute-access +RedirectTemp /22-24 https://docs.python.org/3/reference/datamodel.html#special-method-lookup +RedirectTemp /22-25 https://docs.python.org/3/library/stdtypes.html#special-attributes +RedirectTemp /22-26 http://wiki.c2.com/?WelcomeVisitors +RedirectTemp /22-27 http://wiki.c2.com/?UniformAccessPrinciple +RedirectTemp /22-28 https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html +RedirectTemp /22-29 http://www.pingo.io/docs/ +RedirectTemp /22-30 https://www.drdobbs.com/javas-new-considered-harmful/184405016 +RedirectTemp /22-31 https://www.python.org/dev/peps/pep-0008/#class-names +############################################################ 23 +RedirectTemp /23-1 http://www.aleax.it/goo_pydp.pdf +RedirectTemp /23-2 https://docs.python.org/3.10/reference/datamodel.html#implementing-descriptors +RedirectTemp /23-3 https://docs.python.org/3/howto/descriptor.html +RedirectTemp /23-4 https://docs.python.org/3/howto/ +RedirectTemp /23-5 http://www.aleax.it/Python/nylug05_om.pdf +RedirectTemp /23-6 https://www.youtube.com/watch?v=VOzvpHoYQoo +RedirectTemp /23-7 https://www.python.org/dev/peps/pep-0487/#trait-descriptors +RedirectTemp /23-8 https://dreamsongs.com/RiseOfWorseIsBetter.html +RedirectTemp /23-9 http://web.archive.org/web/20031002184114/www.amk.ca/python/writing/warts.html +RedirectTemp /23-10 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this +RedirectTemp /23-11 http://python-history.blogspot.com/2009/02/adding-support-for-user-defined-classes.html +############################################################ 24 +RedirectTemp /24-1 https://docs.python.org/3/library/stdtypes.html#special-attributes +RedirectTemp /24-2 https://docs.djangoproject.com/en/3.2/topics/db/models/#meta-options +RedirectTemp /24-3 https://www.python.org/dev/peps/pep-3155/ +RedirectTemp /24-4 https://github.com/python/cpython/blob/3.9/Lib/collections/__init__.py +RedirectTemp /24-5 https://en.wikipedia.org/wiki/Tony_Hoare#Apologies_and_retractions +RedirectTemp /24-6 https://go.dev/tour/basics/12 +RedirectTemp /24-7 https://bugs.python.org/issue42102 +RedirectTemp /24-8 https://docs.python.org/3/reference/datamodel.html#object.__init_subclass__ +RedirectTemp /24-9 https://www.python.org/dev/peps/pep-0557/#abstract +RedirectTemp /24-10 https://github.com/python/cpython/blob/3.9/Lib/dataclasses.py +RedirectTemp /24-11 https://docs.python.org/3/reference/datamodel.html#creating-the-class-object +RedirectTemp /24-12 https://mail.python.org/pipermail/python-list/2002-December/134521.html +RedirectTemp /24-13 https://mail.python.org/pipermail/python-list/2002-July/162558.html +RedirectTemp /24-14 https://github.com/fluentpython/example-code/tree/master/21-class-metaprog/bulkfood +RedirectTemp /24-15 https://en.wikipedia.org/wiki/Principle_of_least_astonishment +RedirectTemp /24-16 https://docs.python.org/3/reference/datamodel.html#object.__class_getitem__ +RedirectTemp /24-17 https://en.wikipedia.org/wiki/Trait_(computer_programming) +RedirectTemp /24-18 https://en.wikipedia.org/wiki/Aspect-oriented_programming +RedirectTemp /24-19 https://dhh.dk/arc/000416.html +RedirectTemp /24-20 https://github.com/cjrh/autoslot +RedirectTemp /24-21 https://docs.python.org/3/reference/datamodel.html#customizing-class-creation +RedirectTemp /24-22 https://docs.python.org/3/library/functions.html#type +RedirectTemp /24-23 https://docs.python.org/3/library/stdtypes.html#special-attributes +RedirectTemp /24-24 https://docs.python.org/3/library/types.html +RedirectTemp /24-25 https://www.python.org/dev/peps/pep-3129/ +RedirectTemp /24-26 https://www.youtube.com/watch?v=cAGliEJV9_o +RedirectTemp /24-27 https://docs.python.org/3/library/functools.html#functools.total_ordering +RedirectTemp /24-28 https://www.python.org/download/releases/2.2.3/descrintro/ +RedirectTemp /24-29 https://github.com/lihaoyi/macropy +RedirectTemp /24-30 https://people.eecs.berkeley.edu/~bh/ss-toc2.html +# content of short.htaccess file created and managed by short.py + +# appended: 2025-05-23 15:12:13 +RedirectTemp /22 https://pythonfluente.com/2/#pattern_matching_case_study_sec +RedirectTemp /23 https://pythonfluente.com/2/#how_slicing_works +RedirectTemp /24 https://pythonfluente.com/2/#sliceable_sequence +RedirectTemp /25 https://pythonfluente.com/2/#virtual_subclass_sec +RedirectTemp /26 https://pythonfluente.com/2/#environment_class_ex +RedirectTemp /27 https://pythonfluente.com/2/#subclass_builtin_woes +RedirectTemp /28 https://pythonfluente.com/2/#slots_section +RedirectTemp /29 https://pythonfluente.com/2/#typeddict_sec +RedirectTemp /2a https://pythonfluente.com/2/#problems_annot_runtime_sec +RedirectTemp /2b https://pythonfluente.com/2/#legacy_deprecated_typing_box +RedirectTemp /2c https://pythonfluente.com/2/#positional_pattern_implement_sec diff --git a/links/README.md b/links/README.md new file mode 100644 index 0000000..65749b2 --- /dev/null +++ b/links/README.md @@ -0,0 +1,6 @@ +This file is deployed as `.htaccess` to the FPY.LI domain +to map short URLs in Fluent Python to the original URLs. + +To update it, I use tools in this other repo: + +https://github.com/pythonfluente/pythonfluente2e diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..f7b07e2 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,4 @@ +line-length = 100 +[format] +# Like Python's repr(), use single quotes for strings. +quote-style = "single" \ No newline at end of file