{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Reminder\n",
    "\n",
    "Do you need to register for \"Studienleistung\" in HisInOne?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Speedrun: command line treasure hunt (schnitzeljagd)\n",
    "\n",
    "We will do a quick speedrun of the schnitzeljagd exercise starting with `ssh your-user-name@python-course.strawlab.org`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Classes\n",
    "\n",
    "Here is an example of a simple class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyClass:\n",
    "    def my_method(self):\n",
    "        print(\"This was printed from a method inside the class.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We'll discuss the \"self\" parameter below. First, let's create an instance of this class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x = MyClass()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.my_method()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why classes?\n",
    "\n",
    "The great benefit of classes is that they keep data and functions organized togther. The functions within a class are called \"methods\" and always take the first parameter, \"self\".\n",
    "\n",
    "With small pieces of code, this is not so important, but as the size of software grows larger, this is handy for keeping things organized. Most Python libraries make extensive use of classes.\n",
    "\n",
    "A class is like a template and multiple instances of this template can be created.\n",
    "\n",
    "## Creating an instance of a class\n",
    "\n",
    "Class definitions can have a special method caled `__init__()`. This initialization method, also called constructor, is called when an instance of a class is created. It is used to store variables and perform other setup."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyClass:\n",
    "    def __init__(self):\n",
    "        self.my_variable = \"foo\"\n",
    "    def my_method(self):\n",
    "        print(\"My variable is {}.\".format(self.my_variable))\n",
    "    def set_var(self,new_value):\n",
    "        self.my_variable = new_value\n",
    "        \n",
    "x = MyClass()\n",
    "x.my_method()\n",
    "\n",
    "x.set_var(\"bar\")\n",
    "x.my_method()\n",
    "\n",
    "x.my_variable = \"zzz\"\n",
    "x.my_method()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## What is this `self` thing?\n",
    "\n",
    "As mentioned, `self` is always the first argument of any method. It contains the data (variables) for a specific instance of a class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Insect:\n",
    "    def __init__(self, species_name, mass, mass_units='milligrams'):\n",
    "        self.species_name = species_name\n",
    "        self.mass = mass\n",
    "        self.mass_units = mass_units\n",
    "    def print_description(self):\n",
    "        print(\"The insect (species {}) has a mass of {} {}.\".format(\n",
    "            self.species_name, self.mass, self.mass_units))\n",
    "    def eat(self,amount):\n",
    "        # (This could alternatively be done with `self.mass += amount`.)\n",
    "        self.mass = self.mass + amount\n",
    "    def exercise(self,amount):\n",
    "        self.mass = self.mass - amount\n",
    "        \n",
    "x = Insect(\"Bombus terrestris\", 200)\n",
    "x.print_description()\n",
    "x.eat(10)\n",
    "x.exercise(5)\n",
    "x.print_description()\n",
    "\n",
    "y = Insect(\"Apis mellifera\", 100)\n",
    "y.print_description()\n",
    "# y.eat(10)\n",
    "# y.print_description()\n",
    "\n",
    "z = Insect(\"Tarantula gigantus\", 10, mass_units=\"grams\") # yes, it's not really an insect...\n",
    "z.print_description()\n",
    "\n",
    "# print(x.species_name,x.mass)\n",
    "# print(y.species_name,y.mass)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can see we access the value of a variable within a method using `self.variable_name`. Outside the function, we can also access the \"instance variables\" but we need to use the instance name and a dot:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(x.species_name)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(y.species_name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As mentioned, most Python libraries make use of classes to organize their code. All objects in Python act a lot like instances of classes. For example, the string methods `.strip()` and `.format()` could be defined on a hypothetical `String` class exactly like the methods above. (In reality, they are likely implemented differently for performance reasons.)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.11.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
