{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Week 4:  Scoping in Functions (cont.), Modules (cont.), Object Oriented Programming in Python "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Nested functions \n",
    "As stated earlier, ``def`` is just an executable statement that binds a name to a function object. \n",
    "\n",
    "- It's legal syntax to place a ``def`` anywhere a statement is expected\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def f1():\n",
    "    x = 'hello world'\n",
    "    ## The first nested function within the function f1 \n",
    "    def f2():\n",
    "        ## The second nested function withim f2 \n",
    "        def f3():\n",
    "            print(x)\n",
    "        f3()     \n",
    "    f2()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f1() "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Closures \n",
    "\n",
    "- When a function is nested inside another function, it remembers the enclosing scope\n",
    "\n",
    "- The combination of a function and variables defined in the enclosing scope (non-local variables) is called a closure\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def f1():\n",
    "    X = 88\n",
    "    def f2():\n",
    "        print(X)\n",
    "    return f2\n",
    "action = f1()\n",
    "action()\n",
    "action() "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Useful way to create event handlers \"in response to conditions at runtime.\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Closures allow functions to store state\n",
    "def remember():\n",
    "    history = []\n",
    "    def f(*args):\n",
    "        history.append(args)\n",
    "        return history\n",
    "    return f"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stateful_func = remember()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stateful_func(3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stateful_func(\"MPCS\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stateful_func((3,4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### The nonlocal Statement \n",
    "\n",
    "Similar to ``global``, the ``nonlocal`` statement allows us to change variables defined in an enclosing scope"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def square(x):\n",
    "    return x * x\n",
    "def count_calls(func):\n",
    "    count = 0\n",
    "\n",
    "    def inner(*args, **kwargs):\n",
    "        nonlocal count\n",
    "        count += 1\n",
    "        print(f'Called {count} times')\n",
    "        return func(*args, **kwargs)\n",
    "\n",
    "    return inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "call_func = count_calls(square)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(call_func(2))  # What gets printed? \n",
    "print(call_func(3))  # What gets printed?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Summary of Scope \n",
    "\n",
    "![alt text](./images/scope.png \"Learning Python 2013\") -- <cite>Learning Python 2013</cite>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Modules Revisted \n",
    "\n",
    "Why do we use modules? \n",
    "\n",
    "- *Code reuse*: allow code to be saved permanently as opposed to using the RELP and you can define attributes for use by other modules\n",
    "\n",
    "- *Namespace partitioning*: highest-level program organization unit that provides self-contained namespaces that minimize variable name clashes across your programs\n",
    "       ``my_math_module.pi``  \n",
    "       ``math.pi``   \n",
    "    Hold the same value but are in different namespaces based on the module name. \n",
    " \n",
    "- *Shared services/data*: can implement components that are shared across a system, requiring a single copy\n",
    "\n",
    "#### Structuring a Program \n",
    "\n",
    "Python files can either be:\n",
    "\n",
    "  - *Top-level files*: sometimes called a script file, consists of main\n",
    "    control flow of program, uses zero or more modules that define tools\n",
    "  - *Modules*: define a set of variables, functions, classes, etc.,\n",
    "    that can be used by other programs/modules\n",
    "\n",
    "Terminology:\n",
    "  - *Application*: a top-level file that uses other modules\n",
    "  - *Library*: a collection of modules with no top-level file\n",
    "  \n",
    "![alt text](./images/module.png \"Learning Python 2013\") -- <cite>Learning Python 2013</cite>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Import Statement (Review) \n",
    "\n",
    "The ``import`` statement gives the importing module access to everything defined by top-level code in the imported module: \n",
    " \n",
    "  - When a module is imported, all of the statements in the module execute\n",
    "    until the end of the file is reached\n",
    "  - Any names that were assigned at the top-level (global variables,\n",
    "    functions, classes) are assigned to the module namespace\n",
    "  -  Any print statements or other logic at the top-level will be shown on import"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Importing variations \n",
    "\n",
    "Python provides various ways to import modules and their code: \n",
    "\n",
    "1. ``import module_name``: finds, compiles (to byte code), and runs the module file's code (if it hasn't been loaded yet) as a whole. \n",
    "    -   References to the code in the module (names, functioncs, etc.) must be qualified: \n",
    "            ``module_name.attribute_name`` \n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#  We been using this way to import modules in the class \n",
    "import math \n",
    "print(math.pi) # math must be used to gain access to pi\n",
    "print(math.fabs(-34.34)) # math must be used to gain access to fabs "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2.``from module_name import module_attribute``: fetches specific names defined in the module into another modules scope. \n",
    "    - References to these names require no qualification. \n",
    "    - All attributes now exist in the importing module's namespace."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Will only import the fabs and sqrt functions from the math module without qualification\n",
    "from math import fabs,sqrt\n",
    "\n",
    "# No qualification (i.e. no using \"math\") \n",
    "print(fabs(-32.213))\n",
    "\n",
    "print(sqrt(9))\n",
    "\n",
    "print(pi)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. ``import module_name as module_alias`` or ``from module_name import attribute as attribute_alias``: Assigns a different name to a module's attribute, potentially due to a naming clash. \n",
    "    - Refer to module or module attributes using an alias."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def sqrt(x): return x #bogus sqrt \n",
    "\n",
    "from math import sqrt as m_sqrt\n",
    "\n",
    "print(sqrt(4))\n",
    "print(m_sqrt(4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "4. ``from module_name import *``: Copy all names into this scope. \n",
    "  - Collapses one module’s namespace into another. Don’t do itbecause its considered bad practice."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from math import *\n",
    "\n",
    "print(pi)\n",
    "print(sqrt(9))\n",
    "print(fabs(-4.54))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Module Naming Conventions \n",
    "\n",
    "- Names for modules should be consise and lowercase\n",
    "     - Yes: ``spam.py`` \n",
    "     - No: ``MySpamModule.py`` \n",
    "- Use a leading underscore for modules that are meant to be private/internal\n",
    "      ``_spam.py`` \n",
    "      \n",
    "- Don’t use standard-library module names\n",
    "     - Yes: ``my_math.py``\n",
    "     - No: ``math.py``"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Object-Oriented Programming in Python \n",
    "\n",
    "Object-Oriented Programming (OOP) is a paradigm that provides another way of organizing your program logic. OOP combines data and functionality into a single object. \n",
    "\n",
    "Now we can create own types of objects by defining **classes**. For example, let's create a new type for representing Vehicles. \n",
    "\n",
    "A class is defined using the ``class`` statement and has the general form:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Car:\n",
    "     # Definition of Car type goes here\n",
    "     pass # Use pass to indicate we have not implemented the class yet"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We already seen aspects of OOP from creating objects from built-in types (``dict``, ``list``, etc.) and types we imported from other modules: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "name_ages_1 = dict() # Create Empty Dictionary "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "name_ages_1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "names_ages_2 = dict([(\"Bob\",34), (\"Tom\", 54), (\"Joan\", 45)]) #Create dictinary with initial values "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "names_ages_2 "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "names_ages_3 = dict(names_ages_2) # Make a copy of the previous dictionary"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "names_ages_3"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "``dict`` is a class, and ``names_ages_1``, ``names_ages_2``, and ``names_ages_3`` are variables that refer to  *objects* of ``dict``. \n",
    "\n",
    "\n",
    "One of way to think about this distinction is that a class is like the blueprint for creating specific realizations of some entity. \n",
    "\n",
    "\n",
    "![alt text](./images/class.png \"Class vs Instance\") \n",
    "\n",
    "\n",
    "Referring back to our ``Car`` class, where it provides the blueprint and the objects are the actual individual cars. \n",
    "\n",
    "The blueprint specifies certain features of the var can vary from car to car, like its color or transmission type. We can create multiple cars with different values for any given *attribute* (e.g.,  one car could be red and another blue). \n",
    "\n",
    "\n",
    "The objects we create from classes are known as **instances**. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Creating Objects of a Class \n",
    " \n",
    "We can instantiate (i.e., create an object/instance) of a class using the following code:  \n",
    "\n",
    "    ``object = class_name(...)``\n",
    " \n",
    "Although object creation has similiar syntax to a function, we are not calling by instead creating a new instance of ``class_name``, and providing the necessary values to initialze the instance. \n",
    "\n",
    "In our names and ages dictionaries code above, we created three different way to create a dictionary: \n",
    "\n",
    "- No arguments: creating an empty dictionary \n",
    "- List of 2-tuples: creates a dictionary with the key-value pairs in the list \n",
    "- Dictionary: creates a copy of the argument dictionary\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Constructors and Attributes \n",
    "\n",
    "Referring back to our ``Car`` class, we typically should know about the ``make``, ``model`` and ``year`` for a car. We consider the data components that make up a class as *attributes*. In this case, ``make``, ``model`` and ``year`` are attributes of the class.  \n",
    "We could create ``Car`` objects like the following: \n",
    "\n",
    "`car1 = Car(\"Honda\",\"Fit\", 2017) # Create a Car with make=\"Honda\", model=\"Fit\", Year=2020``\n",
    "\n",
    "When creating an class, we can define a special method known as ``__init__`` to initialize an object:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Car: \n",
    "    def __init__(self, make, model, year):\n",
    "        self.make = make \n",
    "        self.model = model \n",
    "        self.year = year "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Before explaining whats happening in ``__init__``, lets create few car objects and access their attributes. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_1 = Car(\"Honda\", \"Fit\", 2017)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_2 = Car(\"Tesla\", \"Model 3\", 2020)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_1.make"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_1.model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_1.year"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_2.make"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_2.model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_2.year"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We created two instances of the ``Car`` class adn assigned them to the varibales ``car_1`` and ``car_2``. Each instace have their own (independent) values for the ``Car``  attributes: ``make``, ``model`` and ``year``. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, what's happening in the ``__init__`` method? Again, the ``__init__`` method is a special method that handles what should happen when we create an object: \n",
    "\n",
    "``car_1 = Car(\"Honda\", \"Fit\", 2017)`` \n",
    "\n",
    "Notice, the first argument is not ``make`` as you may think but instead there's this ``self`` argument. This argument contains the object we are creating. Notice how we don't pass this argument when constructing a ``Car``. Behind the scenes, Python is translating: \n",
    "\n",
    "``car_1 = Car(\"Honda\", \"Fit\", 2017)`` \n",
    "\n",
    "into \n",
    "\n",
    "``Car.__init__(new_object, \"Honda\", \"Fit\", 2017) #new_object is empty initially``\n",
    "\n",
    "after creating and initialzing the object then python does the following: \n",
    "\n",
    "``car_1 = new_object`` \n",
    "\n",
    "**Note**: The ``__init__`` method is also known as a constructor. \n",
    "\n",
    "Inside the ``__init__`` method we initialize the attributes for the object: \n",
    "\n",
    "        ``self.make = make`` \n",
    "\n",
    "We're saying \"set the value of attribute ``make`` of the object to be equal to argument ``make``\".\n",
    "\n",
    "Notice how __init__ doesn't return anything (although you can think of it as implicitly returning ``self``).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Attributes (cont.)\n",
    "\n",
    "- Are immediately created on the first assignment to them. \n",
    "-  Acts as the object state. \n",
    "- Creation syntax: ``object.variable_name = expression``, where typically ``object`` is ``self``\n",
    "- All attributes are cccessible from inside the class and outside. \n",
    "\n",
    "  - Synax: ``self.attribute_name`` (Inside) or \n",
    "           ``object_var_name.attribute_name`` (Outside) \n",
    "\n",
    "#### Dynamic Attributes \n",
    "As with function attributes, Python allows you to dynamically create attributes and modify them at run-time\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_t = Car(\"Toyota\",\"RAV4\", 2018) # Create a Car"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_f = Car(\"Ford\",\"Escape\", 2018) # Create a Car\n",
    "\n",
    "# Creates a dynamica attribute that only the \n",
    "# car_f instance has. \n",
    "car_f.hybrid = True \n",
    "\n",
    "car_f.hybrid"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Running the following code will case an error \n",
    "# car_t has not attribute hybrid \n",
    "car_t.hybrid"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Although this feature is allowed, by convention its better to define attributes at initialization time. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Instance Methods \n",
    "\n",
    "*Instance methods* allow us to define new behaviors/actions for an instance of a class: \n",
    " -  Think of an instance method as \"A function that 'belongs to' an object.\n",
    " \n",
    "For example, we have already seen this with the ``list`` class, where can call its ``append`` method to add an object to the end of the list. \n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lst1 = [1,2,3]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lst2 = ['a','b','c']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lst1.append(True)\n",
    "lst2.append(True)\n",
    "print(lst1)\n",
    "print(lst2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A method always operates on the instance it is called on. So, notice how we call ``append`` with the same parameters on both lists:\n",
    "\n",
    "    .append(True)\n",
    "    .append(True)\n",
    "    \n",
    "But they produce different results based on what object the method is called on.\n",
    "\n",
    "Lets add ``compute_age`` to determine how years old the car is from todays date. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from datetime import date\n",
    "class Car: \n",
    "    def __init__(self, make, model, year):\n",
    "        self.make = make \n",
    "        self.model = model \n",
    "        self.year = year\n",
    "    def compute_age(self):\n",
    "        current_year = int(date.today().year)\n",
    "        return current_year - self.year "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_t = Car(\"Toyota\",\"RAV4\", 2018)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_t.compute_age()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice, how ``compute_age()`` also has a ``self`` argument as its first argument, similiar to ``__init__``. In Python, all instance methods implicitly take the method’s instance object as the first argument. \n",
    "\n",
    "Again, Pything is taking the following line: \n",
    "\n",
    "``car_t.compute_age()`` and is \n",
    "\n",
    "translating it into \n",
    "\n",
    "``Car.compute_age(car_t)`` \n",
    "\n",
    "**Note**: Passing in the object (i.e., ``self``) is ALWAYS the first argument. However, it's not required to be called ``self`` but by convention its always named ``self``. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Restricting Access to Attributes (Information Hiding) \n",
    "\n",
    "Allowing a user of a class to change the values assigned to the attributes, For example, "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create a Car object \n",
    "car_1 = Car(\"Honda\",\"Accord\", 2019)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Modify the year attribute outside of the class \n",
    "car_1.year = 2017 "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Why is accessing the attribute not always a good idea?\n",
    "\n",
    "A few months later, we want to change the ``year`` attribute of the ``Car`` class to ``str`` (e.g., ``\"2019\"``) instead of ``int`` to potentially allow the user to specify variations of the  year. \n",
    "\n",
    "Well it's too late now because we allowed the ``year`` attribute to be public and the developers may be using it in conjunction with set operators throughout their code. Making the modification will break their code. \n",
    "\n",
    "Is there a way to allow this change to happen without affecting our users? Yes, **encapsulation** \n"
   ]
  },
  {
   "cell_type": "markdown",
   "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 (Today) \n",
    "- Getter/Setters (Next Week) \n",
    "- Properties (Next Week)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Private Attributes \n",
    "\n",
    "Often, Python programmers give attributes names starting with two underscore to signal to programmers that these attributes are \"private\" and shouldn't be changed.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Car: \n",
    "    def __init__(self, make, model, year):\n",
    "        self.__make = make \n",
    "        self.__model = model \n",
    "        self.__year = year"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_1 = Car(\"Honda\",\"Accord\", 2019)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Now, we cannot access the attributes directly\n",
    "# will cause an error. It does not cause this error in the Jupyter-notebook. \n",
    "# Try running this in ipython and you'll see the following: \n",
    "# ---------------------------------------------------------------------------\n",
    "#AttributeError                            Traceback (most recent call last)\n",
    "#<ipython-input-3-e0dcc6670c8c> in <module>\n",
    "#----> 1 car.__make\n",
    "#AttributeError: 'Car' object has no attribute '__make'\n",
    "car_1.__make = \"Toyota\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Dunder Methods\n",
    "\n",
    "Dunder methods (also known as special methods) allows us to define methods that make our user-defined types behave like the built-in types. For example, printing out the contents within an object is easily done with ``str``, ``list``, ``dict`` and many other built-in types. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print([1,2,3,5])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"This is a string\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "However, with our current implementation of the ``Car`` method, we do not see our atttributes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car = Car(\"Honda\",\"Accord\", 2019)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(car)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is the default *string representation* of the object.  ``print(car)``, for some objects, prints out the address of where the object lives within memory and not the its actual contents. \n",
    "\n",
    "\n",
    "We can override this behaviour by adding a ``__repr__`` method. This method returns a string that will be used anytime we need to print our new data types internal contents. \n",
    "\n",
    "**Note**: there is also another string representation method called ``__str__``. The differences between the two are subtle; however, we will go over them when we go over dunder methods in more detail next week. \n",
    "\n",
    "Now, lets add the ``__repr__`` method to our ``Car`` class. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Car: \n",
    "    def __init__(self, make, model, year):\n",
    "        self.__make = make \n",
    "        self.__model = model \n",
    "        self.__year = year\n",
    "        \n",
    "    def __repr__(self): \n",
    "        return f'Car(make={self.__make}, model={self.__model}, year={self.__year})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Now we are able to see a better string representation for Car objects \n",
    "car = Car(\"Honda\",\"Accord\", 2019)\n",
    "print(car)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There is also a dunder that allows us to check equality between two objects (i.e., using the ``==`` operator). By default, Python checks whether objects refer to the same memory locations in memory but not if there internal data is the same."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "car_1 = Car(\"Honda\",\"Accord\", 2019)\n",
    "car_2 = Car(\"Honda\",\"Accord\", 2019)\n",
    "\n",
    "#False, because although their internal data is the same they are two completely different \n",
    "# objects in memory. \n",
    "print(car_1 == car_2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "However, we can implement an ``__eq__`` method to define how Python should interpret the ``==`` operator."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Car: \n",
    "    def __init__(self, make, model, year):\n",
    "        self.__make = make \n",
    "        self.__model = model \n",
    "        self.__year = year\n",
    "    \n",
    "    def __eq__(self, other): \n",
    "        return self.__make == other.__make and self.__model == other.__model and self.__year == other.__year\n",
    "    def __repr__(self): \n",
    "        return f'Car(make={self.__make}, model={self.__model}, year={self.__year})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Before defining equal \n",
    "car_1 = Car(\"Honda\",\"Accord\", 2019)\n",
    "car_2 = Car(\"Honda\",\"Accord\", 2019)\n",
    "\n",
    "#True, because we have implement the \"__eq__\" method which overrides the default implementation \n",
    "# and now we are checking the internal data, which is the same. \n",
    "print(car_1 == car_2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Code Demo: Stack Class \n",
    "\n",
    "- See full implementation in week4/stack/stack.py  in the upstream repository. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Aside: Function Attributes \n",
    "Similiar to attributes in classes, we can also assoicate attributes with functions. \n",
    " \n",
    "- Function attributes are another way to maintain state. The syntax is similiar to defining attributes within classes; however, we use the function name instead: \n",
    "\n",
    "    ``function_name.attribute_name = expression`` \n",
    "   \n",
    "Let's look at the following example"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def counter(func):                            # Pass a callable\n",
    "    def inner(*args, **kwargs):\n",
    "        inner.call_count += 1\n",
    "        print(\"call count: {}\".format(inner.call_count))\n",
    "        return func(*args, **kwargs)          # Returns result\n",
    "    # Function attribute definition \n",
    "    # attribute = call_count \n",
    "    # assoicated function = inner \n",
    "    inner.call_count = 0\n",
    "    return inner"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*Notes*:\n",
    "- The attribute is nonlocal in the scope of ``inner``.\n",
    "- The attribute is accessible to the caller as an instance attribute.  \n",
    "  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pow_count = counter(pow)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x = pow_count(5, 2)         # Returns 25,  prints \"call count: 1\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = pow_count(5, 3)         # Returns 125, prints \"call count: 2\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "c = pow_count.call_count    # Accessible to caller's scope\n",
    "print(c)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Code Demo: OO Modeling (Live Discussion Session)\n",
    "    \n",
    "- During our Discussion Session this week, I will go over the importance of defining your own types and using classes and the OOP paradigm. "
   ]
  }
 ],
 "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.7.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
