{ "cells": [ { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/11_classes/00_content.ipynb)." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Chapter 11: Classes & Instances" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "In contrast to all the built-in data types introduced in the previous chapters, **classes** allow us to create **user-defined data types**. They enable us to model **data** and its **associated behavior** in an *abstract* way. *Concrete* **instances** of these custom data types then **encapsulate** the **state** in a running program. Often, classes are **blueprints** modeling \"real world things.\"\n", "\n", "Classes and instances follow the **[object-oriented programming ](https://en.wikipedia.org/wiki/Object-oriented_programming)** (OOP) paradigm where a *large program* is broken down into many *small components* (i.e., the objects) that *reuse* code. This way, a program that is too big for a programmer to fully comprehend as a whole becomes maintainable via its easier to understand individual pieces.\n", "\n", "Often, we see the terminology \"classes & objects\" used instead of \"classes & instances\" in Python related texts. In this book, we are more precise as *both* classes and instances are objects as specified already in the \"*Objects vs. Types vs. Values*\" section in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Objects-vs.-Types-vs.-Values)." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Example: Vectors & Matrices" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Neither core Python nor the standard library offer an implementation of common [linear algebra ](https://en.wikipedia.org/wiki/Linear_algebra) functionalities. While we introduce the popular third-party library [numpy](http://www.numpy.org/) in [Chapter 10 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/10_arrarys/00_content.ipynb) as the de-facto standard for that and recommend to use it in real-life projects, we show how one could use Python's object-oriented language features to implement common matrix and vector operations throughout this chapter. Once we have achieved that, we compare our own library with [numpy](http://www.numpy.org/).\n", "\n", "Without classes, we could model a vector, for example, with a `tuple` or a `list` object, depending on if we want it to be mutable or not.\n", "\n", "Let's take the following vector $\\vec{x}$ as an example and model it as a `tuple`:" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "$\\vec{x} = \\begin{pmatrix} 1 \\\\ 2 \\\\ 3 \\end{pmatrix}$" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "x = (1, 2, 3)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "(1, 2, 3)" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "x" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "We can extend this approach and model a matrix as either a `tuple` holding other `tuple`s or as a `list` holding other `list`s or as a mixture of both. Then, we *must* decide if the inner objects represent rows or columns. A common convention is to go with the former.\n", "\n", "For example, let's model the matrix $\\bf{A}$ below as a `list` of row `list`s:" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "$\\bf{A} = \\begin{bmatrix} 1 & 2 & 3 \\\\ 4 & 5 & 6 \\\\ 7 & 8 & 9 \\end{bmatrix}$" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "A = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "[[1, 2, 3], [4, 5, 6], [7, 8, 9]]" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "A" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "While this way of representing vectors and matrices in memory keeps things simple, we cannot work with them easily as Python does not know about the **semantics** (i.e., \"rules\") of vectors and matrices modeled as `tuple`s and `list`s of `list`s.\n", "\n", "For example, we should be able to multiply $\\bf{A}$ with $\\vec{x}$ if their dimensions match. However, Python does not know how to do this and raises a `TypeError`." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "$\\bf{A} * \\vec{x} = \\begin{bmatrix} 1 & 2 & 3 \\\\ 4 & 5 & 6 \\\\ 7 & 8 & 9 \\end{bmatrix} * \\begin{pmatrix} 1 \\\\ 2 \\\\ 3 \\end{pmatrix} = \\begin{pmatrix} 14 \\\\ 32 \\\\ 50 \\end{pmatrix}$" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "ename": "TypeError", "evalue": "can't multiply sequence by non-int of type 'tuple'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[5], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mA\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mx\u001b[49m\n", "\u001b[0;31mTypeError\u001b[0m: can't multiply sequence by non-int of type 'tuple'" ] } ], "source": [ "A * x" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Throughout this chapter, we \"teach\" Python the rules of [linear algebra ](https://en.wikipedia.org/wiki/Linear_algebra)." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Class Definition" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The compound `class` statement creates a new variable that references a **class object** in memory.\n", "\n", "Following the *header* line, the indented *body* syntactically consists of function definitions (i.e., `.dummy_method()`) and variable assignments (i.e., `.dummy_variable`). Any code put here is executed just as if it were outside the `class` statement. However, the class object acts as a **namespace**, meaning that all the names do *not* exist in the global scope but may only be accessed with the dot operator `.` on the class object. In this context, the names are called **class attributes**.\n", "\n", "Within classes, functions are referred to as **methods** that are **bound** to *future* **instance objects**. This binding process means that Python *implicitly* inserts a reference to a *concrete* instance object as the first argument to any **method invocation** (i.e., \"function call\"). By convention, we call this parameter `self` as it references the instance object on which the method is invoked. Then, as the method is executed, we can set and access attributes via the dot operator `.` on `self`. That is how we manage the *state* of a *concrete* instance within a *generically* written class. At the same time, the code within a method is reused whenever we invoke a method on *any* instance.\n", "\n", "As indicated by [PEP 257 ](https://www.python.org/dev/peps/pep-0257/) and also section 3.8.4 of the [Google Python Style Guide ](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#384-classes), we use docstrings to document relevant parts of the new data type. With respect to naming, classes are named according to the [CamelCase ](https://en.wikipedia.org/wiki/Camel_case) convention while instances are treated like normal variables and named in [snake\\_case ](https://en.wikipedia.org/wiki/Snake_case)." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "code_folding": [], "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "class Vector:\n", " \"\"\"A one-dimensional vector from linear algebra.\"\"\"\n", "\n", " dummy_variable = \"I am a vector\"\n", "\n", " def dummy_method(self):\n", " \"\"\"A dummy method for illustration purposes.\"\"\"\n", " return self.dummy_variable" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "`Vector` is an object on its own with an identity, a type, and a value." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "94113690586816" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "id(Vector)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Its type is `type` indicating that it represents a user-defined data type and it evaluates to its fully qualified name (i.e., `__main__` as it is defined in this Jupyter notebook).\n", "\n", "We have seen the type `type` before in the \"*Constructors*\" section in [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb#Constructors) and also in the \"*The `namedtuple` Type*\" section in [Chapter 7's Appendix ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/05_appendix.ipynb#The-namedtuple-Type). In the latter case, we could also use a `Point` class but the [namedtuple() ](https://docs.python.org/3/library/collections.html#collections.namedtuple) function from the [collections ](https://docs.python.org/3/library/collections.html) module in the [standard library ](https://docs.python.org/3/library/index.html) is a convenient shortcut to create custom data types that can be derived out of a plain `tuple`.\n", "\n", "In all examples, if an object's type is `type`, we can simply view it as a blueprint for a \"family\" of objects." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "type" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(Vector)" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "__main__.Vector" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Vector" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The docstrings are transformed into convenient help texts." ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "\u001b[0;31mInit signature:\u001b[0m \u001b[0mVector\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[0;31mDocstring:\u001b[0m A one-dimensional vector from linear algebra.\n", "\u001b[0;31mType:\u001b[0m type\n", "\u001b[0;31mSubclasses:\u001b[0m " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "Vector?" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Help on class Vector in module __main__:\n", "\n", "class Vector(builtins.object)\n", " | A one-dimensional vector from linear algebra.\n", " |\n", " | Methods defined here:\n", " |\n", " | dummy_method(self)\n", " | A dummy method for illustration purposes.\n", " |\n", " | ----------------------------------------------------------------------\n", " | Data descriptors defined here:\n", " |\n", " | __dict__\n", " | dictionary for instance variables\n", " |\n", " | __weakref__\n", " | list of weak references to the object\n", " |\n", " | ----------------------------------------------------------------------\n", " | Data and other attributes defined here:\n", " |\n", " | dummy_variable = 'I am a vector'\n", "\n" ] } ], "source": [ "help(Vector)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "We can use the built-in [vars() ](https://docs.python.org/3/library/functions.html#vars) function as an alternative to [dir() ](https://docs.python.org/3/library/functions.html#dir) to obtain a *brief* summary of the attributes on `Vector`. Whereas [vars() ](https://docs.python.org/3/library/functions.html#vars) returns a read-only `dict`-like overview on mostly the *explicitly* defined attributes, [dir() ](https://docs.python.org/3/library/functions.html#dir) also shows all *implicitly* added attributes in a `list`." ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "mappingproxy({'__module__': '__main__',\n", " '__doc__': 'A one-dimensional vector from linear algebra.',\n", " 'dummy_variable': 'I am a vector',\n", " 'dummy_method': ,\n", " '__dict__': ,\n", " '__weakref__': })" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vars(Vector)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "['__class__',\n", " '__delattr__',\n", " '__dict__',\n", " '__dir__',\n", " '__doc__',\n", " '__eq__',\n", " '__format__',\n", " '__ge__',\n", " '__getattribute__',\n", " '__getstate__',\n", " '__gt__',\n", " '__hash__',\n", " '__init__',\n", " '__init_subclass__',\n", " '__le__',\n", " '__lt__',\n", " '__module__',\n", " '__ne__',\n", " '__new__',\n", " '__reduce__',\n", " '__reduce_ex__',\n", " '__repr__',\n", " '__setattr__',\n", " '__sizeof__',\n", " '__str__',\n", " '__subclasshook__',\n", " '__weakref__',\n", " 'dummy_method',\n", " 'dummy_variable']" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dir(Vector)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "With the dot operator `.` we access the class attributes." ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "'I am a vector'" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Vector.dummy_variable" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Vector.dummy_method" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "However, invoking the `.dummy_method()` raises a `TypeError`. That makes sense as the method expects a *concrete* instance passed in as the `self` argument. However, we have not yet created one." ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "ename": "TypeError", "evalue": "Vector.dummy_method() missing 1 required positional argument: 'self'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[16], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mVector\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdummy_method\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", "\u001b[0;31mTypeError\u001b[0m: Vector.dummy_method() missing 1 required positional argument: 'self'" ] } ], "source": [ "Vector.dummy_method()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "## Instantiation" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "To create a *new* instance, we need to **instantiate** one.\n", "\n", "In the `class` statement, we see a `.__init__()` method that contains all the validation logic that we require a `Vector` instance to adhere to. In a way, this method serves as a constructor-like function.\n", "\n", "`.__init__()` is an example of a so-called **special method** that we use to make new data types integrate with Python's language features. Their naming follows the dunder convention. In this chapter, we introduce some of the more common special methods, and we refer to the [language reference ](https://docs.python.org/3/reference/datamodel.html) for an exhaustive list of all special methods. Special methods not *explicitly* defined in a class are *implicitly* added with a default implementation.\n", "\n", "The `.__init__()` method (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__init__)) is responsible for **initializing** a *new* instance object immediately after its creation. That usually means setting up some **instance attributes**. In the example, a new `Vector` instance is created from some sequence object (e.g., a `tuple` like `x`) passed in as the `data` argument. The elements provided by the `data` argument are first cast as `float` objects and then stored in a `list` object named `._entries` on the *new* instance object. Together, the `float`s represent the state encapsulated within an instance.\n", "\n", "A best practice is to *separate* the way we use a data type (i.e., its \"behavior\") from how we implement it. By convention, attributes that should not be accessed from \"outside\" of an instance start with one leading underscore `_`. In the example, the instance attribute `._entries` is such an **implementation detail**: We could have decided to store a `Vector`'s entries in a `tuple` instead of a `list`. However, this decision should *not* affect how a `Vector` instance is to be used. Moreover, if we changed how the `._entries` are modeled later on, this must *not* break any existing code using `Vector`s. This idea is also known as **[information hiding ](https://en.wikipedia.org/wiki/Information_hiding)** in software engineering." ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "class Vector:\n", " \"\"\"A one-dimensional vector from linear algebra.\n", "\n", " All entries are converted to floats.\n", " \"\"\"\n", "\n", " def __init__(self, data):\n", " \"\"\"Create a new vector.\n", "\n", " Args:\n", " data (sequence): the vector's entries\n", "\n", " Raises:\n", " ValueError: if no entries are provided\n", " \"\"\"\n", " self._entries = list(float(x) for x in data)\n", " if len(self._entries) == 0:\n", " raise ValueError(\"a vector must have at least one entry\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "To create a `Vector` instance, we call the `Vector` class with the `()` operator. This call is forwarded to the `.__init__()` method behind the scenes. That is what we mean by saying \"make new data types integrate with Python's language features\" above: We use `Vector` just as any other built-in constructor." ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "v = Vector([1, 2, 3])" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "`v` is an object as well." ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "140306181183328" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "id(v)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Unsurprisingly, the type of `v` is `Vector`. That is the main point of this chapter." ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "__main__.Vector" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(v)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "`v`'s semantic \"value\" is not so clear yet. We fix this in the next section." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "<__main__.Vector at 0x7f9b9416d760>" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "v" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Although the `.__init__()` method defines *two* parameters, we must call it with only *one* `data` argument. As noted above, Python implicitly inserts a reference to the newly created instance object (i.e., `v`) as the first argument as `self`.\n", "\n", "Calling a class object with a wrong number of arguments leads to generic `TypeError`s ..." ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "ename": "TypeError", "evalue": "Vector.__init__() missing 1 required positional argument: 'data'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[22], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mVector\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", "\u001b[0;31mTypeError\u001b[0m: Vector.__init__() missing 1 required positional argument: 'data'" ] } ], "source": [ "Vector()" ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "ename": "TypeError", "evalue": "Vector.__init__() takes 2 positional arguments but 4 were given", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[23], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mVector\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m3\u001b[39;49m\u001b[43m)\u001b[49m\n", "\u001b[0;31mTypeError\u001b[0m: Vector.__init__() takes 2 positional arguments but 4 were given" ] } ], "source": [ "Vector(1, 2, 3)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "... while creating a `Vector` instance from an empty sequence raises a custom `ValueError`." ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "ename": "ValueError", "evalue": "a vector must have at least one entry", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[24], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mVector\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n", "Cell \u001b[0;32mIn[17], line 18\u001b[0m, in \u001b[0;36mVector.__init__\u001b[0;34m(self, data)\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mlist\u001b[39m(\u001b[38;5;28mfloat\u001b[39m(x) \u001b[38;5;28;01mfor\u001b[39;00m x \u001b[38;5;129;01min\u001b[39;00m data)\n\u001b[1;32m 17\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m---> 18\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma vector must have at least one entry\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", "\u001b[0;31mValueError\u001b[0m: a vector must have at least one entry" ] } ], "source": [ "Vector([])" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Even though we can access the `._entries` attribute on the `v` object (i.e., \"from outside\"), we are *not* supposed to do that because of the underscore `_` convention. In other words, we should access `._entries` only from within a method via `self`." ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "[1.0, 2.0, 3.0]" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "v._entries # by convention not allowed" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Text Representations" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "For all the built-in data types, an object's value is represented in a *literal notation*, implying that we can simply copy and paste the value into another code cell to create a *new* object with the *same* value.\n", "\n", "The exact representation of the value does *not* have to be identical to the one used to create the object. For example, we can create a `tuple` object without using parentheses and Python still outputs its value with `(` and `)`. That was an *arbitrary* design decision by the core development team." ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "x = 1, 2, 3" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "(1, 2, 3)" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "x" ] }, { "cell_type": "code", "execution_count": 28, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "(1, 2, 3)" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(1, 2, 3)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "To control how objects of a user-defined data type are represented as text, we implement the `.__repr__()` (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__repr__)) and `.__str__()` (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__str__)) methods. Both take only a `self` argument and must return a `str` object." ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "class Vector:\n", "\n", " def __init__(self, data):\n", " self._entries = list(float(x) for x in data)\n", " # ...\n", "\n", " def __repr__(self):\n", " args = \", \".join(repr(x) for x in self._entries)\n", " return f\"Vector(({args}))\"\n", "\n", " def __str__(self):\n", " first, last = self._entries[0], self._entries[-1]\n", " n_entries = len(self._entries)\n", " return f\"Vector({first!r}, ..., {last!r})[{n_entries:d}]\"" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Now, when `v` is evaluated in a code cell, we see the return value of the `.__repr__()` method.\n", "\n", "According to the specification, `.__repr__()` should return a `str` object that, when used as a literal, creates a *new* instance with the *same* state (i.e., their `._entries` attributes compare equal) as the original one. In other words, it should return a **text representation** of the object optimized for direct consumption by the Python interpreter. That is often useful when debugging or logging large applications.\n", "\n", "Our implementation of `.__repr__()` in the `Vector` class uses to a `tuple` notation for the `data` argument. So, even if we create `v` from a `list` object like `[1, 2, 3]` and even though the `_entries` are stored as a `list` object internally, a `Vector` instance's text representation \"defaults\" to `((` and `))` in the output. This decision is arbitrary and we could have used a `list` notation for the `data` argument as well." ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "v = Vector([1, 2, 3])" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "Vector((1.0, 2.0, 3.0))" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "v" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "If we copy and paste the value of the `v` object into another code cell, we create a *new* `Vector` instance with the *same* state as `v`." ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "Vector((1.0, 2.0, 3.0))" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Vector((1.0, 2.0, 3.0))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Alternatively, the built-in [repr()]( https://docs.python.org/3/library/functions.html#repr) function returns an object's value as a `str` object (i.e., with the quotes `'`)." ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "'Vector((1.0, 2.0, 3.0))'" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "repr(v)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "On the contrary, the `.__str__()` method should return a *human-readable* text representation of the object, and we use the built-in [str() ](https://docs.python.org/3/library/functions.html#func-str) and [print() ](https://docs.python.org/3/library/functions.html#print) functions to obtain this representation explicitly.\n", "\n", "For our `Vector` class, this representation only shows a `Vector`'s first and last entries followed by the total number of entries in brackets. So, even for a `Vector` containing millions of entries, we could easily make sense of the representation.\n", "\n", "While [str() ](https://docs.python.org/3/library/functions.html#func-str) returns the text representation as a `str` object, ..." ] }, { "cell_type": "code", "execution_count": 34, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "'Vector(1.0, ..., 3.0)[3]'" ] }, "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ "str(v)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "... [print() ](https://docs.python.org/3/library/functions.html#print) does not show the enclosing quotes." ] }, { "cell_type": "code", "execution_count": 35, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Vector(1.0, ..., 3.0)[3]\n" ] } ], "source": [ "print(v)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "From a theoretical point of view, the text representation provided by `.__repr__()` contains all the information (i.e., the $0$s and $1$s in memory) that is needed to model something in a computer. In a way, it is a natural extension from the binary (cf., [Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/00_content.ipynb#Binary-Representations)), hexadecimal (cf., [Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/00_content.ipynb#Hexadecimal-Representations)), and `bytes` (cf., [Chapter 6 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/02_content.ipynb#The-bytes-Type)) representations of information. After all, just like Unicode characters are encoded in `bytes`, the more \"complex\" objects in this chapter are encoded in Unicode characters via their text representations." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### The `Matrix` Class" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Below is a first implementation of the `Matrix` class that stores the `._entries` internally as a `list` of `list`s.\n", "\n", "The `.__init__()` method ensures that all the rows come with the same number of columns. Again, we do not allow `Matrix` instances without any entries." ] }, { "cell_type": "code", "execution_count": 36, "metadata": { "code_folding": [], "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "class Matrix:\n", "\n", " def __init__(self, data):\n", " self._entries = list(list(float(x) for x in r) for r in data)\n", " for row in self._entries[1:]:\n", " if len(row) != len(self._entries[0]):\n", " raise ValueError(\"rows must have the same number of entries\")\n", " if len(self._entries) == 0:\n", " raise ValueError(\"a matrix must have at least one entry\")\n", "\n", " def __repr__(self):\n", " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", " return f\"Matrix(({args}))\"\n", "\n", " def __str__(self):\n", " first, last = self._entries[0][0], self._entries[-1][-1]\n", " m, n = len(self._entries), len(self._entries[0])\n", " return f\"Matrix(({first!r}, ...), ..., (..., {last!r}))[{m:d}x{n:d}]\"" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "`Matrix` is an object as well." ] }, { "cell_type": "code", "execution_count": 37, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "94113690738160" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "id(Matrix)" ] }, { "cell_type": "code", "execution_count": 38, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "type" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(Matrix)" ] }, { "cell_type": "code", "execution_count": 39, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "__main__.Matrix" ] }, "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Matrix" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Let's create a new `Matrix` instance from a `list` of `tuple`s." ] }, { "cell_type": "code", "execution_count": 40, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "m = Matrix([(1, 2, 3), (4, 5, 6), (7, 8, 9)])" ] }, { "cell_type": "code", "execution_count": 41, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "140306180401856" ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "id(m)" ] }, { "cell_type": "code", "execution_count": 42, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "__main__.Matrix" ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(m)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The text representations work as above." ] }, { "cell_type": "code", "execution_count": 43, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" ] }, "execution_count": 43, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m" ] }, { "cell_type": "code", "execution_count": 44, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Matrix((1.0, ...), ..., (..., 9.0))[3x3]\n" ] } ], "source": [ "print(m)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Passing an invalid `data` argument when instantiating a `Matrix` results in the documented exceptions." ] }, { "cell_type": "code", "execution_count": 45, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "ename": "ValueError", "evalue": "a matrix must have at least one entry", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[45], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mMatrix\u001b[49m\u001b[43m(\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n", "Cell \u001b[0;32mIn[36], line 9\u001b[0m, in \u001b[0;36mMatrix.__init__\u001b[0;34m(self, data)\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrows must have the same number of entries\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m----> 9\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma matrix must have at least one entry\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", "\u001b[0;31mValueError\u001b[0m: a matrix must have at least one entry" ] } ], "source": [ "Matrix(())" ] }, { "cell_type": "code", "execution_count": 46, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "ename": "ValueError", "evalue": "rows must have the same number of entries", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[46], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mMatrix\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m3\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m4\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m5\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n", "Cell \u001b[0;32mIn[36], line 7\u001b[0m, in \u001b[0;36mMatrix.__init__\u001b[0;34m(self, data)\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m row \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries[\u001b[38;5;241m1\u001b[39m:]:\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(row) \u001b[38;5;241m!=\u001b[39m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries[\u001b[38;5;241m0\u001b[39m]):\n\u001b[0;32m----> 7\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrows must have the same number of entries\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m 9\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma matrix must have at least one entry\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", "\u001b[0;31mValueError\u001b[0m: rows must have the same number of entries" ] } ], "source": [ "Matrix([(1, 2, 3), (4, 5)])" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Instance Methods vs. Class Methods" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The methods we have seen so far are all **instance methods**. The characteristic idea behind an instance method is that the behavior it provides either depends on the state of a concrete instance or mutates it. In other words, an instance method *always* works with attributes on the `self` argument. If a method does *not* need access to `self` to do its job, it is conceptually *not* an instance method and we should probably convert it into another kind of method as shown below.\n", "\n", "An example of an instance method from linear algebra is the `.transpose()` method below that switches the rows and columns of an *existing* `Matrix` instance and returns a *new* `Matrix` instance based off that. It is implemented by passing the *iterator* created with the [zip() ](https://docs.python.org/3/library/functions.html#zip) built-in as the `data` argument to the `Matrix` constructor: The expression `zip(*self._entries)` may be a bit hard to understand because of the involved unpacking but simply flips a `Matrix`'s rows and columns. The built-in [list() ](https://docs.python.org/3/library/functions.html#func-list) constructor within the `.__init__()` method then materializes the iterator into the `._entries` attribute. Without a concrete `Matrix`'s rows and columns, `.transpose()` does not make sense, conceptually speaking.\n", "\n", "Also, we see that it is ok to reference a class from within one of its methods. While this seems trivial to some readers, others may find this confusing. The final versions of the `Vector` and `Matrix` classes in the [fourth part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/04_content.ipynb#The-final-Vector-and-Matrix-Classes) of this chapter show how this \"hard coded\" redundancy can be avoided." ] }, { "cell_type": "code", "execution_count": 47, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "class Matrix:\n", "\n", " def __init__(self, data):\n", " self._entries = list(list(float(x) for x in r) for r in data)\n", " # ...\n", "\n", " def __repr__(self):\n", " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", " return f\"Matrix(({args}))\"\n", "\n", " def transpose(self):\n", " return Matrix(zip(*self._entries))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The `.transpose()` method returns a *new* `Matrix` instance where the rows and columns are flipped." ] }, { "cell_type": "code", "execution_count": 48, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "m = Matrix([(1, 2, 3), (4, 5, 6), (7, 8, 9)])" ] }, { "cell_type": "code", "execution_count": 49, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m" ] }, { "cell_type": "code", "execution_count": 50, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "Matrix(((1.0, 4.0, 7.0,), (2.0, 5.0, 8.0,), (3.0, 6.0, 9.0,)))" ] }, "execution_count": 50, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m.transpose()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Two invocations of `.transpose()` may be chained, which negates its overall effect but still creates a *new* instance object (i.e., `m is n` is `False`)." ] }, { "cell_type": "code", "execution_count": 51, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "n = m.transpose().transpose()" ] }, { "cell_type": "code", "execution_count": 52, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" ] }, "execution_count": 52, "metadata": {}, "output_type": "execute_result" } ], "source": [ "n" ] }, { "cell_type": "code", "execution_count": 53, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 53, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m is n" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Unintuitively, the comparison operator `==` returns a wrong result as `m` and `n` have `_entries` attributes that compare equal. We fix this in the \"*Operator Overloading*\" section later in this chapter." ] }, { "cell_type": "code", "execution_count": 54, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 54, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m == n" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Sometimes, it is useful to attach functionality to a class object that does *not* depend on the state of a concrete instance but on the class as a whole. Such methods are called **class methods** and can be created with the [classmethod() ](https://docs.python.org/3/library/functions.html#classmethod) built-in combined with the `@` **decorator** syntax. Then, Python adapts the binding process described above such that it implicitly inserts a reference to the class object itself instead of the instance when the method is invoked. By convention, we name this parameter `cls`.\n", "\n", "Class methods are often used to provide an alternative way to create instances, usually from a different kind of arguments. As an example, `.from_columns()` expects a sequence of columns instead of rows as its `data` argument. It forwards the invocation to the `.__init__()` method (i.e., what `cls(data)` does; `cls` references the *same* class object as `Matrix`), then calls the `.transpose()` method on the newly created instance, and lastly returns the instance created by `.transpose()`. Again, we are intelligently *reusing* a lot of code." ] }, { "cell_type": "code", "execution_count": 55, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "class Matrix:\n", "\n", " def __init__(self, data):\n", " self._entries = list(list(float(x) for x in r) for r in data)\n", " # ...\n", "\n", " def __repr__(self):\n", " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", " return f\"Matrix(({args}))\"\n", "\n", " def transpose(self):\n", " return Matrix(zip(*self._entries))\n", "\n", " @classmethod\n", " def from_columns(cls, data):\n", " return cls(data).transpose()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "We use the alternative `.from_columns()` constructor to create a `Matrix` equivalent to `m` above from a `list` of columns instead of rows." ] }, { "cell_type": "code", "execution_count": 56, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "m = Matrix.from_columns([(1, 4, 7), (2, 5, 8), (3, 6, 9)])" ] }, { "cell_type": "code", "execution_count": 57, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" ] }, "execution_count": 57, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "There is also a [staticmethod() ](https://docs.python.org/3/library/functions.html#staticmethod) built-in to be used with the `@` syntax to define methods that are *independent* from both the class and instance objects but nevertheless related semantically to a class. In this case, the binding process is disabled an no argument is implicitly inserted upon a method's invocation. Such **static methods** are not really needed most of the time and we omit them here fore brevity." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Computed Properties" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "After creation, a `Matrix` instance exhibits certain properties that depend only on the concrete `data` encapsulated in it. For example, every `Matrix` instance implicitly has *two* dimensions: These are commonly denoted as $m$ and $n$ in math and represent the number of rows and columns.\n", "\n", "We would like our `Matrix` instances to have two attributes, `.n_rows` and `.n_cols`, that provide the correct dimensions as `int` objects. To achieve that, we implement two instance methods, `.n_rows()` and `.n_cols()`, and make them **derived attributes** by decorating them with the [property() ](https://docs.python.org/3/library/functions.html#property) built-in. They work like methods except that they do not need to be invoked with the call operator `()` but can be accessed as if they were instance variables.\n", "\n", "To reuse their code, we integrate the new properties already within the `.__init__()` method." ] }, { "cell_type": "code", "execution_count": 58, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "class Matrix:\n", "\n", " def __init__(self, data):\n", " self._entries = list(list(float(x) for x in r) for r in data)\n", " for row in self._entries[1:]:\n", " if len(row) != self.n_cols:\n", " raise ValueError(\"rows must have the same number of entries\")\n", " if self.n_rows == 0:\n", " raise ValueError(\"a matrix must have at least one entry\")\n", "\n", " def __repr__(self):\n", " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", " return f\"Matrix(({args}))\"\n", "\n", " @property\n", " def n_rows(self):\n", " return len(self._entries)\n", "\n", " @property\n", " def n_cols(self):\n", " return len(self._entries[0])" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The revised `m` models a $2 \\times 3$ matrix." ] }, { "cell_type": "code", "execution_count": 59, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "m = Matrix([(1, 2, 3), (4, 5, 6)])" ] }, { "cell_type": "code", "execution_count": 60, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "(2, 3)" ] }, "execution_count": 60, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m.n_rows, m.n_cols" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "In its basic form, properties are *read-only* attributes. This makes sense for `Matrix` instances where we can *not* \"set\" how many rows and columns there are while keeping the `_entries` unchanged." ] }, { "cell_type": "code", "execution_count": 61, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "ename": "AttributeError", "evalue": "property 'n_rows' of 'Matrix' object has no setter", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[61], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mm\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mn_rows\u001b[49m \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m3\u001b[39m\n", "\u001b[0;31mAttributeError\u001b[0m: property 'n_rows' of 'Matrix' object has no setter" ] } ], "source": [ "m.n_rows = 3" ] } ], "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.12.2" }, "livereveal": { "auto_select": "code", "auto_select_fragment": true, "scroll": true, "theme": "serif" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": false, "sideBar": true, "skip_h1_title": true, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": { "height": "calc(100% - 180px)", "left": "10px", "top": "150px", "width": "384px" }, "toc_section_display": false, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }