{ "cells": [ { "cell_type": "markdown", "metadata": { "toc": true }, "source": [ "

Table of Contents

\n", "
" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# code for loading the format for the notebook\n", "import os\n", "\n", "# path : store the current path to convert back to it later\n", "path = os.getcwd()\n", "os.chdir(os.path.join('..', 'notebook_format'))\n", "\n", "from formats import load_style\n", "load_style(plot_style=False)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Ethen 2018-08-19 02:10:18 \n", "\n", "CPython 3.6.4\n", "IPython 6.4.0\n" ] } ], "source": [ "os.chdir(path)\n", "\n", "# 1. magic to print version\n", "# 2. magic so that the notebook will reload external python modules\n", "%load_ext watermark\n", "%load_ext autoreload \n", "%autoreload 2\n", "\n", "%watermark -a 'Ethen' -d -t -v" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Working with Python Classes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Encapsulation** is seen as the bundling of data with the methods that operate on that data. It is often accomplished by providing two kinds of methods for attributes: The methods for retrieving or accessing the values of attributes are called getter methods. Getter methods do not change the values of attributes, they just return the values. The methods used for changing the values of attributes are called setter methods. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Public, Private, Protected" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are two ways to restrict the access to class attributes:\n", "\n", "1. **protected**. First, we can prefix an attribute name with a leading underscore \"_\". This marks the attribute as protected. It tells users of the class not to use this attribute unless, somebody writes a subclass.\n", "2. **private**. Second, we can prefix an attribute name with two leading underscores \"__\". The attribute is now inaccessible and invisible from outside. It's neither possible to read nor write to those attributes except inside of the class definition itself." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "I am public\n", "{'_A__priv': 'I am private', '_prot': 'I am protected', 'pub': 'I am public'}\n" ] } ], "source": [ "class A:\n", " \n", " def __init__(self):\n", " self.__priv = \"I am private\"\n", " self._prot = \"I am protected\"\n", " self.pub = \"I am public\"\n", "\n", "x = A()\n", "print(x.pub)\n", "\n", "# Whenever we assign or retrieve any object attribute \n", "# Python searches it in the object's __dict__ dictionary\n", "print(x.__dict__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When the Python compiler sees a private attribute, it actually transforms the actual name to `_[Class name]__[private attribute name]`. However, this still does not prevent the end-user from accessing the attribute. Thus in Python land, it is more common to use public and protected attribute, write proper docstrings and assume that everyone is a consenting adult, i.e. won't do anything with the protected method unless they know what they are doing." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Class Decorators" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- `@property` The Pythonic way to introduce attributes is to make them public, and not introduce getters and setters to retrieve or change them.\n", "- `@classmethod` To add additional constructor to the class.\n", "- `@staticmethod` To attach functions to classes so people won't misuse them in wrong places." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### @Property" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's assume one day we decide to make a class that could store the temperature in degree Celsius. The temperature will be a private method, so our end-users won't have direct access to it.\n", "\n", "The class will also implement a method to convert the temperature into degree Fahrenheit. And we also want to implement a value constraint to the temperature, so that it cannot go below -273 degree Celsius. One way of doing this is to define a getter and setter interfaces to manipulate it." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class Celsius:\n", " \n", " def __init__(self, temperature = 0):\n", " self.set_temperature(temperature)\n", "\n", " def to_fahrenheit(self):\n", " return (self.get_temperature() * 1.8) + 32\n", "\n", " def get_temperature(self):\n", " return self._temperature\n", "\n", " def set_temperature(self, value):\n", " if value < -273:\n", " raise ValueError('Temperature below -273 is not possible')\n", " \n", " self._temperature = value" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "37" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# c = Celsius(-277) # this returns an error\n", "c = Celsius(37)\n", "c.get_temperature()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Instead of that, now the **property** way. Where we define the `@property` and the `@[attribute name].setter`." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "class Celsius:\n", " \n", " def __init__(self, temperature = 0):\n", " self._temperature = temperature\n", "\n", " def to_fahrenheit(self):\n", " return (self.temperature * 1.8) + 32\n", " \n", " # have access to the value like it is an attribute instead of a method\n", " @property\n", " def temperature(self):\n", " return self._temperature\n", " \n", " # like accessing the attribute with an extra layer of error checking\n", " @temperature.setter\n", " def temperature(self, value):\n", " if value < -273:\n", " raise ValueError('Temperature below -273 is not possible')\n", " \n", " print('Setting value')\n", " self._temperature = value" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "37\n", "-300\n" ] } ], "source": [ "c = Celsius(37)\n", "\n", "# much easier to access then the getter, setter way\n", "print(c.temperature)\n", "\n", "# note that you can still access the private attribute\n", "# and violate the temperature checking, \n", "# but then it's the users fault not yours\n", "c._temperature = -300\n", "print(c._temperature)\n", "\n", "# accessing the attribute will return the ValueError error\n", "# c.temperature = -300" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### @classmethod and @staticmethod" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`@classmethods` create alternative constructors for the class. An example of this behavior is there are different ways to construct a dictionary." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'raymond': None, 'rachel': None, 'mathew': None}\n" ] } ], "source": [ "print(dict.fromkeys(['raymond', 'rachel', 'mathew']))" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'year': 2012, 'month': 12, 'day': 21}\n", "{'year': 2018, 'month': 8, 'day': 19}\n" ] } ], "source": [ "import time\n", "\n", "class Date:\n", " # Primary constructor\n", " def __init__(self, year, month, day):\n", " self.year = year\n", " self.month = month\n", " self.day = day\n", "\n", " # Alternate constructor\n", " @classmethod\n", " def today(cls):\n", " t = time.localtime()\n", " return cls(t.tm_year, t.tm_mon, t.tm_mday)\n", "\n", "# Primary\n", "a = Date(2012, 12, 21) \n", "print(a.__dict__)\n", "\n", "# Alternate\n", "b = Date.today() \n", "print(b.__dict__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `cls` is critical, as it is an object that holds the class itself. This makes them work with inheritance." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'year': 2018, 'month': 8, 'day': 19}\n", "{'year': 2018, 'month': 8, 'day': 19}\n" ] } ], "source": [ "class NewDate(Date):\n", " pass\n", "\n", "# Creates an instance of Date (cls=Date)\n", "c = Date.today() \n", "print(c.__dict__)\n", "\n", "# Creates an instance of NewDate (cls=NewDate)\n", "d = NewDate.today() \n", "print(d.__dict__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The purpose of **@staticmethod** is to attach functions to classes. We do this to improve the findability of the function and to make sure that people are using the function in the appropriate context." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "class Date:\n", " # Primary constructor\n", " def __init__(self, year, month, day):\n", " self.year = year\n", " self.month = month\n", " self.day = day\n", "\n", " # Alternate constructor\n", " @classmethod\n", " def today(cls):\n", " t = time.localtime()\n", " return cls(t.tm_year, t.tm_mon, t.tm_mday)\n", " \n", " # the logic belongs with the date class\n", " @staticmethod\n", " def show_tomorrow_date():\n", " t = time.localtime()\n", " return t.tm_year, t.tm_mon, t.tm_mday + 1" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(2018, 8, 20)" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Date.show_tomorrow_date()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For those interested, the following link contains a much more in-depth introduction into @classmethod and @staticmethod. [Blog: Python's Instance, Class, and Static Methods Demystified](https://realpython.com/instance-class-and-static-methods-demystified/)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Reference\n", "\n", "- [Python Tutorials: Python @property](http://www.programiz.com/python-programming/property) \n", "- [Onlines Python Course Notes: Properties vs. Getters and Setters](http://www.python-course.eu/python3_properties.php)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.6.4" }, "toc": { "nav_menu": { "height": "169px", "width": "252px" }, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": true, "toc_position": { "height": "calc(100% - 180px)", "left": "10px", "top": "150px", "width": "253px" }, "toc_section_display": "block", "toc_window_display": true }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false } }, "nbformat": 4, "nbformat_minor": 1 }