Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
P
pytreat-practicals-2020
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Container Registry
Model registry
Operate
Environments
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Tom Nichols
pytreat-practicals-2020
Commits
9f3718f0
Commit
9f3718f0
authored
7 years ago
by
Paul McCarthy
Browse files
Options
Downloads
Patches
Plain Diff
New practical on operator overloading. Needs work
parent
61687a57
No related branches found
No related tags found
No related merge requests found
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
advanced_topics/operator_overloading.ipynb
+588
-0
588 additions, 0 deletions
advanced_topics/operator_overloading.ipynb
advanced_topics/operator_overloading.md
+434
-0
434 additions, 0 deletions
advanced_topics/operator_overloading.md
with
1022 additions
and
0 deletions
advanced_topics/operator_overloading.ipynb
0 → 100644
+
588
−
0
View file @
9f3718f0
{
"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
}
%% Cell type:markdown id: tags:
# 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:
%% Cell type:code id: tags:
```
a = 5
b = 10
r = a + b
print(r)
```
%% Cell type:markdown id: tags:
What actually goes on behind the scenes is this:
%% Cell type:code id: tags:
```
r = a.__add__(b)
print(r)
```
%% Cell type:markdown id: tags:
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:
%% Cell type:code id: tags:
```
class Vector(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return 'Vector({}, {})'.format(self.x, self.y)
```
%% Cell type:markdown id: tags:
> 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:
%% Cell type:code id: tags:
```
v1 = Vector(2, 3)
v2 = Vector(4, 5)
print(v1 + v2)
```
%% Cell type:markdown id: tags:
But all we need to do to support the
`+`
operator is to implement a method
called
`__add__`
:
%% Cell type:code id: tags:
```
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)
```
%% Cell type:markdown id: tags:
And now we can use
`+`
on
`Vector`
objects - it's that easy:
%% Cell type:code id: tags:
```
v1 = Vector(2, 3)
v2 = Vector(4, 5)
print('{} + {} = {}'.format(v1, v2, v1 + v2))
```
%% Cell type:markdown id: tags:
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:
%% Cell type:code id: tags:
```
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)
```
%% Cell type:markdown id: tags:
So now we can add both
`Vectors`
and scalars numbers together:
%% Cell type:code id: tags:
```
v1 = Vector(2, 3)
v2 = Vector(4, 5)
n = 6
print('{} + {} = {}'.format(v1, v2, v1 + v2))
print('{} + {} = {}'.format(v1, n, v1 + n))
```
%% Cell type:markdown id: tags:
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:
%% Cell type:code id: tags:
```
class Label(object):
def __init__(self, label, name, colour):
self.label = label
self.name = name
self.colour = colour
```
%% Cell type:markdown id: tags:
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:
%% Cell type:code id: tags:
```
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
```
%% Cell type:markdown id: tags:
> 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:
%% Cell type:code id: tags:
```
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)))
```
%% Cell type:markdown id: tags:
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:
%% Cell type:code id: tags:
```
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)
```
%% Cell type:markdown id: tags:
Guess what happens whenever we index a
`TwoTimes`
object:
%% Cell type:code id: tags:
```
tt = TwoTimes()
print('TwoTimes[{}] = {}'.format(2, tt[2]))
print('TwoTimes[{}] = {}'.format(6, tt[6]))
print('TwoTimes[{}] = {}'.format('abc', tt['abc']))
```
%% Cell type:markdown id: tags:
For some unknown reason, the
`TwoTimes`
class allows us to override the value
for a specific key:
%% Cell type:code id: tags:
```
print(tt[4])
tt[4] = 'this is not 4 * 4'
print(tt[4])
```
%% Cell type:markdown id: tags:
And we can also "delete" keys:
%% Cell type:code id: tags:
```
print(tt['12345'])
del tt['12345']
# this is going to raise an error
print(tt['12345'])
```
%% Cell type:markdown id: tags:
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:
%% Cell type:code id: tags:
```
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)]
```
%% Cell type:code id: tags:
```
tt = TwoTimes(10)
print(tt[5])
print(tt[3:7])
print(tt[::2])
```
%% Cell type:markdown id: tags:
> 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.
%% Cell type:code id: tags:
```
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]
```
%% Cell type:code id: tags:
```
v = Vector((4, 5, 6))
print('xyz: ', v.xyz)
print('yz: ', v.yz)
print('zxy: ', v.xzy)
print('y: ', v.y)
```
%% Cell type:markdown id: tags:
## Other special methods
This diff is collapsed.
Click to expand it.
advanced_topics/operator_overloading.md
0 → 100644
+
434
−
0
View file @
9f3718f0
# 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
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment