{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Classes, Objects, Attributes, Methods: Very Basic Object-Oriented Programming in Python"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This work is licensed under [Creative Commons Attribution-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-sa/4.0/)\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is a very basic introduction to object-oriented prgramming in Python: defining *classes* and using them to create *objects* with *methods* (a cousin of functions) that act on those objects.\n",
    "\n",
    "These are illustrated with objects that are vectors, and in particular 3-component vectors that have a cross product."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The first class will be for vectors wih three components, labeld 'x', 'y' and 'z'.\n",
    "\n",
    "On its own, this would be rather redundant, since numpy arrays could be used, but it serves to introduce some basic ideas, and then prepare for the real goal: 3-vectors with cross product."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example A: Class `VeryBasic3Vector`\n",
    "\n",
    "This first class `VeryBasic3Vector` illustrates some basic features of creating and using classes;\n",
    "however, it will be superceded soon!\n",
    "\n",
    "Almost every class has a method with special name `__init__`, which is use to create objects of this class.\n",
    "In this case, `__init__` sets the three *attributes* on a `VeryBasic3Vector` — its x, y, and z components.\n",
    "\n",
    "This class has just one other method, for the scalar (\"dot\") product of two such vectors."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class VeryBasic3Vector():\n",
    "    \"\"\"A first, minimal class for 3-component vectors, offering just creation and the scalar (\"dot\") product.\"\"\"\n",
    "    def __init__(self, values):\n",
    "        self.x = values[0]\n",
    "        self.y = values[1]\n",
    "        self.z = values[2]\n",
    "    def dot(self, other):\n",
    "        '''\"a.dot(b)\" gives the scalar (\"dot\") product of a with b'''\n",
    "        return self.x * other.x + self.y * other.y + self.z * other.z"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Create a couple of `VeryBasic3Vector` objects:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a = VeryBasic3Vector([1, 2, 3])\n",
    "print(f\"The x, y, and z attributes of object a are {a.x}, {a.y} and {a.z}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "b = VeryBasic3Vector([4, 5, 2])\n",
    "print(f\"Object b contains the vector <{b.x}, {b.y}, {b.z}>\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This way of printing values one attribute at a time gets tiresome, so soon, an alternative will be introduced, in improved class `BasicNVector`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The attributes of an object can also be set directly:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a.y = 5\n",
    "print(f\"a is now <{a.x}, {a.y}, {a.z}>\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Methods are used as follows, with the variable before the \".\" part being `self`\n",
    "and any parenthesized variables being the arguments to the method definition."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f'The scalar (\"dot\") product of a and b is {a.dot(b)}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example B: Class `BasicNVector`\n",
    "\n",
    "A second class `BasicNVector` with some improvements over the above class `VeryBasic3Vector`:\n",
    "- Allowinv vecors of any length, \"N\"\n",
    "- Methods for addition, subtraction and vector-by-scalar multiplication.\n",
    "- The special method `__str__` (using this name is mandatory) to output an object's values as a string — for using in `print`, for example.\n",
    "\n",
    "Aside: here and below, the names of these special methods like `__str__` start and end with a *pair* of underscores, \"__\"."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class BasicNVector():\n",
    "    \"\"\"This improves on class VeryBasic3Vector by:\n",
    "    - allowing any number of components,\n",
    "    - adding several methods for vector arithmetic, and\n",
    "    - defining the special method  __str__() to help display the vector's value.\"\"\"\n",
    "\n",
    "    # First mimic the definitions in class VeryBasic3Vector:\n",
    "    \n",
    "    def __init__(self, list_of_components):\n",
    "        self.list = list_of_components.copy()\n",
    "\n",
    "    def dot(self, other):\n",
    "        '''\"a.dot(b)\" gives the scalar (\"dot\") product of a with b'''\n",
    "        dot_product = 0\n",
    "        for i in len(self):\n",
    "            dot_product += self.list[i] * other.list[i]\n",
    "        return dot_product\n",
    "\n",
    "    # Next, some new stuff not in class VeryBasic3Vector\n",
    "\n",
    "    def times(self, scale):\n",
    "        '''\"self.times(scale)\"\" gives the product of vector \"self\" by scalar \"scale\"'''\n",
    "        return BasicNVector([scale * component for component in self.list])  # This uses a list comprehension; noteson comprehensions are coming soon!\n",
    "\n",
    "    # The special method names wrapped in double underscores, __add__, __sub__ and __str__,\n",
    "    # have special meanings, as will be revealed below.\n",
    "\n",
    "    def __add__(self, other):  # Vector addition — with definition of the \"+\" operation for pairs of BasicNVector objects\n",
    "        return BasicNVector([ self.list[i] + other.list[i] for i in range(len(self.list)) ])\n",
    "\n",
    "    def __sub__(self, other):  # Vector subtraction — with definition of the \"-\" operation for pairs of BasicNVector objects\n",
    "        return BasicNVector([ self.list[i] - other.list[i] for i in range(len(self.list)) ])\n",
    "\n",
    "    def __str__(self):\n",
    "        \"\"\"How to convert the value to a text string.\n",
    "        As above, this uses angle brackets <...> for vectors, to distinguish from lists [...] and tuples (...)\"\"\"\n",
    "        string = '<'\n",
    "        for component in self.list[:-1]:\n",
    "            string += f'{component}, '\n",
    "        string += f\"{self.list[-1]}>\"\n",
    "        return string"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We need to create new objects of class BasicNVector to use these new methods:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "c = BasicNVector([1, 2, 3, 4])\n",
    "d = BasicNVector([4, 5, 2, 3])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The new method \"`__str__`\" makes it easer to display the value of a BasicNVector, by just using `print`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"c = \", c)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And now we can try the other new methods:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "c_times_3 = c.times(3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<1, 2, 3, 4> times 3 is <3, 6, 9, 12>\n"
     ]
    }
   ],
   "source": [
    "print(f\"{c} times 3 is {c_times_3}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here is the usual way of using the mysteriously-names method \"`__add__`\" ..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<1, 2, 3, 4> + <4, 5, 2, 3> = <5, 7, 5, 7>\n"
     ]
    }
   ],
   "source": [
    "e = c.__add__(d)\n",
    "print(f\"{c} + {d} = {e}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "... but that special name also means that it also specifies how the operation \"+\" works on a pair of BasicNVector objects:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<1, 2, 3, 4> + <4, 5, 2, 3> = <5, 7, 5, 7>\n"
     ]
    }
   ],
   "source": [
    "f = c + d\n",
    "print(f'{c} + {d} = {f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Likewise for subtraction with `__sub__`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<1, 2, 3, 4> - <4, 5, 2, 3> = <-3, -3, 1, 1>\n"
     ]
    }
   ],
   "source": [
    "print(f'{c} - {d} = {c - d}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Inheritence: new classes that build on old ones\n",
    "\n",
    "A new class can be defined by refining an existing one, by adding methods and such, to avoid defining everything from scratch.\n",
    "The basic syntax for creating class `ChildClass` based on an existing *parent class* named `ParentClass` is\n",
    "\n",
    "    class ChildClass(ParentClass):\n",
    "\n",
    "Here we define class `Vector3`, which is restricted to vectors with 3 components, and uses that restriction to allow defining the vector cross product.\n",
    "\n",
    "In addition, it makes the operator \"\\*\" do cross multiplication on such objects, by also defining the special method `__mul__`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Vector3(BasicNVector):\n",
    "    \"\"\"Restrict to BasicNVector objects of length 3, and then add the vector cross product\"\"\"\n",
    "    def __init__(self, list_of_components):\n",
    "        if len(list_of_components) == 3:\n",
    "            super().__init__(list_of_components)\n",
    "            # Aside: function \"super()\" gives the parent class, so the above is equivalent to\n",
    "            #BasicNVector.__init__(self, list_of_components)\n",
    "        else:  # Complain!\n",
    "            raise ValueError('The length of a Vector3 object must be 3.')\n",
    "\n",
    "    def cross_product(self, other):  # the vector cross product\n",
    "        (x1, y1, z1) = self.list\n",
    "        (x2, y2, z2) = other.list\n",
    "        return Vector3([ y1*z2 - y2*z1, z1*x2 - z2*x1, x1*y2 - x2*y1] )\n",
    "\n",
    "    # Add a synonym: now it redefines the multiplication operator \"*\"\n",
    "    # I could have just used the name `__mul__` intead of `cross_product` above;\n",
    "    # I did it this was to also allow the more descriptive name `cross_product`, and to illustrate the use of synonyms for functions.\n",
    "    __mul__ = cross_product  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Again, we need some `Vector3` objects; the above `BasicNVector` objects do not know about the cross product.\n",
    "\n",
    "But note that the previously-defined methods for class `BasicNVector` also work for `Vector3` objects, so for example we can still print with the help of method `__str__` from there."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The vector cross product of <1, 2, 3> with <4, 5, 10> is <5, 2, -3>\n",
      "The vector cross product of <4, 5, 10> with <1, 2, 3> is <-5, -2, 3>\n"
     ]
    }
   ],
   "source": [
    "u = Vector3([1, 2, 3])\n",
    "v = Vector3([4, 5, 10])\n",
    "print(f'The vector cross product of {u} with {v} is {u.cross_product(v)}')\n",
    "print(f'The vector cross product of {v} with {u} is {v*u}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is what happens with inappropriate input, thanks to that `raise` command:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "ename": "ValueError",
     "evalue": "The length of a Vector3 object must be 3.",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mValueError\u001b[0m                                Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-16-4dc0f24dc07a>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mw\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mVector3\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m3\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m4\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[0;32m<ipython-input-14-a85930062fea>\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, list_of_components)\u001b[0m\n\u001b[1;32m      7\u001b[0m             \u001b[0;31m#BasicNVector.__init__(self, list_of_components)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      8\u001b[0m         \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m  \u001b[0;31m# Complain!\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 9\u001b[0;31m             \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'The length of a Vector3 object must be 3.'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     10\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     11\u001b[0m     \u001b[0;32mdef\u001b[0m \u001b[0mcross_product\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mother\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m  \u001b[0;31m# the vector cross product\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mValueError\u001b[0m: The length of a Vector3 object must be 3."
     ]
    }
   ],
   "source": [
    "w = Vector3([1, 2, 3, 4])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Aside:** That's ugly: as seen in the notes on [Exceptions and Exception Handling](exception-handling), a more careful usage would be:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Well, at least we tried, but: 'The length of a Vector3 object must be 3.'\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    w = Vector3([1, 2, 3, 4])\n",
    "except Exception as what_just_happened:\n",
    "    print(f\"Well, at least we tried, but: '{what_just_happened.args[0]}'\")"
   ]
  }
 ],
 "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.8.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
