{
 "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": 26,
   "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.wheels = 0\n",
    "        Car.registrations.append(self)\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": 27,
   "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": 28,
   "id": "61f681e4-0105-4b20-8d72-dfd5a347df8a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 28,
     "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": 31,
   "id": "9997fc8a-6562-4e0a-8be9-9dc34446f5bd",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "False\n",
      "4\n",
      "4\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": 32,
   "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",
    "        print(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": 35,
   "id": "0805ad8c-4b7a-48e2-9ff9-be57a03067dc",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class '__main__.Car'>\n",
      "Car has 4 wheels with a tire pressure of 35\n",
      "<__main__.Car object at 0x1034b7e50>\n",
      "5\n"
     ]
    }
   ],
   "source": [
    "print(Car.tire_description())\n",
    "#print(car1.tire_description())\n",
    "print(car1.compute_age())"
   ]
  },
  {
   "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": 40,
   "id": "a1c858ea-8222-4776-a3b7-9f7443bd918f",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "car1str = \"Pontiac|Grand Am|1997|4892\"\n",
    "car2str = \"Ford|Mustang|1970|800\"\n",
    "car3str = \"Hyundai|Sonata|2007|0\"\n",
    "\n",
    "\n",
    "def make_car_from_string(s: str) -> Car:\n",
    "    ..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "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",
    "        self.mileage = 0\n",
    "        \n",
    "    @classmethod\n",
    "    def from_string(cls, string):\n",
    "        make, model, year, mileage = string.split(\"|\")\n",
    "        # invoke Car's constructor\n",
    "        new_instance = cls(make, model, year)\n",
    "        new_instance.mileage = mileage\n",
    "        return new_instance\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": 42,
   "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": 43,
   "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": "code",
   "execution_count": 45,
   "id": "d673a408-4e12-49ae-b402-5179820e35fa",
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "map() must have at least two arguments.",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[45], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m x \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mlist\u001b[39m(\u001b[38;5;28;43mmap\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m)\u001b[49m)\n\u001b[1;32m      2\u001b[0m y \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mdict\u001b[39m(\u001b[38;5;241m.\u001b[39m\u001b[38;5;241m.\u001b[39m\u001b[38;5;241m.\u001b[39m)\n",
      "\u001b[0;31mTypeError\u001b[0m: map() must have at least two arguments."
     ]
    }
   ],
   "source": [
    "x = list(map(...))\n",
    "y = dict(...)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "3a5ed2dd-732f-4fb2-b776-475c60f2f0ec",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "datetime.date(2024, 11, 11)"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import datetime\n",
    "datetime.date(2024, 11, 11)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "dccd79cc-4f80-4609-bc11-d136beb1b9b9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "datetime.date(2009, 2, 13)"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "datetime.date.fromtimestamp(1234567890)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "id": "6d9f0442-429c-4ed4-8dc2-3bd6f5064f09",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1\n",
      "2\n",
      "3\n",
      "4\n",
      "5\n",
      "6\n"
     ]
    }
   ],
   "source": [
    "import itertools\n",
    "for x in itertools.chain.from_iterable([(1,2,3), (4,5,6)]):\n",
    "    print(x)\n",
    "#for x in (1,2,3):\n",
    "#    print(x)\n",
    "#for x in (4,5,6):\n",
    "#    print(x)"
   ]
  },
  {
   "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": 2,
   "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 _calculate_age_from_birthday():\n",
    "        pass\n",
    "\n",
    "    def get_age(self):\n",
    "        return self._calculate_age_from_birthday()\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": 3,
   "id": "1923f424-f9a4-4bb1-bd30-5847ecbc3064",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "p = Person(\"C. Montgomery Burns\", 100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "74f206d8-3ecd-464d-a3ca-8a17f390f6ac",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "100"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.get_age()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "75b8d93c-c2f7-4712-9934-8ccf0ad22439",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "p.set_age(101)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "c89dd611-adec-405f-9ac8-0fb65a68e35d",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "101"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.get_age()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "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[8], 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[2], 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": 9,
   "id": "9705d447-8568-476e-97ab-541b61f05002",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "101"
      ]
     },
     "execution_count": 9,
     "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": 16,
   "id": "b92490b9-867a-461b-b922-a54235ca9463",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "class Person:\n",
    "    \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": 17,
   "id": "c32cd2dd-cb18-4858-b662-19790b74571e",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "inside get age\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "1000"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p = Person(\"Wayne\", 30)\n",
    "p.age"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6c52d816-af8d-46bf-8e23-cc1417743868",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "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[15], 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[10], 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": 13,
   "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": 59,
   "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 #self.set_age(age)\n",
    "        self.birth_date = ...\n",
    "\n",
    "    @property\n",
    "    def age(self):\n",
    "        \"\"\" returns the age property \"\"\"\n",
    "        print('getter called')\n",
    "        return self.__age\n",
    "    #age = property(age)\n",
    "    \n",
    "    @age.setter\n",
    "    def age(self, age):\n",
    "        print('setter called')\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": 61,
   "id": "3bc6ff3e-9dc3-4b48-876b-3a7a98911da4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "setter called\n",
      "getter called\n",
      "28\n"
     ]
    }
   ],
   "source": [
    "p2 = Person(\"Emma\", 28)\n",
    "#p2.age = -1\n",
    "print(p2.age)"
   ]
  },
  {
   "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": 26,
   "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",
    "        self.area = width*height\n",
    "    \n",
    "    # read-only calculated property\n",
    "    #@property \n",
    "    #def area(self):\n",
    "    #    return self.width * self.height "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "2aa20ea3-1292-4224-9d99-b40eab748021",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "r = Rectangle(3, 9)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "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": 31,
   "id": "7d9ce39e-bfc3-4389-9315-a9bb16dcdec0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "27\n"
     ]
    }
   ],
   "source": [
    "# area is dynamically calculated each call\n",
    "r.width = 6\n",
    "print(r.area)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 78,
   "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[78], 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": 25,
   "id": "a5ffcdd9-5c89-4cdb-84cc-81b255c2e28f",
   "metadata": {},
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "can't delete 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[25], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mdel\u001b[39;00m r\u001b[38;5;241m.\u001b[39marea\n",
      "\u001b[0;31mAttributeError\u001b[0m: can't delete attribute 'area'"
     ]
    }
   ],
   "source": [
    "del r.area"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "952bb276-725e-43ea-835d-3ab4116696b6",
   "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
}
