diff --git a/05-data-classes/05-data-classes.ipynb b/05-data-classes/05-data-classes.ipynb new file mode 100644 index 0000000..aea9d71 --- /dev/null +++ b/05-data-classes/05-data-classes.ipynb @@ -0,0 +1,1774 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": true, + "pycharm": { + "name": "#%%\n" + } + }, + "source": [ + "# Chapter 5 — Data Class Builders" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Overview of Data Class Builders" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-1. [class/coordinates.py](class/coordinates.py)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "class Coordinate:\n", + "\n", + " def __init__(self, lat, lon):\n", + " self.lat = lat\n", + " self.lon = lon" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "<__main__.Coordinate at 0x7f117e307b80>" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "moscow = Coordinate(55.76, 37.62)\n", + "moscow" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "location = Coordinate(55.76, 37.62)\n", + "location == moscow" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(location.lat, location.lon) == (moscow.lat, moscow.lon)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Coordinate` class built with `collections.namedtuple`" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from collections import namedtuple\n", + "Coordinate = namedtuple('Coordinate', 'lat lon')\n", + "issubclass(Coordinate, tuple)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Coordinate(lat=55.756, lon=37.617)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "moscow = Coordinate(55.756, 37.617)\n", + "moscow" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "moscow == Coordinate(55.756, 37.617)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Coordinate` class built with `typing.NamedTuple`" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import typing\n", + "Coordinate = typing.NamedTuple('Coordinate', [('lat', float), ('lon', float)])\n", + "# Coordinate = typing.NamedTuple('Coordinate', lat=float, lon=float)\n", + "issubclass(Coordinate, tuple)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'lat': float, 'lon': float}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "typing.get_type_hints(Coordinate)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-2. [typing_namedtuple/coordinates.py](typing_namedtuple/coordinates.py)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import NamedTuple\n", + "\n", + "class Coordinate(NamedTuple):\n", + " lat: float\n", + " lon: float\n", + "\n", + " def __str__(self):\n", + " ns = 'N' if self.lat >= 0 else 'S'\n", + " we = 'E' if self.lon >= 0 else 'W'\n", + " return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "issubclass() arg 2 must be a class, a tuple of classes, or a union\n" + ] + } + ], + "source": [ + "try:\n", + " issubclass(Coordinate, typing.NamedTuple)\n", + "except TypeError as err:\n", + " print(err)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "issubclass(Coordinate, tuple)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-3. [dataclass/coordinates.py](dataclass/coordinates.py)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "\n", + "@dataclass(frozen=True)\n", + "class Coordinate:\n", + " lat: float\n", + " lon: float\n", + "\n", + " def __str__(self):\n", + " ns = 'N' if self.lat >= 0 else 'S'\n", + " we = 'E' if self.lon >= 0 else 'W'\n", + " return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "issubclass(Coordinate, object)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Classic Named Tuples" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-4. Defining and using a named tuple type" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 139.691667))" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "City = namedtuple('City', 'name country population coordinates')\n", + "tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))\n", + "tokyo" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "36.933" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tokyo.population" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(35.689722, 139.691667)" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tokyo.coordinates" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'JP'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tokyo[1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-5. Named tuple attributes and methods (continued from the previous example)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('name', 'country', 'population', 'coordinates')" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "City._fields" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'Delhi NCR',\n", + " 'country': 'IN',\n", + " 'population': 21.935,\n", + " 'coordinates': Coordinate(lat=28.613889, lon=77.208889)}" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Coordinate = namedtuple('Coordinate', 'lat lon')\n", + "delhi_data = ('Delhi NCR', 'IN', 21.935, Coordinate(28.613889, 77.208889))\n", + "delhi = City._make(delhi_data)\n", + "delhi._asdict()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"name\": \"Delhi NCR\", \"country\": \"IN\", \"population\": 21.935, \"coordinates\": [28.613889, 77.208889]}'" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import json\n", + "json.dumps(delhi._asdict())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-6. Named tuple attributes and methods, continued from Example 5-5." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Coordinate(lat=0, lon=0, reference='WGS84')" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Coordinate = namedtuple('Coordinate', 'lat lon reference', defaults=['WGS84'])\n", + "Coordinate(0, 0)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'reference': 'WGS84'}" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Coordinate._field_defaults" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Hacking a `namedtuple` to inject a method" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-7. Adding a class attribute and a method to `Card`, the `namedtuple` from \"A Pythonic Card Deck\"" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "import collections\n", + "\n", + "\n", + "Card = collections.namedtuple('Card', ['rank', 'suit'])\n", + "Card.suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)\n", + "\n", + "\n", + "class FrenchDeck:\n", + " ranks = [str(n) for n in range(2, 11)] + list('JQKA')\n", + " suits = 'spades diamonds clubs hearts'.split()\n", + "\n", + " def __init__(self):\n", + " self._cards = [Card(rank, suit) for suit in self.suits\n", + " for rank in self.ranks]\n", + "\n", + " def __len__(self):\n", + " return len(self._cards)\n", + "\n", + " def __getitem__(self, position):\n", + " return self._cards[position]\n", + "\n", + "\n", + "def spades_high(card):\n", + " rank_value = FrenchDeck.ranks.index(card.rank)\n", + " suit_value = card.suit_values[card.suit]\n", + " return rank_value * len(card.suit_values) + suit_value\n", + "\n", + "\n", + "Card.overall_rank = spades_high\n", + "lowest_card = Card('2', 'clubs')\n", + "highest_card = Card('A', 'spades')" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lowest_card.overall_rank()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "51" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "highest_card.overall_rank()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Typed Named Tuples" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-8. [typing_namedtuple/coordinates2.py](typing_namedtuple/coordinates2.py)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "class Coordinate(NamedTuple):\n", + " lat: float\n", + " lon: float\n", + " reference: str = 'WGS84'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Type Hints 101" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-9. Python does not enforce type hints at runtime" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "class Coordinate(typing.NamedTuple):\n", + " lat: float\n", + " lon: float" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Coordinate(lat='Ni', lon=None)\n" + ] + } + ], + "source": [ + "trash = Coordinate('Ni', None)\n", + "print(trash)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The Meaning of Variable Annotations" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-10. [meaning/demo_plain.py](meaning/demo_plain.py): a plain class with type hints" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "class DemoPlainClass:\n", + " a: int\n", + " b: float = 1.1\n", + " c = 'spam'" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'a': int, 'b': float}" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DemoPlainClass.__annotations__" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'DemonPlainClass' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/tmp/ipykernel_161355/1123823172.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mDemonPlainClass\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mNameError\u001b[0m: name 'DemonPlainClass' is not defined" + ] + } + ], + "source": [ + "DemonPlainClass.a" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1.1" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DemoPlainClass.b" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'spam'" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DemoPlainClass.c" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-11. [meaning/demo_nt.py](meaning/demo_nt.py): a class built with `typing.NamedTuple`" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "class DemoNTClass(typing.NamedTuple):\n", + " a: int\n", + " b: float = 1.1\n", + " c = 'spam'" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'a': int, 'b': float}" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DemoNTClass.__annotations__" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "_tuplegetter(0, 'Alias for field number 0')" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DemoNTClass.a" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "_tuplegetter(1, 'Alias for field number 1')" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DemoNTClass.b" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'spam'" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DemoNTClass.c" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'DemoNTClass(a, b)'" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DemoNTClass.__doc__" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "8" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nt = DemoNTClass(8)\n", + "nt.a" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1.1" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nt.b" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'spam'" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nt.c" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "can't set attribute", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/tmp/ipykernel_161355/1430086050.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mnt\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0ma\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m: can't set attribute" + ] + } + ], + "source": [ + "nt.a = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "can't set attribute", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/tmp/ipykernel_161355/3556876644.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mnt\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m17.3\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m: can't set attribute" + ] + } + ], + "source": [ + "nt.b = 17.3" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'DemoNTClass' object attribute 'c' is read-only", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/tmp/ipykernel_161355/927667453.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mnt\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mc\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'spam2'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m: 'DemoNTClass' object attribute 'c' is read-only" + ] + } + ], + "source": [ + "nt.c = 'spam2'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-12. [meaning/demo_dc.py](meaning/demo_dc.py): a class decorated with `@dataclass`" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "\n", + "@dataclass\n", + "class DemoDataClass:\n", + " a: int\n", + " b: float = 1.1\n", + " c = 'spam'" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'a': int, 'b': float}" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DemoDataClass.__annotations__" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'DemoDataClass(a: int, b: float = 1.1)'" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DemoDataClass.__doc__" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "type object 'DemoDataClass' has no attribute 'a'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/tmp/ipykernel_161355/3309639985.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mDemoDataClass\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m: type object 'DemoDataClass' has no attribute 'a'" + ] + } + ], + "source": [ + "DemoDataClass.a" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1.1" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DemoDataClass.b" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'spam'" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DemoDataClass.c" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "9" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dc = DemoDataClass(9)\n", + "dc.a" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1.1" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dc.b" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'spam'" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dc.c" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "No type checking is done at runtime" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [], + "source": [ + "dc.a = 10\n", + "dc.b = 'oops'\n", + "dc.c = 'whatever'\n", + "dc.z = 'secret stash'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## More about `@dataclass`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Field Options" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-13. [dataclass/club_wrong.py](dataclass/club_wrong.py): this class raises `ValueError`" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ValueError: mutable default for field guests is not allowed: use default_factory\n" + ] + } + ], + "source": [ + "try:\n", + " @dataclass\n", + " class ClubMember:\n", + " name: str\n", + " guests: list = []\n", + "except ValueError as err:\n", + " print(f\"ValueError: {err}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-14. [dataclass/club.py](dataclass/club.py): this `ClubMember` definition works" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import field\n", + "\n", + "\n", + "@dataclass\n", + "class ClubMember:\n", + " name: str\n", + " guests: list = field(default_factory=list)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-15. [dataclass/club_generic.py](dataclass/club_generic.py): this `ClubMember` definition is more precise" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class ClubMember:\n", + " name: str\n", + " guests: list[str] = field(default_factory=list)\n", + " athelete: bool = field(default=False, repr=False) # Omitted from __repr__" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Post-init Processing" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-16. [dataclass/hackerclub.py](dataclass/hackerclub.py)" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"HackerClubMember(name: str, guests: list = , handle: str = '')\"" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from dataclass.club import ClubMember\n", + "\n", + "\n", + "@dataclass\n", + "class HackerClubMember(ClubMember):\n", + " all_handles = set()\n", + " # all_handles: ClassVar[set[str]] = set() # For mypy type checking\n", + " handle: str = ''\n", + " \n", + " def __post_init__(self):\n", + " cls = self.__class__\n", + " if self.handle == '':\n", + " self.handle = self.name.split()[0]\n", + " if self.handle in cls.all_handles:\n", + " msg = f'handle {self.handle!r} already exists.'\n", + " raise ValueError(msg)\n", + " cls.all_handles.add(self.handle)\n", + " \n", + "HackerClubMember.__doc__" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven')" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "anna = HackerClubMember('Anna Ravenscroft', handle='AnnaRaven')\n", + "anna" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "HackerClubMember(name='Leo Rochael', guests=[], handle='Leo')" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "leo = HackerClubMember('Leo Rochael')\n", + "leo" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ValueError: handle 'Leo' already exists.\n" + ] + } + ], + "source": [ + "try:\n", + " leo2 = HackerClubMember('Leo DaVinci')\n", + "except ValueError as err:\n", + " print(f\"ValueError: {err}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initialization Variables That Are Not Fields" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-18. Example from [`dataclasses`](https://docs.python.org/3/library/dataclasses.html#init-only-variables) module documentation\n", + "https://docs.python.org/3/library/dataclasses.html#init-only-variables" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### `@dataclass` Example: Dublin Core Resource Record" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-19. [dataclass/resource.py](dataclass/resource.py): code for `Resource`, a class based on Dublin Core terms" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass, field, fields\n", + "from typing import Optional\n", + "from enum import Enum, auto\n", + "from datetime import date\n", + "\n", + "\n", + "class ResourceType(Enum):\n", + " BOOK = auto()\n", + " EBOOK = auto()\n", + " VIDEO = auto()\n", + " \n", + "\n", + "@dataclass\n", + "class Resource:\n", + " \"\"\"Media resource description.\"\"\"\n", + " identifier: str\n", + " title: str = ''\n", + " creators: list[str] = field(default_factory=list)\n", + " date: Optional[date] = None\n", + " type: ResourceType = ResourceType.BOOK\n", + " description: str = ''\n", + " language: str = ''\n", + " subjects: list[str] = field(default_factory=list)\n", + " \n", + " def __repr__(self):\n", + " cls = self.__class__\n", + " cls_name = cls.__name__\n", + " indent = ' ' * 4\n", + " res = [f'{cls_name}(']\n", + " for f in fields(cls):\n", + " value = getattr(self, f.name)\n", + " res.append(f'{indent}{f.name} = {value!r},')\n", + " \n", + " res.append(')')\n", + " return '\\n'.join(res) " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-20. [dataclass/resource.py](dataclass/resource.py): code for `Resource`, a class based on Dublin Core terms" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Resource(\n", + " identifier = '978-0-13-475759-9',\n", + " title = 'Refactoring, 2nd edition',\n", + " creators = ['Martin Fowler', 'Kent Back'],\n", + " date = datetime.date(2018, 11, 19),\n", + " type = ,\n", + " description = 'Improving the design of existing code',\n", + " language = 'EN',\n", + " subjects = ['computer programming', 'OOP'],\n", + ")" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "description = 'Improving the design of existing code'\n", + "book = Resource('978-0-13-475759-9', 'Refactoring, 2nd edition',\n", + " ['Martin Fowler', 'Kent Back'], date(2018, 11, 19),\n", + " ResourceType.BOOK, description, 'EN',\n", + " ['computer programming', 'OOP'])\n", + "book" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pattern Matching Class Instances" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Keyword Class Patterns" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 5-22. `City` class and a few instances" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [], + "source": [ + "import typing\n", + "\n", + "\n", + "class City(typing.NamedTuple):\n", + " continent: str\n", + " name: str\n", + " country: str\n", + " \n", + "cities = [\n", + " City('Asia', 'Toyko', 'JP'),\n", + " City('Asia', 'Delhi', 'IN'),\n", + " City('North America', 'Mexico City', 'MX'),\n", + " City('North America', 'New York', 'US'),\n", + " City('South America', 'São Paulo', 'BR'), \n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[City(continent='Asia', name='Toyko', country='JP'),\n", + " City(continent='Asia', name='Delhi', country='IN')]" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def match_asian_cities():\n", + " results = []\n", + " for city in cities:\n", + " match city:\n", + " case City(continent='Asia'):\n", + " results.append(city)\n", + " return results\n", + "\n", + "\n", + "match_asian_cities()" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['JP', 'IN']" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def match_asian_countries():\n", + " results = []\n", + " for city in cities:\n", + " match city:\n", + " case City(continent='Asia', country=cc):\n", + " results.append(cc)\n", + " return results\n", + "\n", + "\n", + "match_asian_countries()" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['JP', 'IN']" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def match_asian_cities_pos():\n", + " results = []\n", + " for city in cities:\n", + " match city:\n", + " case City('Asia', _, country):\n", + " results.append(country)\n", + " return results\n", + "\n", + "match_asian_cities_pos()" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('continent', 'name', 'country')" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "City.__match_args__" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +}