{ "cells": [ { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Object oriented programming\n", "\n", "## Objectives\n", "* Learn how to use objects\n", "* Learn how to make classes" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Objects are everywhere in Python. And you can make your own!\n", "\n", "Why?\n", "\n", "* You can collect data and actions into one package\n", "* You can change the object behavior to be \"natural\" for your situation\n", "* You can replace parts of functions\n", "* You can template an interface" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Collect data and actions\n", "\n", "```python\n", "my_data_file = MyDataClass(...)\n", "\n", "my_data_file.check_data()\n", "my_data_file.process()\n", "my_data_file.plot()\n", "```\n", "\n", "Data and actions usually are related." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Change behavior of objects\n", "\n", "```python\n", "vector_1 = Vector(...)\n", "vector_2 = Vector(...)\n", "vector_3 = vector_1 + vector_2\n", "print(vector_3)\n", "```\n", "\n", "Most of Python's syntax can be controlled by the classes. Math, printing, most built-in functions, indexing, iteration, `with` statements, you name it. It's easier to go over what you can't modify: `and`, `or`, `not`, `is`, and the basic assignment operator." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Replacing parts of functions (or data):\n", "\n", "```python\n", "def my_long_function():\n", " # Do something 1\n", " ...\n", " # Do something 2\n", " ...\n", " # Do something 3\n", " ...\n", "```\n", "\n", "What happens if you only need to replace the middle part? Objects give you a way to make these parts modular without replacing `my_long_function`" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Template an interface\n", "\n", "```python\n", "\n", "class MySolution(InstructorTemplate):\n", " def solution_1(...\n", "```\n", "\n", "You can implement a framework that requires a user implement a few parts, while you do the rest. Might be handy for problem sets, for example! Given Python's amazing abilities at inspection, you can do practically anything here." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Making your own classes" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "-" } }, "source": [ "Simplest class ever:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class VerySimpleClass:\n", " pass" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "The class block is a lot like a \"bag\" that holds all definitions made in it. There are only two extras: a little bit of information is injected to make the class easier to use (like the name and the location in the file), and you \"inherit\" from `object` - which means you get some predefined methods for free." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "---\n", "\n", "> ## Aside: Python 2:\n", ">\n", "> If you are stuck in Python 2, never leave the inherit part of a class empty - this will leave you with an \"old-style\" class, and it will not work as you expect. Put the Python base class `object` there instead.\n", ">\n", "> ```python\n", "> class VerySimpleClass(object):\n", "> pass\n", "> ```\n", ">\n", "> Python 3 removed old style classes so it is safe to do this again." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Slightly more useful:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class MySimpleClass:\n", " def __init__(self, value):\n", " self.value = value" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "v = MySimpleClass(3)\n", "v.value" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Any functions (methods) in the class are expected to take the class instance as the first argument - always called `self` by convention." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Special methods\n", "\n", "\n", "* `__init__`: Sets up a class when a new one is created - called as part of the \"constructor\"\n", "* `__repr__`: Controls the \"programmer's\" display of a class (such as in interactive prompts) - often looks like the constructor.\n", "* `__str__`: Controls the printed form of a class\n", "* `__add__`: Most math operations are available, like adding (see [Dive into Python 3](http://www.diveintopython3.net/special-method-names.html) and [official docs](https://docs.python.org/3/reference/datamodel.html))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Case study: imaginary numbers\n", "\n", "Let's pretend we don't know imaginary numbers are part of Python, and make our own class (listing 4.1, 4.2, 4.3 in our book):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Complex:\n", " def __init__(self, real, imag):\n", " self.real = real\n", " self.imag = imag\n", "\n", " def __add__(self, other):\n", " return self.__class__(self.real + other.real, self.imag + other.imag)\n", "\n", " def __sub__(self, other):\n", " return self.__class__(self.real - other.real, self.imag - other.imag)\n", "\n", " def __mul__(self, other):\n", " return self.__class__(\n", " self.real * other.real - self.imag * other.imag,\n", " self.real * other.imag + self.imag * other.real,\n", " )\n", "\n", " def __repr__(self):\n", " return f\"{self.real} + {self.imag}j\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "Complex(1, 2)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Complex(1, 0) + Complex(0, 1)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Complex(1, 0) - Complex(0, 1)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "We can compare this with the builtin complex numbers:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "Complex(1, 1) * Complex(1, 1)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "complex(1, 1) * complex(1, 1)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# From the book: Beats" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import math" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "# Using pure python to make a list from 0 to 5 with 500 points\n", "x = [v / 100 for v in range(500)]\n", "\n", "# Now compute the function, again with a list comprehension\n", "y = [math.sin(30 * v) + math.sin(33 * v) for v in x]\n", "\n", "plt.figure(figsize=(15, 3.5))\n", "plt.plot(x, y)\n", "plt.title(\"Beats: f(x)=sin(30*x)+sin(33*x)\")\n", "plt.xlabel(\"x\")\n", "plt.ylabel(\"f(x)\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "* When we go into plotting, we'll see a \"better\" way to plot\n", "* Later we'll use numpy here instead of Python's lists and math library\n", "\n", "---\n", "> Reminder! list comprehensions and looping in Python is **slow**. We will soon see how to do this more cleanly and beautifully in numpy, and it will be faster too." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class Wave:\n", " def __init__(self, x, coef):\n", " self.x = x\n", " self.y = [math.sin(coef * v) for v in x]\n", "\n", " def __add__(self, other):\n", " result = self.__class__(self.x, 0)\n", " result.y = [a + b for a, b in zip(self.y, other.y)]\n", " return result\n", "\n", " def plot(self):\n", " plt.plot(self.x, self.y)\n", " plt.xlabel(\"x\")\n", " plt.ylabel(\"f(x)\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "# Using pure python to make a list from 0 to 5 with 500 points\n", "x = [v / 100 for v in range(500)]\n", "\n", "# Now compute the function\n", "wave = Wave(x, 30) + Wave(x, 33)\n", "\n", "plt.figure()\n", "wave.plot()\n", "plt.title(\"Beats: f(x)=sin(30*x)+sin(33*x)\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Bonus: Using Numpy notation" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "import numpy as np\n", "\n", "\n", "class WaveNP:\n", " def __init__(self, x, coef):\n", " self.x = x\n", " self.y = np.sin(coef * x)\n", "\n", " def __add__(self, other):\n", " result = self.__class__(self.x, 0)\n", " result.y = self.y + other.y\n", " return result\n", "\n", " def plot(self):\n", " plt.plot(self.x, self.y)\n", " plt.xlabel(\"x\")\n", " plt.ylabel(\"f(x)\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "# Using numpy to make an array from 0 to 5 with 500 points\n", "x = np.linspace(0, 5, 500, endpoint=False)\n", "\n", "# Now compute the function\n", "wavenp = WaveNP(x, 30) + WaveNP(x, 33)\n", "\n", "plt.figure()\n", "wavenp.plot()\n", "plt.title(\"Beats: f(x)=sin(30*x)+sin(33*x)\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Inheritance\n", "\n", "We can use inheritance to add or change anything in a class:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Wave2(WaveNP):\n", " def plot(self):\n", " plt.plot(self.x, self.y, \".r\")\n", " plt.xlabel(\"x\")\n", " plt.ylabel(\"f(x)\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "wave2 = Wave2(x, 30) + Wave2(x, 33)\n", "plt.figure()\n", "wave2.plot()\n", "plt.title(\"Beats: f(x)=sin(30*x)+sin(33*x)\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "-" } }, "source": [ "You can even inherit from builtin classes, like `int`!" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "You can also do several other things with classes:\n", "\n", "* Forcing a user to subclass an implement one or more methods to use your class (This would be called an Abstract Base Class, or ABC)\n", "* Multiple inheritance allows you to combine classes. Usually a bad idea.\n", "\n", "We won't cover these things for now." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Path comparisons\n", "\n", "Let's look at a simple but useful example of OO code vs. procedural code (that is, just normal functions).\n", "\n", "Paths are the address to files on your computer. There are three ways to manipulate them:\n", "\n", "* By hand\n", "* Using os.path functions (procedural)\n", "* Using pathlib (OO)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Our goal will be to take this:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "original_path = \"/home/myself/repository/folder\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And get this path from it:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "new_path = \"/home/myself/repository/other/file.txt\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In words: Go up one folder, down into `other` folder, then get `file.txt` in that folder." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### By hand\n", "\n", "Python's string manipulations are very powerful, and could be used here:\n", "\n", "" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "original_path.rsplit(\"/\", 1)[0] + \"/other/file.txt\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "What's wrong with this?" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "\n", "\n", "* Requires '/' be the separator - may break on Windows\n", "* Breaks if you add ending slash to `original_path`\n", "* Is not self documenting - the procedure is in the code, not the *intent*\n", " \n", "" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Using os.path\n", "\n", "This is the \"old\" way of doing it, and is procedural.\n", "\n", "" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "os.path.join(os.path.dirname(original_path), \"other\", \"file.txt\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "\n", "\n", "* Avoid explicit separator\n", "\n", "\n", "\n", "\n", "* Still breaks on ending slash\n", "* Often not ideal on one line\n", "* Does not scale well\n", "* Correct function may be hard to find\n", " \n", "" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Using pathlib\n", "\n", "This is the object oriented way to do it. Was added in Python 3, improved in last several versions. Backported to Python 2 as an external library.\n", "\n", "" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", "\n", "p = Path(original_path)\n", "p.parent / \"other\" / \"file.txt\"" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "\n", "\n", "* Clear intent\n", "* Ignores final slash\n", "* Tab-completion on object\n", "\n", "\n", "\n", "\n", "* Some libraries require that you add `str(p)` to use (or Python \\< 3.6)\n", " \n", "" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Other features:\n", "\n", "* Replace name: `p.with_name(\"name.txt\")`\n", "* With suffix: `p.with_suffix(\".rst\")`\n", "\n", "And, you can also make inquiries about the target file:\n", "\n", "* Make path absolute: `p.absolute()`\n", "* Check for file at that location: `p.exists()`\n", "\n", "And many more!" ] } ], "metadata": { "kernelspec": { "display_name": "compclass", "language": "python", "name": "compclass" }, "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.2" } }, "nbformat": 4, "nbformat_minor": 4 }