{ "cells": [ { "cell_type": "markdown", "id": "18c0435e-cd1b-4d1b-a9c5-7ccc02d5da72", "metadata": {}, "source": [ "# Creating classes in Python\n", "\n", "# Attribute vs property and class containers\n", "\n", "Germain Salvato Vallverdu\n" ] }, { "cell_type": "markdown", "id": "e7d7bdd9-4f1a-4f2d-89f2-9e4567c508aa", "metadata": {}, "source": [ "## Without property" ] }, { "cell_type": "code", "execution_count": 1, "id": "4f856eef-d27d-4919-9290-e9c654226f7f", "metadata": {}, "outputs": [], "source": [ "class Leaf:\n", " \"\"\" This class represents a leaf's tree \"\"\"\n", " \n", " def __init__(self, width: float, length: float, color: str = \"green\"):\n", " \n", " self.width = width\n", " self.length = length\n", " self.color = color\n", " \n", " def get_area(self):\n", " return self.width * self.length" ] }, { "cell_type": "code", "execution_count": 2, "id": "67df53fd-d7d7-44aa-b2ea-037332ac3fb5", "metadata": {}, "outputs": [], "source": [ "l = Leaf(4, 12)" ] }, { "cell_type": "code", "execution_count": 4, "id": "9e405032-9c6d-440c-9341-b31d1f787442", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "<__main__.Leaf at 0x7f7e3a37ae80>" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l" ] }, { "cell_type": "code", "execution_count": 5, "id": "37f6d5a4-da17-4926-a251-aec2438dbadf", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(4, 12, 'green')" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l.width, l.length, l.color" ] }, { "cell_type": "code", "execution_count": 6, "id": "fcfa4c9c-8559-44ab-913c-134df4561e8c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "48" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l.get_area()" ] }, { "cell_type": "markdown", "id": "0a1cecb1-c2a9-4405-ae99-5c297f72a067", "metadata": {}, "source": [ "By default there is no control on attribute. You can thus change their value." ] }, { "cell_type": "code", "execution_count": 7, "id": "7491307e-d3d2-4c7b-aeda-eaaef439268c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "-60" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# not any control\n", "l.width = -5\n", "l.get_area()" ] }, { "cell_type": "markdown", "id": "3416ef32-ffed-43b2-bdeb-199bd5110b8c", "metadata": {}, "source": [ "All python object are fully dynamic. Attribute are thus created on the fly even\n", "on instances." ] }, { "cell_type": "code", "execution_count": 11, "id": "c47dd7fc-30f9-40bc-a033-5efe26b3182d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'width': -5, 'length': 12, 'color': 'green', 'weight': 18}" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# really no control\n", "l.weight = 18\n", "vars(l)" ] }, { "cell_type": "markdown", "id": "ef4e0ae1-c9e8-4b37-8695-0563557ab891", "metadata": {}, "source": [ "## Define properties" ] }, { "cell_type": "code", "execution_count": 14, "id": "fe57739b-6203-4689-b098-7b8350a566df", "metadata": {}, "outputs": [], "source": [ "class Leaf:\n", " \"\"\" This class represents a leaf's tree \"\"\"\n", " \n", " def __init__(self, width: float, length: float, color: str = \"green\"):\n", " \n", " self.width = width\n", " self.length = length\n", " self.color = color\n", " \n", " # for the sake of efficiency it is better to compute it one times in\n", " # the init. If you don't, you will compute it each time you use the\n", " # property\n", " self._area = self.width * self.length\n", " \n", " @property\n", " def area(self):\n", " \"\"\" Area of the leaf \"\"\"\n", " return self._area" ] }, { "cell_type": "code", "execution_count": 15, "id": "aebb08ab-aace-4795-8990-082119b97610", "metadata": {}, "outputs": [], "source": [ "l = Leaf(4, 12)" ] }, { "cell_type": "markdown", "id": "34639bb0-ea44-4b21-843b-c2137a8871ad", "metadata": {}, "source": [ "`area` is now a property and no more a function/method." ] }, { "cell_type": "code", "execution_count": 16, "id": "9a7a9215-ca7c-4fcd-ba9c-5b8dad0ba819", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "48" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l.area" ] }, { "cell_type": "markdown", "id": "d4f045d5-f9ed-4d01-b1ad-c18dec6ad81a", "metadata": {}, "source": [ "You are still able to set the `width` or other attributres but not the `area` which is a property.\n", "\n", "This leads to strange or erroneous behavior." ] }, { "cell_type": "code", "execution_count": 23, "id": "dcb5dddd-17a0-458b-827a-bfdef9030348", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "48" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l.width = -5\n", "l.area" ] }, { "cell_type": "code", "execution_count": 29, "id": "faa48669-b690-4ff2-a76a-76c5e672f1e9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "can't set attribute\n" ] } ], "source": [ "try:\n", " l.area = -5\n", "except AttributeError as error:\n", " print(\"#ERROR#\", error)" ] }, { "cell_type": "markdown", "id": "ad37a9d5-a3a2-4fcf-be56-48e1847fc86e", "metadata": {}, "source": [ "## Define more properties\n", "\n", "We assume the following:\n", "\n", "* area is a property computed from init data\n", "* width and length cannot be changed after the instance is created" ] }, { "cell_type": "code", "execution_count": 31, "id": "76b9cbbb-abf2-48bc-9f3a-77ff0176c8d2", "metadata": {}, "outputs": [], "source": [ "class Leaf:\n", " \"\"\" This class represents a leaf's tree \"\"\"\n", " \n", " def __init__(self, width: float, length: float, color: str = \"green\"):\n", " \n", " self._width = width\n", " self._length = length\n", " self.color = color\n", " \n", " # for the sake of efficiency it is better to compute it one times in\n", " # the init. If you don't, you will compute it each time you use the\n", " # property\n", " self._area = self.width * self.length\n", " \n", " @property\n", " def area(self):\n", " \"\"\" Area of the leaf \"\"\"\n", " return self._area\n", " \n", " @property\n", " def width(self):\n", " \"\"\" leaf's width \"\"\"\n", " return self._width\n", "\n", " @property\n", " def length(self):\n", " \"\"\" leaf's width \"\"\"\n", " return self._length" ] }, { "cell_type": "code", "execution_count": 32, "id": "34fc8eda-feca-4f28-a0ef-3bc86faba91e", "metadata": {}, "outputs": [], "source": [ "l = Leaf(4, 12)" ] }, { "cell_type": "code", "execution_count": 33, "id": "1ec2180c-b819-4697-8422-06b8b9d398cf", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "can't set attribute\n" ] } ], "source": [ "try:\n", " l.width = -5\n", "except AttributeError as error:\n", " print(\"#ERROR#\", error)" ] }, { "cell_type": "code", "execution_count": 40, "id": "8554daeb-9359-4b7a-8a40-4a887a8a227f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "can't set attribute\n" ] } ], "source": [ "try:\n", " l.length = -5\n", "except AttributeError as error:\n", " print(\"#ERROR#\", error)" ] }, { "cell_type": "markdown", "id": "5887e859-aa72-4cfd-9916-e78c14b0d9b1", "metadata": {}, "source": [ "But it is still possible to modify hidden attributes" ] }, { "cell_type": "code", "execution_count": 41, "id": "bab07702-fe4e-4513-b4e3-7f81d8ccec48", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'_width': 4, '_length': 12, 'color': 'green', '_area': 48}\n" ] } ], "source": [ "print(vars(l))" ] }, { "cell_type": "code", "execution_count": 46, "id": "5b3e2330-05e6-424d-8f86-4c8efc42f51c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'_width': -5, '_length': 12, 'color': 'green', '_area': 48}\n", "area = 48\n" ] } ], "source": [ "l._width = - 5\n", "print(vars(l))\n", "print(\"area =\", l.area)" ] }, { "cell_type": "markdown", "id": "4bdf4746-e6fc-4257-9c0d-108e63691118", "metadata": {}, "source": [ "## Define properties and setters\n", "\n", "We assume the following:\n", "\n", "* area is a property computed from instance data\n", "* width and length can be update but with control" ] }, { "cell_type": "code", "execution_count": 49, "id": "0dcb56a9-2c42-4c81-90c5-9d29db887de4", "metadata": {}, "outputs": [], "source": [ "class Leaf:\n", " \"\"\" This class represents a leaf's tree \"\"\"\n", " \n", " def __init__(self, width: float, length: float, color: str = \"green\"):\n", " \n", " self._width = width\n", " self._length = length\n", " self.color = color\n", " \n", " # for the sake of efficiency it is better to compute it one times in\n", " # the init. If you don't, you will compute it each time you use the\n", " # property\n", " self._area = self.width * self.length\n", " \n", " @property\n", " def area(self):\n", " \"\"\" Area of the leaf \"\"\"\n", " return self._area\n", " \n", " @property\n", " def width(self):\n", " \"\"\" leaf's width \"\"\"\n", " return self._width\n", " \n", " @width.setter\n", " def width(self, val):\n", " \"\"\" check width and upadte self \"\"\"\n", " if val > 0:\n", " self._width = val\n", " self._area = self._width * self._length\n", " else:\n", " raise ValueError(f\"Wrong width values: '{val}'\")\n", "\n", " # other option to define property\n", " def get_length(self):\n", " \"\"\" leaf's width \"\"\"\n", " return self._length\n", " \n", " def set_length(self, val):\n", " \"\"\" check width and upadte self \"\"\"\n", " if val > 0:\n", " self._length = val\n", " self._area = self._width * self._length\n", " else:\n", " raise ValueError(f\"Wrong width values: '{val}'\")\n", " \n", " length = property(get_length, set_length)" ] }, { "cell_type": "code", "execution_count": 57, "id": "e1e66333-988b-464e-86bd-a448f76ac0aa", "metadata": {}, "outputs": [], "source": [ "l = Leaf(4, 12)" ] }, { "cell_type": "code", "execution_count": 59, "id": "86caaf71-20b3-408f-bf0c-bf3c6c23c47e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'_width': 5, '_length': 24, 'color': 'green', '_area': 120} area = 120\n", "{'_width': 5, '_length': 24, 'color': 'green', '_area': 120} area = 120\n", "{'_width': 5, '_length': 24, 'color': 'green', '_area': 120} area = 120\n", "#ERROR# Wrong width values: '-1'\n" ] } ], "source": [ "print(vars(l), \" area =\", l.area)\n", "l.length = 24\n", "print(vars(l), \" area =\", l.area)\n", "l.width = 5\n", "print(vars(l), \" area =\", l.area)\n", "try:\n", " l.width = -1\n", "except ValueError as error:\n", " print(\"#ERROR#\", error)" ] }, { "cell_type": "markdown", "id": "8ff52eb3-737f-436e-a46a-d88675e92260", "metadata": {}, "source": [ "## Fast class writing \n", "\n", "There are several class factories in python that write some parts of the code for you. For\n", "example the special methods `__init__` or `__repr__` or others.\n", "\n", "### Use NamedTuple\n", "\n", "`namedtuple` is a very fast way to get a datacontainer with imutable fields (as tuples)." ] }, { "cell_type": "code", "execution_count": 71, "id": "4a363f8c-0fdb-4dd0-8259-8fde8dee519b", "metadata": {}, "outputs": [], "source": [ "from collections import namedtuple" ] }, { "cell_type": "code", "execution_count": 102, "id": "db0662ca-0c08-4b74-a226-365cb80ae410", "metadata": {}, "outputs": [], "source": [ "Leaf = namedtuple(\"Leaf\", [\"width\", \"length\", \"color\"], defaults=[\"green\"])" ] }, { "cell_type": "code", "execution_count": 103, "id": "0b625522-b881-479e-adbc-e14da036f7cc", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Leaf(width=4, length=12, color='green')" ] }, "execution_count": 103, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l = Leaf(4, 12)\n", "l" ] }, { "cell_type": "code", "execution_count": 104, "id": "6c41e5d4-3582-4905-8c98-233a00348b4a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 104, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l2 = Leaf(4, 12)\n", "l == l2 # this comparison was not available in previous implementation." ] }, { "cell_type": "code", "execution_count": 105, "id": "1d9df4a6-3ab5-4128-b041-018dea1daa4d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(4, 12)" ] }, "execution_count": 105, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l.width, l.length" ] }, { "cell_type": "code", "execution_count": 106, "id": "25482abf-dfff-4a86-bc1f-a5682410c496", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4\n", "12\n", "green\n" ] } ], "source": [ "for item in l:\n", " print(item)" ] }, { "cell_type": "code", "execution_count": 107, "id": "c8a25596-e3f6-4b60-b981-db0eabf8d848", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4\n", "12\n", "green\n" ] } ], "source": [ "for i in range(3):\n", " print(l[i])" ] }, { "cell_type": "markdown", "id": "3e8ac142-026a-4f78-aa65-8d51a62e3429", "metadata": {}, "source": [ "NamedTuple are immutable object. You cannot set attributes." ] }, { "cell_type": "code", "execution_count": 109, "id": "468854e9-94ce-4b9c-802d-0a647afea296", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "can't set attribute\n" ] } ], "source": [ "try:\n", " l.length = 5\n", "except AttributeError as error:\n", " print(error)" ] }, { "cell_type": "code", "execution_count": 111, "id": "fa3407fa-45e2-4a0a-b1d3-ef0c41d7cd84", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "'Leaf' object has no attribute 'area'\n" ] } ], "source": [ "try:\n", " l.area = l.length * l.width\n", "except AttributeError as error:\n", " print(error)" ] }, { "cell_type": "markdown", "id": "446566ef-ac47-4028-b12d-780954eb9e2c", "metadata": {}, "source": [ "NamedTuple is very fast but very basic. For example, here `area` is not available. You can however extend it:" ] }, { "cell_type": "code", "execution_count": 112, "id": "9e8b17f5-86a2-4e8a-8894-c068f740c649", "metadata": {}, "outputs": [], "source": [ "class Leaf(namedtuple(\n", " \"Leaf\", [\"width\", \"length\", \"color\"], defaults=[\"green\"])):\n", " \n", " @property\n", " def area(self):\n", " return self.width * self.length" ] }, { "cell_type": "code", "execution_count": 114, "id": "51f35f8b-cf6f-4c5b-9bc0-2bc0dd4c33c7", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Leaf(width=4, length=12, color='green')" ] }, "execution_count": 114, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l = Leaf(4, 12)\n", "l" ] }, { "cell_type": "code", "execution_count": 115, "id": "29b346b7-63e6-4105-a373-8c471fa7e7e2", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "48" ] }, "execution_count": 115, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l.area" ] }, { "cell_type": "markdown", "id": "0fa3247e-d5ef-49db-ac19-1f7be00c9a80", "metadata": {}, "source": [ "### Dataclass Example\n", "\n", "The aim of dataclass is to be a container. By default, the aim is not to protect\n", "data. In the following attributed are thus editable." ] }, { "cell_type": "code", "execution_count": 88, "id": "6f30ac4f-8867-41c8-b947-e191811cf0e5", "metadata": {}, "outputs": [], "source": [ "from dataclasses import dataclass, field" ] }, { "cell_type": "code", "execution_count": 116, "id": "24531d86-4d38-4532-9d65-51331f087dec", "metadata": {}, "outputs": [], "source": [ "@dataclass\n", "class Leaf:\n", " \"\"\" This class represents a leaf's tree \"\"\"\n", " \n", " width: float\n", " length: float\n", " color: str = \"green\"\n", " \n", " area: float = field(init=False)\n", " \n", " def __post_init__(self):\n", " self.area = self.width * self.length" ] }, { "cell_type": "code", "execution_count": 122, "id": "3b68cbce-ce3d-4c75-be02-be5a8a8336d4", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Leaf(width=4, length=12, color='green', area=48)" ] }, "execution_count": 122, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l = Leaf(4, 12)\n", "l" ] }, { "cell_type": "code", "execution_count": 123, "id": "915cefe4-bc7f-41f5-b741-d360a7bbb876", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "48" ] }, "execution_count": 123, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l.area" ] }, { "cell_type": "code", "execution_count": 124, "id": "a10c937a-4d8c-47f5-9e84-2b24b5862a01", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'width': 4, 'length': 12, 'color': 'green', 'area': 48}" ] }, "execution_count": 124, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vars(l)" ] }, { "cell_type": "code", "execution_count": 125, "id": "e9685940-3fb8-4649-8df2-2c9c3da417f3", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'width': 4, 'length': 2, 'color': 'green', 'area': 48}" ] }, "execution_count": 125, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# you loose the control on attributed relationship\n", "l.length = 2\n", "vars(l)" ] }, { "cell_type": "markdown", "id": "06a84fa0-859a-41ae-9c15-17c86dee38ea", "metadata": {}, "source": [ "### Kind of Dataclass with validation\n", "\n", "Fine tuning of container classes can be achieve using pydantic `BaseModel`." ] }, { "cell_type": "code", "execution_count": 143, "id": "d213b50a-fb2e-4cc8-a98e-9576a4e87678", "metadata": {}, "outputs": [], "source": [ "from enum import Enum\n", "from pydantic import BaseModel, NonNegativeFloat, ValidationError, validator" ] }, { "cell_type": "code", "execution_count": 156, "id": "3ed3bc76-74eb-46c5-8e82-164224b82ff3", "metadata": {}, "outputs": [], "source": [ "class ColorChoice(str, Enum):\n", " green = \"green\"\n", " very_green = \"very_green\"\n", " so_green = \"so_green\"\n", "\n", "class Leaf(BaseModel):\n", " width: NonNegativeFloat\n", " length: NonNegativeFloat\n", " color: ColorChoice = ColorChoice.green\n", " \n", " @validator(\"length\")\n", " def length_lt_width_validation(cls, v, values):\n", " if v < values[\"width\"]:\n", " raise ValueError(f\"Length must be larger than width.\")\n", " else:\n", " return v\n", "\n", " @property\n", " def area(self):\n", " return self.width * self.length" ] }, { "cell_type": "code", "execution_count": 148, "id": "b413e670-b09d-4fe1-bb7b-01d91ba8228c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Leaf(width=3.0, length=8.0, color=)" ] }, "execution_count": 148, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l = Leaf(width=3, length=8)\n", "l" ] }, { "cell_type": "code", "execution_count": 149, "id": "c292081f-b16d-4a81-8f1e-55283fef83e0", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "24.0" ] }, "execution_count": 149, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l.area" ] }, { "cell_type": "code", "execution_count": 138, "id": "76e8ca7e-e918-434e-b803-8793ac33f085", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 validation error for Leaf\n", "width\n", " ensure this value is greater than or equal to 0 (type=value_error.number.not_ge; limit_value=0)\n" ] } ], "source": [ "try:\n", " Leaf(width=-2, length=2)\n", "except ValidationError as error:\n", " print(error)" ] }, { "cell_type": "code", "execution_count": 153, "id": "3b454c4e-4e5a-441e-a0b9-ec35791f0537", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 validation error for Leaf\n", "length\n", " Length must be larger than width. (type=value_error)\n" ] } ], "source": [ "try:\n", " Leaf(width=12, length=2)\n", "except ValueError as error:\n", " print(error)" ] }, { "cell_type": "code", "execution_count": 157, "id": "0cbe2d6e-869e-4eb9-9c9d-bb7b82218b32", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'width': 3.0, 'length': 8.0, 'color': }" ] }, "execution_count": 157, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l.dict()" ] }, { "cell_type": "code", "execution_count": 158, "id": "d2f30713-aa7f-48ac-a707-2277db11ff6f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'{\"width\": 3.0, \"length\": 8.0, \"color\": \"green\"}'" ] }, "execution_count": 158, "metadata": {}, "output_type": "execute_result" } ], "source": [ "l.json()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.7" } }, "nbformat": 4, "nbformat_minor": 5 }