From 9f3718f08173db574a67f5c8efbe9e4969060f8b Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauldmccarthy@gmail.com> Date: Mon, 12 Feb 2018 18:10:52 +0000 Subject: [PATCH] New practical on operator overloading. Needs work --- advanced_topics/operator_overloading.ipynb | 588 +++++++++++++++++++++ advanced_topics/operator_overloading.md | 434 +++++++++++++++ 2 files changed, 1022 insertions(+) create mode 100644 advanced_topics/operator_overloading.ipynb create mode 100644 advanced_topics/operator_overloading.md diff --git a/advanced_topics/operator_overloading.ipynb b/advanced_topics/operator_overloading.ipynb new file mode 100644 index 0000000..45a7207 --- /dev/null +++ b/advanced_topics/operator_overloading.ipynb @@ -0,0 +1,588 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Operator overloading\n", + "\n", + "\n", + "> This practical assumes you are familiar with the basics of object-oriented\n", + "> programming in Python.\n", + "\n", + "\n", + "Operator overloading, in an object-oriented programming language, is the\n", + "process of customising the behaviour of _operators_ (e.g. `+`, `*`, `/` and\n", + "`-`) on user-defined types. This practical aims to show you that operator\n", + "overloading is __very__ easy to do in Python.\n", + "\n", + "\n", + "## Overview\n", + "\n", + "\n", + "In Python, when you add two numbers together:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "a = 5\n", + "b = 10\n", + "r = a + b\n", + "print(r)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What actually goes on behind the scenes is this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = a.__add__(b)\n", + "print(r)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In other words, whenever you use the `+` operator on two variables (the\n", + "operands to the `+` operator), the Python interpreter calls the `__add__`\n", + "method of the first operand (`a`), and passes the second operand (`b`) as an\n", + "argument.\n", + "\n", + "\n", + "So it is very easy to use the `+` operator with our own classes - all we have\n", + "to do is implement a method called `__add__`.\n", + "\n", + "\n", + "## Arithmetic operators\n", + "\n", + "\n", + "Let's play with an example - a class which represents a 2D vector:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Vector(object):\n", + " def __init__(self, x, y):\n", + " self.x = x\n", + " self.y = y\n", + "\n", + " def __str__(self):\n", + " return 'Vector({}, {})'.format(self.x, self.y)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> Note that we have implemented the special `__str__` method, which allows our\n", + "> `Vector` instances to be converted into strings.\n", + "\n", + "\n", + "If we try to use the `+` operator on this class, we are bound to get an error:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "v1 = Vector(2, 3)\n", + "v2 = Vector(4, 5)\n", + "print(v1 + v2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But all we need to do to support the `+` operator is to implement a method\n", + "called `__add__`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Vector(object):\n", + " def __init__(self, x, y):\n", + " self.x = x\n", + " self.y = y\n", + "\n", + " def __str__(self):\n", + " return 'Vector({}, {})'.format(self.x, self.y)\n", + "\n", + " def __add__(self, other):\n", + " return Vector(self.x + other.x,\n", + " self.y + other.y)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And now we can use `+` on `Vector` objects - it's that easy:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "v1 = Vector(2, 3)\n", + "v2 = Vector(4, 5)\n", + "print('{} + {} = {}'.format(v1, v2, v1 + v2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our `__add__` method creates and returns a new `Vector` which contains the sum\n", + "of the `x` and `y` components of the `Vector` on which it is called, and the\n", + "`Vector` which is passed in. We could also make the `__add__` method work\n", + "with scalars, by extending its definition a bit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Vector(object):\n", + " def __init__(self, x, y):\n", + " self.x = x\n", + " self.y = y\n", + "\n", + " def __add__(self, other):\n", + " if isinstance(other, Vector):\n", + " return Vector(self.x + other.x,\n", + " self.y + other.y)\n", + " else:\n", + " return Vector(self.x + other, self.y + other)\n", + "\n", + " def __str__(self):\n", + " return 'Vector({}, {})'.format(self.x, self.y)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So now we can add both `Vectors` and scalars numbers together:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "v1 = Vector(2, 3)\n", + "v2 = Vector(4, 5)\n", + "n = 6\n", + "\n", + "print('{} + {} = {}'.format(v1, v2, v1 + v2))\n", + "print('{} + {} = {}'.format(v1, n, v1 + n))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Other nuemric and logical operators can be supported by implementing the\n", + "appropriate method, for example:\n", + "\n", + "- Multiplication (`*`): `__mul__`\n", + "- Division (`/`): `__div__`\n", + "- Negation (`-`): `__neg__`\n", + "- In-place addition (`+=`): `__iadd__`\n", + "- Exclusive or (`^`): `__xor__`\n", + "\n", + "\n", + "Take a look at the [official\n", + "documentation](https://docs.python.org/3.5/reference/datamodel.html#emulating-numeric-types)\n", + "for a full list of the arithmetic and logical operators that your classes can\n", + "support.\n", + "\n", + "\n", + "## Equality and comparison operators\n", + "\n", + "\n", + "Adding support for equality (`==`, `!=`) and comparison (e.g. `>=`) operators\n", + "is just as easy. Imagine that we have a class called `Label`, which represents\n", + "a label in a lookup table. Our `Label` has an integer label, a name, and an\n", + "RGB colour:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Label(object):\n", + " def __init__(self, label, name, colour):\n", + " self.label = label\n", + " self.name = name\n", + " self.colour = colour" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to ensure that a list of `Label` objects is ordered by their label\n", + "values, we can implement a set of functions, so that `Label` classes can be\n", + "compared using the standard comparison operators:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import functools\n", + "\n", + "@functools.total_ordering\n", + "class Label(object):\n", + " def __init__(self, label, name, colour):\n", + " self.label = label\n", + " self.name = name\n", + " self.colour = colour\n", + "\n", + " def __str__(self):\n", + " rgb = ''.join(['{:02x}'.format(c) for c in self.colour])\n", + " return 'Label({}, {}, #{})'.format(self.label, self.name, rgb)\n", + "\n", + " def __repr__(self):\n", + " return str(self)\n", + "\n", + " def __eq__(self, other):\n", + " return self.label == other.label\n", + "\n", + " def __lt__(self, other):\n", + " return self.label < other.label" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> We also added `__str__` and `__repr__` methods to the `Label` class so that\n", + "> `Label` instances will be printed nicely.\n", + "\n", + "\n", + "Now we can compare and sort our `Label` instances:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "l1 = Label(1, 'Parietal', (255, 0, 0))\n", + "l2 = Label(2, 'Occipital', ( 0, 255, 0))\n", + "l3 = Label(3, 'Temporal', ( 0, 0, 255))\n", + "\n", + "print('{} > {}: {}'.format(l1, l2, l1 > l2))\n", + "print('{} < {}: {}'.format(l1, l3, l1 < l3))\n", + "print('{} != {}: {}'.format(l2, l3, l2 != l3))\n", + "print(sorted((l3, l1, l2)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `@functools.total_ordering` is a convenience decorator which, given a\n", + "class that implements equality and a single comparison function (`__lt__` in\n", + "the above code), will \"fill in\" the remainder of the comparison operators. If\n", + "you need very specific or complicated behaviour, then you can provide methods\n", + "for _all_ of the comparison operators, e.g. `__gt__` for `>`, `__ge__` for\n", + "`>=`, etc.).\n", + "\n", + "\n", + "But if you just want the operators to work in the conventional manner, you can\n", + "just use the `@functools.total_ordering` decorator, and provide `__eq__`, and\n", + "just one of `__lt__`, `__le__`, `__gt__` or `__ge__`.\n", + "\n", + "\n", + "Refer to the [official\n", + "documentation](https://docs.python.org/3.5/reference/datamodel.html#object.__lt__)\n", + "for all of the details on supporting comparison operators.\n", + "\n", + "\n", + "> You may see the `__cmp__` method in older code bases - this provides a\n", + "> C-style comparison function which returns `<0`, `0`, or `>0` based on\n", + "> comparing two items. This has been superseded by the rich\n", + "> comparison operators and is no longer used in Python 3.\n", + "\n", + "\n", + "## The indexing operator `[]`\n", + "\n", + "\n", + "The indexing operator (`[]`) is generally used by \"container\" types, such as\n", + "the built-in `list` and `dict` classes.\n", + "\n", + "\n", + "At its essence, there are only three types of behaviours that are possible\n", + "with the `[]` operator. All that is needed to support them are to implement\n", + "three special methods in your class, regardless of whether your class will be\n", + "indexed by sequential integers (like a `list`) or by hashable values (like a\n", + "`dict`):\n", + "\n", + "\n", + "- __Retrieval__ is performed by the `__getitem__` method\n", + "- __Assignment__ is performed by the `__setitem__` method\n", + "- __Deletion__ is performed by the `__delitem__` method\n", + "\n", + "\n", + "Note that, if you implement these methods in your own class, there is no\n", + "requirement for them to actually provide any form. However if you don't, you\n", + "will probably confuse users of your code - make sure you explain it all in\n", + "your comprehensive documentation!\n", + "\n", + "\n", + "The following contrived example demonstrates all three behaviours:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class TwoTimes(object):\n", + "\n", + " def __init__(self):\n", + " self.__deleted = set()\n", + " self.__assigned = {}\n", + "\n", + " def __getitem__(self, key):\n", + " if key in self.__deleted:\n", + " raise KeyError('{} has been deleted!')\n", + " elif key in self.__assigned:\n", + " return self.__assigned[key]\n", + " else:\n", + " return key * 2\n", + "\n", + " def __setitem__(self, key, value):\n", + " self.__assigned[key] = value\n", + "\n", + " def __delitem__(self, key):\n", + " self.__deleted.add(key)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Guess what happens whenever we index a `TwoTimes` object:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tt = TwoTimes()\n", + "print('TwoTimes[{}] = {}'.format(2, tt[2]))\n", + "print('TwoTimes[{}] = {}'.format(6, tt[6]))\n", + "print('TwoTimes[{}] = {}'.format('abc', tt['abc']))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For some unknown reason, the `TwoTimes` class allows us to override the value\n", + "for a specific key:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(tt[4])\n", + "tt[4] = 'this is not 4 * 4'\n", + "print(tt[4])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And we can also \"delete\" keys:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(tt['12345'])\n", + "del tt['12345']\n", + "\n", + "# this is going to raise an error\n", + "print(tt['12345'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you wish to support the Python `start:stop:step` [slice\n", + "notation](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/),\n", + "you simply need to write your `__getitem__` and `__setitem__` methods so that they\n", + "can detect `slice` objects:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class TwoTimes(object):\n", + "\n", + " def __init__(self, max):\n", + " self.__max = max\n", + "\n", + " def __getitem__(self, key):\n", + " if isinstance(key, slice):\n", + " start = key.start or 0\n", + " stop = key.stop or self.__max\n", + " step = key.step or 1\n", + " else:\n", + " start = key\n", + " stop = key + 1\n", + " step = 1\n", + "\n", + " return [i * 2 for i in range(start, stop, step)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tt = TwoTimes(10)\n", + "\n", + "print(tt[5])\n", + "print(tt[3:7])\n", + "print(tt[::2])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> It is possible to sub-class the built-in `list` and `dict` classes if you\n", + "> wish to extend their functionality in some way. However, if you are writing\n", + "> a class that should mimic the one of the `list` or `dict` classes, but work\n", + "> in a different way internally (e.g. a `dict`-like object which uses a\n", + "> different hashing algorithm), the `Sequence` and `MutableMapping` classes\n", + "> are [a better choice](https://stackoverflow.com/a/7148602) - you can find\n", + "> them in the\n", + "> [`collections.abc`](https://docs.python.org/3.5/library/collections.abc.html)\n", + "> module.\n", + "\n", + "\n", + "## The call operator `()`\n", + "\n", + "\n", + "Remember how everything in Python is an object, even functions? When you call\n", + "a function, a method called `__call__` is called on the function object. We can\n", + "implement the `__call__` method on our own class, which will allow us to \"call\"\n", + "objects as if they are functions.\n", + "\n", + "\n", + "For example, the `TimedFunction` class allows us to calculate the execution\n", + "time of any function:\n", + "\n", + "\n", + "## The dot operator `.`\n", + "\n", + "\n", + "Python allows us to override the `.` (dot) operator which is used to access\n", + "the attributes and methods of an object. attributes and methods of an\n", + "object. This is a fairly niche feature, and you need to be careful that you\n", + "don't unintentionally introduce recursive attribute lookups into your code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Vector(object):\n", + " def __init__(self, xyz):\n", + " self.__xyz = list(xyz)\n", + "\n", + " def __str__(self):\n", + " return 'Vector({})'.format(', '.join(self.__xyz))\n", + "\n", + " def __getattr__(self, key):\n", + " key = ['xyz'.index(c) for c in key]\n", + " return [self.__xyz[c] for c in key]\n", + "\n", + " def __setattr__(self, key, value):\n", + " pass # key = ['xyz'.index(c) for c in key]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "v = Vector((4, 5, 6))\n", + "\n", + "print('xyz: ', v.xyz)\n", + "print('yz: ', v.yz)\n", + "print('zxy: ', v.xzy)\n", + "print('y: ', v.y)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Other special methods" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/advanced_topics/operator_overloading.md b/advanced_topics/operator_overloading.md new file mode 100644 index 0000000..3e78721 --- /dev/null +++ b/advanced_topics/operator_overloading.md @@ -0,0 +1,434 @@ +# Operator overloading + + +> This practical assumes you are familiar with the basics of object-oriented +> programming in Python. + + +Operator overloading, in an object-oriented programming language, is the +process of customising the behaviour of _operators_ (e.g. `+`, `*`, `/` and +`-`) on user-defined types. This practical aims to show you that operator +overloading is __very__ easy to do in Python. + + +## Overview + + +In Python, when you add two numbers together: + + +``` +a = 5 +b = 10 +r = a + b +print(r) +``` + + +What actually goes on behind the scenes is this: + + +``` +r = a.__add__(b) +print(r) +``` + + +In other words, whenever you use the `+` operator on two variables (the +operands to the `+` operator), the Python interpreter calls the `__add__` +method of the first operand (`a`), and passes the second operand (`b`) as an +argument. + + +So it is very easy to use the `+` operator with our own classes - all we have +to do is implement a method called `__add__`. + + +## Arithmetic operators + + +Let's play with an example - a class which represents a 2D vector: + + +``` +class Vector(object): + def __init__(self, x, y): + self.x = x + self.y = y + + def __str__(self): + return 'Vector({}, {})'.format(self.x, self.y) +``` + + +> Note that we have implemented the special `__str__` method, which allows our +> `Vector` instances to be converted into strings. + + +If we try to use the `+` operator on this class, we are bound to get an error: + + +``` +v1 = Vector(2, 3) +v2 = Vector(4, 5) +print(v1 + v2) +``` + + +But all we need to do to support the `+` operator is to implement a method +called `__add__`: + + +``` +class Vector(object): + def __init__(self, x, y): + self.x = x + self.y = y + + def __str__(self): + return 'Vector({}, {})'.format(self.x, self.y) + + def __add__(self, other): + return Vector(self.x + other.x, + self.y + other.y) +``` + + +And now we can use `+` on `Vector` objects - it's that easy: + + +``` +v1 = Vector(2, 3) +v2 = Vector(4, 5) +print('{} + {} = {}'.format(v1, v2, v1 + v2)) +``` + + +Our `__add__` method creates and returns a new `Vector` which contains the sum +of the `x` and `y` components of the `Vector` on which it is called, and the +`Vector` which is passed in. We could also make the `__add__` method work +with scalars, by extending its definition a bit: + + +``` +class Vector(object): + def __init__(self, x, y): + self.x = x + self.y = y + + def __add__(self, other): + if isinstance(other, Vector): + return Vector(self.x + other.x, + self.y + other.y) + else: + return Vector(self.x + other, self.y + other) + + def __str__(self): + return 'Vector({}, {})'.format(self.x, self.y) +``` + + +So now we can add both `Vectors` and scalars numbers together: + + +``` +v1 = Vector(2, 3) +v2 = Vector(4, 5) +n = 6 + +print('{} + {} = {}'.format(v1, v2, v1 + v2)) +print('{} + {} = {}'.format(v1, n, v1 + n)) +``` + + +Other nuemric and logical operators can be supported by implementing the +appropriate method, for example: + +- Multiplication (`*`): `__mul__` +- Division (`/`): `__div__` +- Negation (`-`): `__neg__` +- In-place addition (`+=`): `__iadd__` +- Exclusive or (`^`): `__xor__` + + +Take a look at the [official +documentation](https://docs.python.org/3.5/reference/datamodel.html#emulating-numeric-types) +for a full list of the arithmetic and logical operators that your classes can +support. + + +## Equality and comparison operators + + +Adding support for equality (`==`, `!=`) and comparison (e.g. `>=`) operators +is just as easy. Imagine that we have a class called `Label`, which represents +a label in a lookup table. Our `Label` has an integer label, a name, and an +RGB colour: + + +``` +class Label(object): + def __init__(self, label, name, colour): + self.label = label + self.name = name + self.colour = colour +``` + + +In order to ensure that a list of `Label` objects is ordered by their label +values, we can implement a set of functions, so that `Label` classes can be +compared using the standard comparison operators: + + +``` +import functools + +@functools.total_ordering +class Label(object): + def __init__(self, label, name, colour): + self.label = label + self.name = name + self.colour = colour + + def __str__(self): + rgb = ''.join(['{:02x}'.format(c) for c in self.colour]) + return 'Label({}, {}, #{})'.format(self.label, self.name, rgb) + + def __repr__(self): + return str(self) + + def __eq__(self, other): + return self.label == other.label + + def __lt__(self, other): + return self.label < other.label +``` + +> We also added `__str__` and `__repr__` methods to the `Label` class so that +> `Label` instances will be printed nicely. + + +Now we can compare and sort our `Label` instances: + + +``` +l1 = Label(1, 'Parietal', (255, 0, 0)) +l2 = Label(2, 'Occipital', ( 0, 255, 0)) +l3 = Label(3, 'Temporal', ( 0, 0, 255)) + +print('{} > {}: {}'.format(l1, l2, l1 > l2)) +print('{} < {}: {}'.format(l1, l3, l1 < l3)) +print('{} != {}: {}'.format(l2, l3, l2 != l3)) +print(sorted((l3, l1, l2))) +``` + + +The `@functools.total_ordering` is a convenience decorator which, given a +class that implements equality and a single comparison function (`__lt__` in +the above code), will "fill in" the remainder of the comparison operators. If +you need very specific or complicated behaviour, then you can provide methods +for _all_ of the comparison operators, e.g. `__gt__` for `>`, `__ge__` for +`>=`, etc.). + + +But if you just want the operators to work in the conventional manner, you can +just use the `@functools.total_ordering` decorator, and provide `__eq__`, and +just one of `__lt__`, `__le__`, `__gt__` or `__ge__`. + + +Refer to the [official +documentation](https://docs.python.org/3.5/reference/datamodel.html#object.__lt__) +for all of the details on supporting comparison operators. + + +> You may see the `__cmp__` method in older code bases - this provides a +> C-style comparison function which returns `<0`, `0`, or `>0` based on +> comparing two items. This has been superseded by the rich +> comparison operators and is no longer used in Python 3. + + +## The indexing operator `[]` + + +The indexing operator (`[]`) is generally used by "container" types, such as +the built-in `list` and `dict` classes. + + +At its essence, there are only three types of behaviours that are possible +with the `[]` operator. All that is needed to support them are to implement +three special methods in your class, regardless of whether your class will be +indexed by sequential integers (like a `list`) or by hashable values (like a +`dict`): + + +- __Retrieval__ is performed by the `__getitem__` method +- __Assignment__ is performed by the `__setitem__` method +- __Deletion__ is performed by the `__delitem__` method + + +Note that, if you implement these methods in your own class, there is no +requirement for them to actually provide any form. However if you don't, you +will probably confuse users of your code - make sure you explain it all in +your comprehensive documentation! + + +The following contrived example demonstrates all three behaviours: + + +``` +class TwoTimes(object): + + def __init__(self): + self.__deleted = set() + self.__assigned = {} + + def __getitem__(self, key): + if key in self.__deleted: + raise KeyError('{} has been deleted!') + elif key in self.__assigned: + return self.__assigned[key] + else: + return key * 2 + + def __setitem__(self, key, value): + self.__assigned[key] = value + + def __delitem__(self, key): + self.__deleted.add(key) +``` + + +Guess what happens whenever we index a `TwoTimes` object: + + +``` +tt = TwoTimes() +print('TwoTimes[{}] = {}'.format(2, tt[2])) +print('TwoTimes[{}] = {}'.format(6, tt[6])) +print('TwoTimes[{}] = {}'.format('abc', tt['abc'])) +``` + + +For some unknown reason, the `TwoTimes` class allows us to override the value +for a specific key: + + +``` +print(tt[4]) +tt[4] = 'this is not 4 * 4' +print(tt[4]) +``` + + +And we can also "delete" keys: + + +``` +print(tt['12345']) +del tt['12345'] + +# this is going to raise an error +print(tt['12345']) +``` + + +If you wish to support the Python `start:stop:step` [slice +notation](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/), +you simply need to write your `__getitem__` and `__setitem__` methods so that they +can detect `slice` objects: + + +``` +class TwoTimes(object): + + def __init__(self, max): + self.__max = max + + def __getitem__(self, key): + if isinstance(key, slice): + start = key.start or 0 + stop = key.stop or self.__max + step = key.step or 1 + else: + start = key + stop = key + 1 + step = 1 + + return [i * 2 for i in range(start, stop, step)] +``` + +``` +tt = TwoTimes(10) + +print(tt[5]) +print(tt[3:7]) +print(tt[::2]) +``` + + + + +> It is possible to sub-class the built-in `list` and `dict` classes if you +> wish to extend their functionality in some way. However, if you are writing +> a class that should mimic the one of the `list` or `dict` classes, but work +> in a different way internally (e.g. a `dict`-like object which uses a +> different hashing algorithm), the `Sequence` and `MutableMapping` classes +> are [a better choice](https://stackoverflow.com/a/7148602) - you can find +> them in the +> [`collections.abc`](https://docs.python.org/3.5/library/collections.abc.html) +> module. + + +## The call operator `()` + + +Remember how everything in Python is an object, even functions? When you call +a function, a method called `__call__` is called on the function object. We can +implement the `__call__` method on our own class, which will allow us to "call" +objects as if they are functions. + + +For example, the `TimedFunction` class allows us to calculate the execution +time of any function: + + +## The dot operator `.` + + +Python allows us to override the `.` (dot) operator which is used to access +the attributes and methods of an object. attributes and methods of an +object. This is a fairly niche feature, and you need to be careful that you +don't unintentionally introduce recursive attribute lookups into your code. + + + + + +``` +class Vector(object): + def __init__(self, xyz): + self.__xyz = list(xyz) + + def __str__(self): + return 'Vector({})'.format(', '.join(self.__xyz)) + + def __getattr__(self, key): + key = ['xyz'.index(c) for c in key] + return [self.__xyz[c] for c in key] + + def __setattr__(self, key, value): + pass # key = ['xyz'.index(c) for c in key] +``` + + +``` +v = Vector((4, 5, 6)) + +print('xyz: ', v.xyz) +print('yz: ', v.yz) +print('zxy: ', v.xzy) +print('y: ', v.y) +``` + + +## Other special methods -- GitLab