{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "#
Python 3.10 — **Structural Pattern Matching**
\n", "\n", "References:\n", "\n", "- [PEP 622 Structural Pattern Matching](https://www.python.org/dev/peps/pep-0622/)\n", "- [PEP 635 Structural Pattern Matching: Motivation and Rationale](https://www.python.org/dev/peps/pep-0635/)\n", "- [PEP 636 Structural Pattern Matching: Tutorial](https://www.python.org/dev/peps/pep-0636/)\n", "\n", "


" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "### We've always had it!\n", "\n", "

*) since at least Python 1.6

\n", "\n", "#### ...with assignment" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "18 hours, 32 minutes and 50 seconds\n" ] } ], "source": [ "text = \"18:32:50\"\n", "#text = \"18:32\" # 🔧\n", "\n", "# ↘️\n", "h, m, s = text.split(\":\")\n", "\n", "print(f\"{h} hours, {m} minutes and {s} seconds\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "#### ...with \"for\"" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "red is #FF0000\n", "blue is #0000FF\n", "yellow is #FFFF00\n" ] } ], "source": [ "colors = {\n", " \"red\": (255, 0, 0),\n", " \"blue\": (0, 0, 255),\n", " \"yellow\": (255, 255, 0),\n", " #\"transparent gray\": (100, 100, 100, 0.5) # 🔧\n", "}\n", "\n", "# ↘️ ↘️\n", "for name, (r, g, b) in colors.items():\n", " print(name, \"is\", f\"#{r:02X}{g:02X}{b:02X}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### ...with try/except" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "No such file\n" ] } ], "source": [ "try:\n", " open(\"non-existent.txt\")\n", " #open(\"/etc/shadow\")\n", "except FileNotFoundError as e: # ⬅️\n", " print(\"No such file\")\n", "except PermissionError as e: # ⬅️\n", " print(f\"Not allowed (error {e.errno})\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "### Now we can use \"match\"" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "18 hours, 32 minutes and 50 seconds\n" ] } ], "source": [ "text = \"18:32:50\"\n", "#text = \"18:32\" # 🔧\n", "#text = \"tea time\" # 🔧\n", "\n", "match text.split(\":\"):\n", " case [h, m]:\n", " print(f\"{h} hours and {m} minutes\")\n", " case [h, m, s]:\n", " print(f\"{h} hours, {m} minutes and {s} seconds\")\n", " case _:\n", " print(\"oops\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Let's have a look at 3 examples
\n", "\n", "\n", "1️⃣ Event Queue

\n", "2️⃣ JSON API

\n", "3️⃣ Tree Traversal\n", "
\n", "\n", "


" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Example 1: Event Queue\n", "\n", "
\n", "\n", "Snippet from previous Big Python tutorial (event-driven Snake in PyGame, [link](https://youtu.be/_1KkTIxAZGg))." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Python 3.9\n", "if isinstance(message, SnakeChangeDirectionMessage):\n", " if message.direction != opposite_direction:\n", " self.direction = message.direction\n", "elif isinstance(message, EntityCollisionMessage) and message.entity is self:\n", " if isinstance(message.other, (Snake, Wall)):\n", " new_messages.append(GameOverMessage(\"Snake collision\"))\n", " elif isinstance(message.other, Food):\n", " self.max_length += 1\n", " new_messages.append(RemoveEntityMessage(message.other))\n", " new_messages.append(SpawnFoodMessage())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Python 3.10\n", "match message:\n", " case SnakeChangeDirectionMessage(direction):\n", " if direction != opposite_direction:\n", " self.direction = direction\n", " case EntityCollisionMessage(entity, other) if entity is self:\n", " match other:\n", " case Wall() | Snake():\n", " new_messages.append(GameOverMessage(\"Snake collision\"))\n", " case Food():\n", " self.max_length += 1\n", " new_messages.append(RemoveEntityMessage(other))\n", " new_messages.append(SpawnFoodMessage())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "


\n", "## Example 2: JSON API\n", "
\n", "With mapping and string literal patterns, processing JSON dicts is easy.\n", "\n", "Let's look at commits in the Flask GitHub repository." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import requests\n", "\n", "# 🔍 commits in Flask repository containing \"jinja\"\n", "response = requests.get(\"https://api.github.com/search/commits?q=repo:pallets/flask+jinja\",\n", " headers={\"Accept\": \"application/vnd.github.cloak-preview+json\"})\n", "data = response.json()\n", "data" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Python 3.9\n", "for item in data[\"items\"]:\n", " sha = item[\"sha\"]\n", " message = item[\"commit\"][\"message\"]\n", " name = item[\"commit\"][\"author\"][\"name\"]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Python 3.10\n", "for item in data[\"items\"]:\n", " match item:\n", " case {\"sha\": sha, \"commit\": {\"message\": message, \"author\": {\"name\": name}}}:\n", " print(sha, \"by\", name)\n", " print(message)\n", " print(80*\"-\")\n", " #case {\"sha\": sha, \"commit\": {\"message\": message, \"author\": {\"name\": \"Armin Ronacher\"}}}:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "


\n", "## Example 3: Tree Traversal\n", "\n", "
\n", "Functional programming with pattern matching and recursion can be very powerful for processing tree-like structures.\n", "\n", "This example shows derivation and simplification of mathematical expressions using Python `ast` module; something you could do with `sympy`." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "7 * x\n", "\n", "Expression(\n", " body=BinOp(\n", " left=Constant(value=7),\n", " op=Mult(),\n", " right=Name(id='x', ctx=Load())))\n" ] } ], "source": [ "import ast; from ast import *\n", "\n", "eq = \"7*x\"\n", "expr = ast.parse(eq, mode=\"eval\")\n", "print(ast.unparse(expr), ast.dump(expr, indent=4), sep=\"\\n\\n\")" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def derivate(expr, dx: str):\n", " match expr:\n", " case Expression(e):\n", " return Expression(derivate(e, dx))\n", " case Constant(): # d/dx C = 0\n", " return Constant(0)\n", " case Name(x) if x == dx: # d/dx x = 1\n", " return Constant(1)\n", " case Name(x) if x != dx: # d/dx y = 0\n", " return Constant(0)\n", " case BinOp(Name(x), Pow(), Constant(a)) if x == dx: # d/dx x^a = ax^(a-1)\n", " return BinOp(Constant(a),\n", " Mult(),\n", " BinOp(Name(x), Pow(), Constant(a-1)))\n", " case BinOp(lhs, Add(), rhs): # (a+b)' = a' + b'\n", " return BinOp(derivate(lhs, dx), Add(), derivate(rhs, dx))\n", " case BinOp(lhs, Mult(), rhs): # (ab)' = a'b + ab'\n", " return BinOp(BinOp(derivate(lhs, dx), Mult(), rhs),\n", " Add(),\n", " BinOp(lhs, Mult(), derivate(rhs, dx)))\n", " case _:\n", " raise NotImplementedError(f\"{expr!r}\")" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0 * x + 7 * 1\n", "\n", "Expression(\n", " body=BinOp(\n", " left=BinOp(\n", " left=Constant(value=0),\n", " op=Mult(),\n", " right=Name(id='x', ctx=Load())),\n", " op=Add(),\n", " right=BinOp(\n", " left=Constant(value=7),\n", " op=Mult(),\n", " right=Constant(value=1))))\n" ] } ], "source": [ "eq = \"7*x\"\n", "expr = ast.parse(eq, mode=\"eval\")\n", "expr_prime = derivate(expr, \"x\")\n", "print(ast.unparse(expr_prime), ast.dump(expr_prime, indent=4), sep=\"\\n\\n\")" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "def simplify(expr):\n", " # recurse\n", " match expr:\n", " case Expression(e):\n", " return Expression(simplify(e))\n", " case BinOp(rhs, op, lhs):\n", " expr = BinOp(simplify(rhs), op, simplify(lhs))\n", " \n", " # simplify\n", " match expr:\n", " case BinOp(_, Mult(), Constant(0)) | BinOp(Constant(0), Mult(), _):\n", " return Constant(0)\n", " case BinOp(x, Add(), Constant(0)) | BinOp(Constant(0), Add(), x) | \\\n", " BinOp(x, Mult(), Constant(1)) | BinOp(Constant(1), Mult(), x) | \\\n", " BinOp(x, Pow(), Constant(1)):\n", " return x\n", " case _:\n", " return expr" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0 * x ** 9 + 4 * (9 * x ** 8) + (0 * x ** 3 + 12 * (3 * x ** 2)) + ((0 * x + 34 * 1) * y + 34 * x * 0) + 0 + 0\n", "\n", "4 * (9 * x ** 8) + 12 * (3 * x ** 2) + 34 * y\n" ] } ], "source": [ "#eq = \"7*x\"\n", "eq = \"4*x**9 + 12*x**3 + 34*x*y + y + 5\"\n", "expr = ast.parse(eq, mode=\"eval\")\n", "expr_prime = derivate(expr, \"x\")\n", "expr_prime_simple = simplify(expr_prime)\n", "print(ast.unparse(expr_prime), ast.unparse(expr_prime_simple), sep=\"\\n\\n\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "
\n", "
\n", "\n", "## Addendum\n", "#### How to compile Python-3.10.0a6 on Ubuntu 20.04 LTS\n", "\n", "
\n", "\n", "```sh\n", "sudo apt install python3-dev libffi-dev libsqlite3-dev\n", "./configure && make -j 4\n", "./python -m ensurepip\n", "./python -m pip install jupyter\n", "```" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# set Jupyter theme: https://github.com/dunovank/jupyter-themes\n", "!jt -t grade3" ] } ], "metadata": { "kernelspec": { "display_name": "venv-3.10", "language": "python", "name": "venv-3.10" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.0a6" } }, "nbformat": 4, "nbformat_minor": 4 }