{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Classes\n", "\n", "In programming, classes, or more general Object Oriented Programing, OOP, is a fundamental paradigm (next to others, mostly functional programming). It is quite powerful when it comes to encapsulation of the logic and states of objects. Some languages, for example Java, are heavily based on it. Python is a multi-paradigm language, meaning there are classes, functions etc.\n", "\n", "However, classes play in Python an extremely central role - although it is not needed to explicitely know about it - since actually every object in Python \"comes from a class\". But we're going ahead of things.\n", "\n", "Let's start with an example problem" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's imagine the following: We want to describe particles and want to do some calculations with them and their velocity, such as calculating their absolute speed.\n", "(we focus on just one paricle here, sure we could use lists but this means to keep track of which entry is which etc.)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# particle 'pi1'\n", "pi1_vx = -10\n", "pi1_vy = 20\n", "pi1_vz = 30\n", "\n", "def calc_velocity_simple(vx, vy, vz):\n", " return np.sqrt(vx ** 2 + vy ** 2 + vz ** 2)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "calc_velocity_simple(pi1_vx, pi1_vy, pi1_vz)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Alright, but clearly cumbersom and not scalable at all to many particles. Better if we could stick it together into one container. Let's use a dict!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pi1 = {'vx': 10,\n", " 'vy': 20,\n", " 'vz': 30}\n", "\n", "\n", "def calc_velocity(particle):\n", " \"\"\"Calculate the absolute velocity of a 'particle dict'.\"\"\"\n", " velocity_squared = particle['vx'] ** 2 + particle['vy'] ** 2 + particle['vz'] ** 2\n", " return np.sqrt(velocity_squared)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "calc_velocity(particle=pi1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Encapsulation\n", "\n", "That looks better! But now, `calc_velocity` critically depends on the structure of `pi1` if we e.g. want to create new particles. How can we \"communicate\" that well? (sure, docstrings, but is there a more \"formal way\"?)\n", "\n", "Furthermore: `calc_velocity` somehow \"belongs\" to pi1, we want to calculate the absolute velocity of it. We always use `calc_velocity` together with a particle dict." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# trying to attach the function to the \"particle\"\n", "pi1 = {'vx': 10,\n", " 'vy': 20,\n", " 'vz': 30,\n", " 'calc_velocity': calc_velocity}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pi1['calc_velocity'](pi1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Blueprint for the object\n", "\n", "This was cumbersome, but better, we get there! For the communication of the exact layout, what we want is a \"template\"/blueprint to know how our dict should always look like. So that if we want to create a new particle, we have to make sure to specify vx, vy and vz in the dict (so that it is valid). And then to also add the `calc_velocity` function." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def make_particle(vx, vy, vz):\n", " return {'vx': vx,\n", " 'vy': vy,\n", " 'vz': vz,\n", " 'calc_velocity': calc_velocity}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "e1 = make_particle(20, 30, 20)\n", "e1['calc_velocity'](e1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The next step will maybe look a bit (as it seems now) overcomplicated, but it will improve the understanding later on. Let's split the above even more (just one last time) into a\n", "\n", "- constructor: this \"makes\" the empty particle with the fields and the methods.\n", "- initializer: adds attributes to the instance, initializes it.\n", "\n", "This is a fine difference and the former is in Python usually not needed (as it is implemented and invoked automatically), so in general, the word constructor may also be used for the initializer in Python. The latter serves mostly the same purpose as a constructor would in other languages." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def make_particle():\n", " return {'calc_velocity': calc_velocity}\n", "\n", "def initialize_particle(particle, vx, vy, vz):\n", " particle['vx'] = vx\n", " particle['vy'] = vy\n", " particle['vz'] = vz\n", " return particle\n", "\n", "particle1 = initialize_particle(make_particle(), vx=20, vy=30, vz=20)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# \"magic line\"\n", "particle1['calc_velocity'](particle1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The call to calculate the mass is still not perfect: We want something that\n", "- \"feeds itself to the function called\".\n", "- is created through a function (\"constructor\")\n", "- has attributes (better then this ['...'] accesing would be with the dot)\n", "\n", "...more like this:\n", "`particle1.calc_velocity()`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Welcome to classes\n", "\n", "**A class is a blueprint of an object**\n", "\n", "The following code does basically what we did above but solves problems from above. It is a simple implementation of our first class called `SimpleParticle`.\n", "\n", "Note that the methods, that's the name of a function \"of a class\", are intended into the class body." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class SimpleParticle:\n", " # what we don't see: before the __init__, there is a (automatic) make_particle (advanced concept) that we\n", " # basically never need to care about.\n", "\n", " # the initialiser, basically initialize_particle\n", " def __init__(self, vx, vy, vz): # self is the instance, the future object.\n", " self.vx = vx\n", " self.vy = vy\n", " self.vz = vz\n", "\n", " def calc_velocity(self):\n", " # we can just reuse the function from above\n", " return calc_velocity_simple(vx=self.vx, vy=self.vy, vz=self.vz)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's use it!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# where is __init__ called? (magic method again)\n", "# answer: when calling the class\n", "particle1 = SimpleParticle(20, 30, vz=40) # NOT equivalent to Particle.__init__(), because\n", " # it calls a constructor before (make_particle)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### self: referencing itself\n", "\n", "An object knows about itself by a simple trick. Before we needed to give the particle explicitly to the method in the dict as the first argument. Here we expect the same (we simply called the argument `self` instead of `particle`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "particle1.calc_velocity() # where did self go?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In a class, `self` is given _automatically_ as the first argument! Hereby, we solved our problem from above." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Furthermore, we can now access attributes instead of using the item access operator `[...]`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "particle1.vz" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That's what we want!\n", "\n", "And we can create more particles with different vx and velocities." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "particle2 = SimpleParticle(35, -30, 40)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Exercise: override the `__add__` method to make two particle addable. Name it `Particle`\n", "Hint: you need to make a new class called Particle" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Particle:\n", " # what we don't see: before the __init__, there is an (automatic) make_particle. Normally we don't need it.\n", " # This is the initialiser, basically initialize_particle\n", " def __init__(self, vx, vy, vz): # self is the instance, the future object.\n", " self.vx = vx\n", " self.vy = vy\n", " self.vz = vz\n", "\n", " def calc_velocity(self):\n", " return calc_velocity_simple(vx=self.vx, vy=self.vy, vz=self.vz)\n", "\n", " def __add__(self, other):\n", " new_vx = self.vx + other.vx\n", " new_vy = self.vy + other.vy\n", " new_vz = self.vz + other.vz\n", " return Particle(new_vx, new_vy, new_vz)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "particle1 = Particle(10, 20, 30)\n", "particle2 = Particle(50, 10, 20)\n", "\n", "# test it here\n", "new_particle = particle1 + particle2" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Inheritance: a glance" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Instead of completely rewriting `Particle`, we can also inherit the class from it. This means we use the parent class as the \"starting point\" and add/replace certain fields." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class VerboseParticle(Particle): # This is inheritance\n", "\n", " def describe_velocity(self):\n", " return f\"vx: {self.vx}, vy: {self.vy}, vz {self.vz}\"\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# test it here again\n", "particle1 = VerboseParticle(10, 12, -10)\n", "particle2 = VerboseParticle(20, 10, 10)\n", "new_particle = particle1 + particle2" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "type(new_particle)" ] }, { "cell_type": "markdown", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "We have one problem now: the particle is again a `Particle`, not a `VerboseParticle`. This is because we \"hardcoded\"\n", "the name into the `__add__` method.\n", "\n", "### How to fix\n", "Let's first step back. We have seen quite a few things in this lecture. This was an introduction into classes in a\n", "minimal time. Classes are a powerful yet non-trivial concept that require to know a lot more than the simple behavior\n", "that we just looked at. There are many concepts - interfaces, multiple inheritance and MRO, inheritance vs composition,\n", "private vs public, getter and setter, stateful/stateless, classmethods and staticmethods, ... - that we just did not\n", "cover here, as it takes a full fledged course on OOP to master these things.\n", "\n", "The problem above should make one thing clear: it is a powerful, yet difficult tool to use and without the proper\n", "knowledge, things can go wrong in completely unexpected corners; that's why good software practices are not just\n", "a nice-to-have but a mandatory asset to guarantee the best-possible (most bugfree) codebase.\n", "\n", "How to actually fix it: instead of `Particle`, we can use the class dynamically itself.\n", "`type` comes in handy: this may has been encountered as a tool to return the type of an object. But this type\n", "_is exactly the class we need!_.\n", "\n", "(Sidenote: be aware of `isinstance` vs `type`, use the former if not explicitly type has to be used.)\n", "\n", "So we can replace the call in `__add__` as follows. Insead of\n", "```return Particle(new_vx, new_vy, new_vz, new_E)```\n", "we have\n", "```return type(self)(new_vx, new_vy, new_vz, new_E)```\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Overriding a method\n", "\n", "If we add a method in the subclass that has the same name as the parent, we will simply shadow the latter and our method will be called (as it is the last in the inheritance tree).\n", "\n", "In order to still use the super class method -- crucially important if we want to implement another `__init__` as the parent also wants to be initialized -- `super()` can be used, which refers to the superclass.\n", "\n", "For example, if we want to shift our mass by a constant up, we can do it by first calling the super method that calculates the real mass. Then we add something to it and return it. Note that it is not necessary at all to call super if a method gets overriden -- it's just like any other method -- but it is ofter useful. And it the case of the `__init__` it should alway be done as shown below." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class BetterParticle(Particle):\n", " def __init__(self, vx, vy, vz, superpower=42):\n", " super().__init__(vx, vy, vz)\n", " self.superpower = superpower\n", "\n", " def calc_velocity(self):\n", " unshifted_velocity = super().calc_velocity()\n", " return unshifted_velocity + self.superpower\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "particle_superpower = BetterParticle(10, 12, 14)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "particle_superpower.calc_velocity()" ] } ], "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.10" } }, "nbformat": 4, "nbformat_minor": 2 }