From 4a0ac4a74b75f8806b0ee64f29041884cf49448b Mon Sep 17 00:00:00 2001 From: James Turk Date: Mon, 4 Nov 2024 14:26:13 -0600 Subject: [PATCH] 14 --- 14.more-classes.ipynb | 1170 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1170 insertions(+) create mode 100644 14.more-classes.ipynb diff --git a/14.more-classes.ipynb b/14.more-classes.ipynb new file mode 100644 index 0000000..bddf981 --- /dev/null +++ b/14.more-classes.ipynb @@ -0,0 +1,1170 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "33fdf2a8-ee0f-4340-8cec-a43546275939", + "metadata": {}, + "source": [ + "# Class Methods & Properties" + ] + }, + { + "cell_type": "markdown", + "id": "98e7e0ac-17fc-46ec-98ab-9435789a471c", + "metadata": {}, + "source": [ + "### Class Attributes\n", + "\n", + "Sometimes we want to share data between all instances of a given class.\n", + "\n", + "All cars have 4 wheels, so we could define a shared variable accessible to all instances of the `Car` class.\n", + "\n", + "To do this, we create them within the `class` body, usually right above the `__init__`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "aea2b16e-3012-4ce5-a806-7a2f0f712c08", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import datetime\n", + "\n", + "class Car:\n", + " # class attribute\n", + " wheels = 4\n", + " registrations = []\n", + " \n", + " def __init__(self, make, model, year):\n", + " self.make = make \n", + " self.model = model \n", + " self.year = year\n", + " self.registrations = []\n", + " \n", + " def compute_age(self):\n", + " return datetime.date.today().year - self.year \n", + " \n", + " \n", + "car1 = Car(\"Honda\", \"Accord\", 2019)\n", + "car2 = Car(\"Toyota\", \"RAV4\", 2006)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a3aee627-9404-4c08-9571-6c1206ed9790", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4\n", + "4\n", + "4\n" + ] + } + ], + "source": [ + "# class attribute can be accessed on instances, or the class itself\n", + "\n", + "print(Car.wheels)\n", + "print(car1.wheels)\n", + "print(car2.wheels)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "61f681e4-0105-4b20-8d72-dfd5a347df8a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# these are all the same variable\n", + "Car.wheels is car1.wheels" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2bf5dbb8-c432-49e3-970c-3b6ecfdef5f7", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n", + "3\n" + ] + } + ], + "source": [ + "# which means changes to the class attribute\n", + "# will modify for all classes \n", + "\n", + "Car.wheels = 3\n", + "print(car1.wheels)\n", + "print(car2.wheels)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9997fc8a-6562-4e0a-8be9-9dc34446f5bd", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "False\n", + "3\n", + "3\n" + ] + } + ], + "source": [ + "# note: assigning to an instance attribute makes a new attribute\n", + "\n", + "# creates a new instance variable!\n", + "car2.wheels = 2\n", + "print(car2.wheels is car1.wheels)\n", + "print(car1.wheels)\n", + "print(Car.wheels)" + ] + }, + { + "cell_type": "markdown", + "id": "a222dcb1-fcfa-43c8-bf4b-13ad9626b6c3", + "metadata": {}, + "source": [ + "### Class Methods\n", + "\n", + "It can also be useful to provide methods that are accessible to all instances of a class.\n", + "\n", + "Class methods are similar to instance methods with a few distinctions:\n", + "\n", + "1. They can not access instance methods or attributes.\n", + "2. The first argument to the method is not `self`, but instead `cls` by convention. `cls` is the class object itself (e.g. `Car`)\n", + "3. Class methods are declared with the `@classmethod` decorator." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "95bcb4f6-d11e-4074-90d3-63dc6a0f19eb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from datetime import date\n", + "\n", + "class Car: \n", + " \n", + " # wheels class attribute \n", + " wheels = 4\n", + " # tire pressure class attribute \n", + " psi = 35 \n", + " \n", + " def __init__(self, make, model, year):\n", + " self.make = make \n", + " self.model = model \n", + " self.year = year\n", + " \n", + " def compute_age(self):\n", + " current_year = int(date.today().year)\n", + " return current_year - self.year \n", + " \n", + " @classmethod \n", + " def tire_description(cls):\n", + " print(cls)\n", + " return f'Car has {cls.wheels} wheels with a tire pressure of {Car.psi}' \n", + " \n", + "car1 = Car(\"Honda\", \"Accord\", 2019)\n", + "car2 = Car(\"Toyota\", \"RAV4\", 2006)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "0805ad8c-4b7a-48e2-9ff9-be57a03067dc", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Car has 4 wheels with a tire pressure of 35\n", + "\n", + "Car has 4 wheels with a tire pressure of 35\n" + ] + } + ], + "source": [ + "print(Car.tire_description())\n", + "print(car1.tire_description())" + ] + }, + { + "cell_type": "markdown", + "id": "cdea319d-db59-4342-bbc3-41d1bf17e1c7", + "metadata": {}, + "source": [ + "Notice that we can use `Car.psi` or `cls.wheels` to access class attributes. `cls` is generally preferred, both to avoid repetition and for reasons we'll see when we get to inheritance.\n", + "\n", + "Finally, note that we can access class methods and instances from within instance methods. (but not vice-versa!)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "718fb1d6-7bd8-47db-b3c6-5f2c4b69056e", + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import date\n", + "class Car: \n", + " \n", + " # wheels class attribute \n", + " wheels = 4\n", + " \n", + " # tire pressure amount \n", + " psi = 35 \n", + " \n", + " def __init__(self, make, model, year):\n", + " self.make = make \n", + " self.model = model \n", + " self.year = year\n", + " \n", + " def compute_age(self):\n", + " current_year = int(date.today().year)\n", + " return current_year - self.year \n", + " \n", + " @classmethod \n", + " def tire_description(cls):\n", + " return f'Car has {cls.wheels} wheels, each with a tire pressure of {Car.psi}' \n", + "\n", + " def __repr__(self): \n", + " instance_str = f'Car(make={self.make}, model={self.model}, year={self.year}, '\n", + " instance_str += f'wheels={Car.wheels}, {self.tire_description()})'\n", + " return instance_str" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "fd34539f-abc4-4bb2-99f1-b49af1eb1570", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Car(make=Honda, model=Civic, year=2019, wheels=4, Car has 4 wheels, each with a tire pressure of 35)\n" + ] + } + ], + "source": [ + "car1 = Car(\"Honda\", \"Civic\", 2019)\n", + "print(car1)" + ] + }, + { + "cell_type": "markdown", + "id": "45037ff6-af90-4934-9eea-ad9b2d241cc8", + "metadata": {}, + "source": [ + "### Alternate Constructors\n", + "\n", + "A common use of class methods is to define alternate ways to initialize an isntance. In Python there can only be one constructor (`__init__`), whereas some other languages allow multiple.\n", + "\n", + "Perhaps we have Car data coming from a file, meaning we'd have strings like:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "a1c858ea-8222-4776-a3b7-9f7443bd918f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "car1str = \"Pontiac|Grand Am|1997\"\n", + "car2str = \"Ford|Mustang|1970\"\n", + "car3str = \"Hyundai|Sonata|2007\"" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "5c10ac8b-deb5-4a23-9beb-d0d32306552d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from datetime import date\n", + "\n", + "class Car: \n", + " wheels = 4\n", + " psi = 35\n", + " \n", + " def __init__(self, make, model, year):\n", + " self.make = make \n", + " self.model = model \n", + " self.year = year\n", + " \n", + " @classmethod\n", + " def from_string(cls, string):\n", + " make, model, year = string.split(\"|\")\n", + " # invoke Car's constructor\n", + " return cls(make, model, year)\n", + " \n", + " def compute_age(self):\n", + " current_year = int(date.today().year)\n", + " return current_year - self.year \n", + " \n", + " @classmethod \n", + " def tire_description(cls):\n", + " return f'Car has {cls.wheels} wheels, each with a tire pressure of {Car.psi}' \n", + "\n", + " def __repr__(self): \n", + " instance_str = f'Car(make={self.make}, model={self.model}, year={self.year}, '\n", + " instance_str += f'wheels={Car.wheels})'\n", + " return instance_str" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "39b783df-a6a6-4a6e-b33e-a20ec18f4671", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "car1 = Car.from_string(car1str)\n", + "car2 = Car.from_string(car2str)\n", + "car3 = Car.from_string(car3str)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "b5cde4b1-168f-48bd-ac95-b0ca87c66c5a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Car(make=Pontiac, model=Grand Am, year=1997, wheels=4)\n", + "Car(make=Ford, model=Mustang, year=1970, wheels=4)\n", + "Car(make=Hyundai, model=Sonata, year=2007, wheels=4)\n" + ] + } + ], + "source": [ + "print(car1)\n", + "print(car2)\n", + "print(car3)" + ] + }, + { + "cell_type": "markdown", + "id": "c0001b40-39e0-4502-9d9c-563ed14b9978", + "metadata": {}, + "source": [ + "This is a common pattern, seen throughout Python:\n", + "\n", + " - ``int.from_bytes()``\n", + " - ``float.fromhex()`` \n", + " - ``datetime.date.fromtimestamp()``\n", + " - ``itertools.chain.from_iterable()``\n" + ] + }, + { + "cell_type": "markdown", + "id": "c4d1ad9f-b280-4bcf-af74-bf8dc931dbab", + "metadata": {}, + "source": [ + "### staticmethod\n", + "\n", + "Sometimes it makes sense to just attach a method to a class for the purpose of namespacing." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "75dc6b84-f24c-4f95-a718-6c802c767e04", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Car(make=Pontiac, model=Grand Am, year=1997, wheels=4)" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def which_is_newer(a, b):\n", + " if a.year > b.year:\n", + " return a\n", + " else:\n", + " return b\n", + "\n", + "which_is_newer(car1, car2)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "4c412729-d23c-470a-baa3-b9aab42beb84", + "metadata": {}, + "outputs": [], + "source": [ + "# it might make sense to attach this to the class, \n", + "# but neither a classmethod nor an instance method\n", + "\n", + "from datetime import date\n", + "class Car: \n", + " wheels = 4\n", + " psi = 35\n", + " \n", + " # does not take self or cls\n", + " @staticmethod\n", + " def which_is_newer(a, b):\n", + " if a.year > b.year:\n", + " return a\n", + " else:\n", + " return b\n", + " \n", + " @staticmethod\n", + " def something():\n", + " return []\n", + " \n", + "\n", + " \n", + " def __init__(self, make, model, year):\n", + " self.make = make \n", + " self.model = model \n", + " self.year = year\n", + " \n", + " @classmethod\n", + " def from_string(cls, string):\n", + " make, model, year = string.split(\"|\")\n", + " # invoke Car's constructor\n", + " return cls(make, model, year)\n", + "\n", + " def __repr__(self): \n", + " instance_str = f'Car(make={self.make}, model={self.model}, year={self.year}, '\n", + " instance_str += f'wheels={Car.wheels})'\n", + " return instance_str" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "24ffc511-9540-4777-a71e-63a020705b3b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Car(make=Pontiac, model=Grand Am, year=1997, wheels=4)" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# now would be called this way\n", + "Car.which_is_newer(car1, car2)" + ] + }, + { + "cell_type": "markdown", + "id": "8100f484-0abd-495d-8580-c9ba1946e758", + "metadata": {}, + "source": [ + "### Encapsulation\n", + "\n", + ">``[Encapsulation] allows the implementation of an object's interface to be changed without impacting the users of that object.\"\n", + "\n", + "The main idea of encapsulation is to hide implementation details from the users of an object. You only expose a public interface to the users.\n", + "\n", + "There are a few ways to encapsulation is handled in Python: \n", + "\n", + "- Private attributes using underscores\n", + "- Getter/Setters\n", + "- Properties\n", + "\n", + "### Private Attributes\n", + "\n", + "We saw last week, if we define class attributes with double underscores they are not accessible outside the class." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "5833020e-398c-4947-8698-0fa3183e63ab", + "metadata": {}, + "outputs": [], + "source": [ + "class Example:\n", + " def __init__(self, x, y, z):\n", + " self.x = x\n", + " self._y = y\n", + " self.__z = z\n", + " \n", + " def __repr__(self):\n", + " return f\"Example({self.x}, {self._y}, {self.__z})\"\n", + "\n", + "instance = Example(1, 2, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "f76bc0e2-4454-4c66-9ff4-8602ee4e35d0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# normal public attribute\n", + "instance.x" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "857f934a-3b7c-4fcb-bdfa-f802e302c966", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# single underscore attributes are private by convention only\n", + "# (there is no enforcement)\n", + "instance._y" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "69902de2-aa03-4132-8c9a-13a9e954877b", + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'Example' object has no attribute '__z'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[23], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# double underscore methods are name-mangled\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43minstance\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m__z\u001b[49m\n", + "\u001b[0;31mAttributeError\u001b[0m: 'Example' object has no attribute '__z'" + ] + } + ], + "source": [ + "# double underscore methods are name-mangled\n", + "instance.__z" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43e08993-7858-4c91-b4b1-98d6bbe5b359", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "d7b06268-0bc1-4d70-9c54-eca2f728b590", + "metadata": {}, + "source": [ + "### Getters / Setters\n", + "\n", + "Another common pattern to hide data in OOP languages is to use getter and setter methods that control access." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "b5d16735-9280-4002-a4ba-bc267fd52ff2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, name, age):\n", + " self.__name = name # Assume it has getter/setters \n", + " self.set_age(age)\n", + "\n", + " def get_age(self):\n", + " return self.__age\n", + "\n", + " def set_age(self, age):\n", + " if age < 0:\n", + " raise ValueError(\"Person can't have a negative age!\")\n", + " self.__age = age\n", + " \n", + " def set_name(self, name):\n", + " if \" \" not in name:\n", + " raise ValueError(\"must be at least two words\")\n", + " self.__name = name" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "1923f424-f9a4-4bb1-bd30-5847ecbc3064", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "p = Person(\"Joe Biden\", 79)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "74f206d8-3ecd-464d-a3ca-8a17f390f6ac", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "79" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p.get_age()" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "75b8d93c-c2f7-4712-9934-8ccf0ad22439", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "p.set_age(80)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "c89dd611-adec-405f-9ac8-0fb65a68e35d", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "80" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p.get_age()" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "af8b33ec-388a-4d9a-98c0-d5a824eede66", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "Person can't have a negative age!", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[29], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mset_age\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[24], line 11\u001b[0m, in \u001b[0;36mPerson.set_age\u001b[0;34m(self, age)\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mset_age\u001b[39m(\u001b[38;5;28mself\u001b[39m, age):\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m age \u001b[38;5;241m<\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m---> 11\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mPerson can\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt have a negative age!\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 12\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m__age \u001b[38;5;241m=\u001b[39m age\n", + "\u001b[0;31mValueError\u001b[0m: Person can't have a negative age!" + ] + } + ], + "source": [ + "p.set_age(-1)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "9705d447-8568-476e-97ab-541b61f05002", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "80" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p.get_age()" + ] + }, + { + "cell_type": "markdown", + "id": "e339267b-e14d-4665-99e6-63a57ee9c575", + "metadata": {}, + "source": [ + "This can become very tedious, and as we've seen they don't actually protect access to variables. Therefore we typically **avoid getters and setters in Python.**" + ] + }, + { + "cell_type": "markdown", + "id": "a3b96d2d-5050-4973-a69b-469a0da63c40", + "metadata": {}, + "source": [ + "### Properties\n", + "\n", + "We want the advantages of encapsulation (being able to avoid improper use, hiding our internal representation, etc.) but without the need to start with a bunch of getter/setter functions from the get go.\n", + "\n", + "Python has a much nicer way to control access to attributes via **properties**.\n", + "\n", + "There is a built in function `property()` that creates and returns a property object.\n", + "\n", + "`property(fget=None, fset=None, fdel=None, doc=None)`\n", + "\n", + "- `fget` is a function to get value of the attribute\n", + "- `fset` is a function to set value of the attribute\n", + "- `fdel` is a function to delete the attribute\n", + "- `doc` is a docstring for the attribute" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "b92490b9-867a-461b-b922-a54235ca9463", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, name, age):\n", + " self.name = name # Assume it has getter/setters \n", + " self.age = age\n", + "\n", + " def get_age(self):\n", + " print(\"inside get age\")\n", + " return self.__age\n", + "\n", + " def set_age(self, age):\n", + " if age < 0:\n", + " raise ValueError(\"Person can't have a negative age!\")\n", + " self.__age = age\n", + " \n", + " def __repr__(self):\n", + " return f\"Person({self.__name!r}, {self.__age})\"\n", + " \n", + " age = property(get_age, set_age, doc=\"age of the person\")" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "c32cd2dd-cb18-4858-b662-19790b74571e", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['_Person__age',\n", + " '__class__',\n", + " '__delattr__',\n", + " '__dict__',\n", + " '__dir__',\n", + " '__doc__',\n", + " '__eq__',\n", + " '__format__',\n", + " '__ge__',\n", + " '__getattribute__',\n", + " '__gt__',\n", + " '__hash__',\n", + " '__init__',\n", + " '__init_subclass__',\n", + " '__le__',\n", + " '__lt__',\n", + " '__module__',\n", + " '__ne__',\n", + " '__new__',\n", + " '__reduce__',\n", + " '__reduce_ex__',\n", + " '__repr__',\n", + " '__setattr__',\n", + " '__sizeof__',\n", + " '__str__',\n", + " '__subclasshook__',\n", + " '__weakref__',\n", + " 'age',\n", + " 'get_age',\n", + " 'name',\n", + " 'set_age']" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p = Person(\"Wayne\", 30)\n", + "dir(p)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "30497dd2-ebf6-44f8-a4f9-d37b4384b06a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "Person can't have a negative age!", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[33], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mage\u001b[49m \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m\n", + "Cell \u001b[0;32mIn[31], line 12\u001b[0m, in \u001b[0;36mPerson.set_age\u001b[0;34m(self, age)\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mset_age\u001b[39m(\u001b[38;5;28mself\u001b[39m, age):\n\u001b[1;32m 11\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m age \u001b[38;5;241m<\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m---> 12\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mPerson can\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt have a negative age!\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 13\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m__age \u001b[38;5;241m=\u001b[39m age\n", + "\u001b[0;31mValueError\u001b[0m: Person can't have a negative age!" + ] + } + ], + "source": [ + "p.age = -1" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "06a9c879-8c84-4b57-b585-b162e6c489fa", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "inside get age\n", + "30\n" + ] + } + ], + "source": [ + "print(p.age)" + ] + }, + { + "cell_type": "markdown", + "id": "725eb530-d85e-4c9d-979f-00ad1a12a0b7", + "metadata": {}, + "source": [ + "#### @property\n", + "\n", + "We can also use `property` as a decorator. \n", + "\n", + "- Place the `@property` directly above the function header of the getter function.\n", + "\n", + "- Place the code `@name_of_property.setter` above the function header of the setter function. You need to replace the name_of_property with the actual name of the property.\n", + "\n", + "- The function names for both the setter/getter need to match." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "abe093f5-b911-434b-8071-00c9d177e5bf", + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, name, age):\n", + " self.__name = name # Assume it has getter/setters \n", + " # invokes setter\n", + " self.age = age\n", + "\n", + " @property\n", + " def age(self):\n", + " \"\"\" returns the age property \"\"\"\n", + " return self.__age\n", + " #age = property(age)\n", + " \n", + " @age.setter\n", + " def age(self, age):\n", + " if age < 0:\n", + " raise ValueError(\"Person can't have a negative age!\")\n", + " self.__age = age\n", + " \n", + " def __repr__(self):\n", + " return f\"Person({self.__name!r}, {self.__age})\"" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "3bc6ff3e-9dc3-4b48-876b-3a7a98911da4", + "metadata": {}, + "outputs": [], + "source": [ + "p2 = Person(\"Emma\", 28)\n", + "#p2.age = -1\n", + "p2.age = 4" + ] + }, + { + "cell_type": "markdown", + "id": "e492919b-2c90-4723-9a11-4cc50feb086f", + "metadata": {}, + "source": [ + "This allows us to start class attributes as public, and add properties as needed." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "4eb66674-f3c2-40fa-8d75-9aa08fcafa87", + "metadata": {}, + "outputs": [], + "source": [ + "class Point:\n", + " def __init__(self, x, y):\n", + " self.x = x \n", + " self.y = y" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "60455fce-fd92-41a3-a770-7b6a9f348552", + "metadata": {}, + "outputs": [], + "source": [ + "p = Point(10, 10)" + ] + }, + { + "cell_type": "markdown", + "id": "06d8973f-0eb4-45f9-b234-7680b51775b3", + "metadata": {}, + "source": [ + "#### Read-only/Calculated Properties" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "0c9fc2ed-9a6e-41d3-a1bb-a7c13641d617", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class Rectangle: \n", + " \n", + " def __init__(self,width,height):\n", + " self.width = width \n", + " self.height = height \n", + " \n", + " # read-only calculated property\n", + " @property \n", + " def area(self):\n", + " return self.width * self.height " + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "2aa20ea3-1292-4224-9d99-b40eab748021", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "r = Rectangle(3, 9)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "a03f9038-7b97-4e0b-a46e-b8d558f12882", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "27\n" + ] + } + ], + "source": [ + "print(r.area)" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "7d9ce39e-bfc3-4389-9315-a9bb16dcdec0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "54\n" + ] + } + ], + "source": [ + "# area is dynamically calculated each call\n", + "r.width = 6\n", + "print(r.area)" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "489244f1-9568-4c73-93c8-86cb6ffecfe4", + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "can't set attribute 'area'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[48], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# but can't be set\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43mr\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43marea\u001b[49m \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m4\u001b[39m\n", + "\u001b[0;31mAttributeError\u001b[0m: can't set attribute 'area'" + ] + } + ], + "source": [ + "# but can't be set\n", + "r.area = 4" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5ffcdd9-5c89-4cdb-84cc-81b255c2e28f", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}