From 1e6a4277deaac1ec798ea44d58ba5e54744f42b8 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauldmccarthy@gmail.com> Date: Mon, 12 Feb 2018 21:50:02 +0000 Subject: [PATCH] operator overloading practical finished --- advanced_topics/operator_overloading.ipynb | 269 ++++++++++++++++----- advanced_topics/operator_overloading.md | 241 +++++++++++++----- 2 files changed, 387 insertions(+), 123 deletions(-) diff --git a/advanced_topics/operator_overloading.ipynb b/advanced_topics/operator_overloading.ipynb index 45a7207..1b9ab7f 100644 --- a/advanced_topics/operator_overloading.ipynb +++ b/advanced_topics/operator_overloading.ipynb @@ -17,6 +17,22 @@ "overloading is __very__ easy to do in Python.\n", "\n", "\n", + "This practical gives a brief overview of the operators which you may be most\n", + "interested in implementing. However, there are many operators (and other\n", + "special methods) which you can support in your own classes - the [official\n", + "documentation](https://docs.python.org/3.5/reference/datamodel.html#basic-customization)\n", + "is the best reference if you are interested in learning more.\n", + "\n", + "\n", + "* [Overview](#overview)\n", + "* [Arithmetic operators](#arithmetic-operators)\n", + "* [Equality and comparison operators](#equality-and-comparison-operators)\n", + "* [The indexing operator `[]`](#the-indexing-operator)\n", + "* [The call operator `()`](#the-call-operator)\n", + "* [The dot operator `.`](#the-dot-operator)\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"overview\"></a>\n", "## Overview\n", "\n", "\n", @@ -66,6 +82,7 @@ "to do is implement a method called `__add__`.\n", "\n", "\n", + "<a class=\"anchor\" id=\"arithmetic-operators\"></a>\n", "## Arithmetic operators\n", "\n", "\n", @@ -78,13 +95,13 @@ "metadata": {}, "outputs": [], "source": [ - "class Vector(object):\n", + "class Vector2D(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)" + " return 'Vector2D({}, {})'.format(self.x, self.y)" ] }, { @@ -92,7 +109,7 @@ "metadata": {}, "source": [ "> Note that we have implemented the special `__str__` method, which allows our\n", - "> `Vector` instances to be converted into strings.\n", + "> `Vector2D` 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:" @@ -104,8 +121,8 @@ "metadata": {}, "outputs": [], "source": [ - "v1 = Vector(2, 3)\n", - "v2 = Vector(4, 5)\n", + "v1 = Vector2D(2, 3)\n", + "v2 = Vector2D(4, 5)\n", "print(v1 + v2)" ] }, @@ -123,24 +140,24 @@ "metadata": {}, "outputs": [], "source": [ - "class Vector(object):\n", + "class Vector2D(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", + " return 'Vector2D({}, {})'.format(self.x, self.y)\n", "\n", " def __add__(self, other):\n", - " return Vector(self.x + other.x,\n", - " self.y + other.y)" + " return Vector2D(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:" + "And now we can use `+` on `Vector2D` objects - it's that easy:" ] }, { @@ -149,8 +166,8 @@ "metadata": {}, "outputs": [], "source": [ - "v1 = Vector(2, 3)\n", - "v2 = Vector(4, 5)\n", + "v1 = Vector2D(2, 3)\n", + "v2 = Vector2D(4, 5)\n", "print('{} + {} = {}'.format(v1, v2, v1 + v2))" ] }, @@ -158,10 +175,10 @@ "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:" + "Our `__add__` method creates and returns a new `Vector2D` which contains the\n", + "sum of the `x` and `y` components of the `Vector2D` on which it is called, and\n", + "the `Vector2D` which is passed in. We could also make the `__add__` method\n", + "work with scalars, by extending its definition a bit:" ] }, { @@ -170,27 +187,27 @@ "metadata": {}, "outputs": [], "source": [ - "class Vector(object):\n", + "class Vector2D(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", + " if isinstance(other, Vector2D):\n", + " return Vector2D(self.x + other.x,\n", + " self.y + other.y)\n", " else:\n", - " return Vector(self.x + other, self.y + other)\n", + " return Vector2D(self.x + other, self.y + other)\n", "\n", " def __str__(self):\n", - " return 'Vector({}, {})'.format(self.x, self.y)" + " return 'Vector2D({}, {})'.format(self.x, self.y)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "So now we can add both `Vectors` and scalars numbers together:" + "So now we can add both `Vector2D` instances and scalars numbers together:" ] }, { @@ -199,8 +216,8 @@ "metadata": {}, "outputs": [], "source": [ - "v1 = Vector(2, 3)\n", - "v2 = Vector(4, 5)\n", + "v1 = Vector2D(2, 3)\n", + "v2 = Vector2D(4, 5)\n", "n = 6\n", "\n", "print('{} + {} = {}'.format(v1, v2, v1 + v2))\n", @@ -211,7 +228,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Other nuemric and logical operators can be supported by implementing the\n", + "Other numeric and logical operators can be supported by implementing the\n", "appropriate method, for example:\n", "\n", "- Multiplication (`*`): `__mul__`\n", @@ -227,6 +244,7 @@ "support.\n", "\n", "\n", + "<a class=\"anchor\" id=\"equality-and-comparison-operators\"></a>\n", "## Equality and comparison operators\n", "\n", "\n", @@ -266,7 +284,10 @@ "source": [ "import functools\n", "\n", + "# Don't worry about this statement\n", + "# just yet - it is explained below\n", "@functools.total_ordering\n", + "\n", "class Label(object):\n", " def __init__(self, label, name, colour):\n", " self.label = label\n", @@ -280,9 +301,11 @@ " def __repr__(self):\n", " return str(self)\n", "\n", + " # implement Label == Label\n", " def __eq__(self, other):\n", " return self.label == other.label\n", "\n", + " # implement Label < Label\n", " def __lt__(self, other):\n", " return self.label < other.label" ] @@ -318,17 +341,23 @@ "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", + "The\n", + "[`@functools.total_ordering`](https://docs.python.org/3.5/library/functools.html#functools.total_ordering)\n", + "is a convenience\n", + "[decorator](https://docs.python.org/3.5/glossary.html#term-decorator) which,\n", + "given a class that implements equality and a single comparison function\n", + "(`__lt__` in the above code), will \"fill in\" the remainder of the comparison\n", + "operators. If you need very specific or complicated behaviour, then you can\n", + "provide methods for _all_ of the comparison operators, e.g. `__gt__` for `>`,\n", + "`__ge__` for `>=`, etc.).\n", + "\n", + "\n", + "> Decorators are introduced in another practical.\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", + "simply use the `@functools.total_ordering` decorator, and provide `__eq__`,\n", + "and just one of `__lt__`, `__le__`, `__gt__` or `__ge__`.\n", "\n", "\n", "Refer to the [official\n", @@ -338,10 +367,11 @@ "\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", + "> comparing two items. This has been superseded by the rich comparison\n", + "> operators introduced here, and is no longer supported in Python 3.\n", "\n", "\n", + "<a class=\"anchor\" id=\"the-indexing-operator\"></a>\n", "## The indexing operator `[]`\n", "\n", "\n", @@ -352,8 +382,9 @@ "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", + "indexed by sequential integers (like a `list`) or by\n", + "[hashable](https://docs.python.org/3.5/glossary.html#term-hashable) values\n", + "(like a `dict`):\n", "\n", "\n", "- __Retrieval__ is performed by the `__getitem__` method\n", @@ -362,9 +393,11 @@ "\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", + "requirement for them to actually provide any form of data storage or\n", + "retrieval. However if you don't, you will probably confuse users of your code\n", + "who are used to how the `list` and `dict` types work. Whenever you deviate\n", + "from conventional behaviour, make sure you explain it well in your\n", + "documentation!\n", "\n", "\n", "The following contrived example demonstrates all three behaviours:" @@ -384,7 +417,7 @@ "\n", " def __getitem__(self, key):\n", " if key in self.__deleted:\n", - " raise KeyError('{} has been deleted!')\n", + " raise KeyError('{} has been deleted!'.format(key))\n", " elif key in self.__assigned:\n", " return self.__assigned[key]\n", " else:\n", @@ -420,8 +453,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "For some unknown reason, the `TwoTimes` class allows us to override the value\n", - "for a specific key:" + "The `TwoTimes` class allows us to override the value for a specific key:" ] }, { @@ -460,8 +492,8 @@ "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", + "notation](https://docs.python.org/3.5/library/functions.html#slice), you\n", + "simply need to write your `__getitem__` and `__setitem__` methods so that they\n", "can detect `slice` objects:" ] }, @@ -489,6 +521,13 @@ " return [i * 2 for i in range(start, stop, step)]" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can \"slice\" a `TwoTimes` instance:" + ] + }, { "cell_type": "code", "execution_count": null, @@ -517,6 +556,7 @@ "> module.\n", "\n", "\n", + "<a class=\"anchor\" id=\"the-call-operator\"></a>\n", "## The call operator `()`\n", "\n", "\n", @@ -527,16 +567,96 @@ "\n", "\n", "For example, the `TimedFunction` class allows us to calculate the execution\n", - "time of any function:\n", + "time of any function:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "class TimedFunction(object):\n", "\n", + " def __init__(self, func):\n", + " self.func = func\n", "\n", + " def __call__(self, *args, **kwargs):\n", + " print('Timing {}...'.format(self.func.__name__))\n", + "\n", + " start = time.time()\n", + " retval = self.func(*args, **kwargs)\n", + " end = time.time()\n", + "\n", + " print('Elapsed time: {:0.2f} seconds'.format(end - start))\n", + " return retval" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see how the `TimedFunction` behaves:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import numpy.linalg as npla\n", + "\n", + "def inverse(data):\n", + " return npla.inv(data)\n", + "\n", + "tf = TimedFunction(inverse)\n", + "data = np.random.random((5000, 5000))\n", + "\n", + "# Wait a few seconds after\n", + "# running this code block!\n", + "inv = tf(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> The `TimedFunction` class is conceptually very similar to a\n", + "> [decorator](https://docs.python.org/3.5/glossary.html#term-decorator) -\n", + "> decorators are covered in another practical.\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"the-dot-operator\"></a>\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." + "the attributes and methods of an object. This is very powerful, but is also\n", + "quite a niche feature, and it is easy to trip yourself up, so if you wish to\n", + "use this in your own project, make sure that you carefully read (and\n", + "understand) [the\n", + "documentation](https://docs.python.org/3.5/reference/datamodel.html#customizing-attribute-access),\n", + "and test your code comprehensively!\n", + "\n", + "\n", + "For this example, we need a little background information. OpenGL includes\n", + "the native data types `vec2`, `vec3`, and `vec4`, which can be used to\n", + "represent 2, 3, or 4 component vectors respectively. These data types have a\n", + "neat feature called [_swizzling_][glslref], which allows you to access any\n", + "component (`x`,`y`, `z`, `w` for vectors, or `r`, `g`, `b`, `a` for colours)\n", + "in any order, with a syntax similar to attribute access in Python.\n", + "\n", + "\n", + "[glslref]: https://www.khronos.org/opengl/wiki/Data_Type_(GLSL)#Swizzling\n", + "\n", + "\n", + "So here is an example which implements this swizzle-style attribute access on\n", + "a class called `Vector`, in which we have customised the behaviour of the `.`\n", + "operator:" ] }, { @@ -550,35 +670,58 @@ " self.__xyz = list(xyz)\n", "\n", " def __str__(self):\n", - " return 'Vector({})'.format(', '.join(self.__xyz))\n", + " return 'Vector({})'.format(self.__xyz)\n", "\n", " def __getattr__(self, key):\n", + "\n", + " # Swizzling behaviour only occurs when\n", + " # the attribute name is entirely comprised\n", + " # of 'x', 'y', and 'z'.\n", + " if not all([c in 'xyz' for c in key]):\n", + " raise AttributeError(key)\n", + "\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]" + "\n", + " # Restrict swizzling behaviour as above\n", + " if not all([c in 'xyz' for c in key]):\n", + " return super().__setattr__(key, value)\n", + "\n", + " if len(key) == 1:\n", + " value = (value,)\n", + "\n", + " idxs = ['xyz'.index(c) for c in key]\n", + "\n", + " for i, v in sorted(zip(idxs, value)):\n", + " self.__xyz[i] = v\n" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "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)" + "And here it is in action:" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "## Other special methods" + "v = Vector((1, 2, 3))\n", + "\n", + "print('v: ', v)\n", + "print('xyz: ', v.xyz)\n", + "print('yz: ', v.zy)\n", + "print('xx: ', v.xx)\n", + "\n", + "v.xz = 10, 30\n", + "print(v)\n", + "v.y = 20\n", + "print(v)" ] } ], diff --git a/advanced_topics/operator_overloading.md b/advanced_topics/operator_overloading.md index 3e78721..142368f 100644 --- a/advanced_topics/operator_overloading.md +++ b/advanced_topics/operator_overloading.md @@ -11,6 +11,22 @@ process of customising the behaviour of _operators_ (e.g. `+`, `*`, `/` and overloading is __very__ easy to do in Python. +This practical gives a brief overview of the operators which you may be most +interested in implementing. However, there are many operators (and other +special methods) which you can support in your own classes - the [official +documentation](https://docs.python.org/3.5/reference/datamodel.html#basic-customization) +is the best reference if you are interested in learning more. + + +* [Overview](#overview) +* [Arithmetic operators](#arithmetic-operators) +* [Equality and comparison operators](#equality-and-comparison-operators) +* [The indexing operator `[]`](#the-indexing-operator) +* [The call operator `()`](#the-call-operator) +* [The dot operator `.`](#the-dot-operator) + + +<a class="anchor" id="overview"></a> ## Overview @@ -44,6 +60,7 @@ So it is very easy to use the `+` operator with our own classes - all we have to do is implement a method called `__add__`. +<a class="anchor" id="arithmetic-operators"></a> ## Arithmetic operators @@ -51,26 +68,26 @@ Let's play with an example - a class which represents a 2D vector: ``` -class Vector(object): +class Vector2D(object): def __init__(self, x, y): self.x = x self.y = y def __str__(self): - return 'Vector({}, {})'.format(self.x, self.y) + return 'Vector2D({}, {})'.format(self.x, self.y) ``` > Note that we have implemented the special `__str__` method, which allows our -> `Vector` instances to be converted into strings. +> `Vector2D` 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) +v1 = Vector2D(2, 3) +v2 = Vector2D(4, 5) print(v1 + v2) ``` @@ -80,60 +97,60 @@ called `__add__`: ``` -class Vector(object): +class Vector2D(object): def __init__(self, x, y): self.x = x self.y = y def __str__(self): - return 'Vector({}, {})'.format(self.x, self.y) + return 'Vector2D({}, {})'.format(self.x, self.y) def __add__(self, other): - return Vector(self.x + other.x, - self.y + other.y) + return Vector2D(self.x + other.x, + self.y + other.y) ``` -And now we can use `+` on `Vector` objects - it's that easy: +And now we can use `+` on `Vector2D` objects - it's that easy: ``` -v1 = Vector(2, 3) -v2 = Vector(4, 5) +v1 = Vector2D(2, 3) +v2 = Vector2D(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: +Our `__add__` method creates and returns a new `Vector2D` which contains the +sum of the `x` and `y` components of the `Vector2D` on which it is called, and +the `Vector2D` which is passed in. We could also make the `__add__` method +work with scalars, by extending its definition a bit: ``` -class Vector(object): +class Vector2D(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) + if isinstance(other, Vector2D): + return Vector2D(self.x + other.x, + self.y + other.y) else: - return Vector(self.x + other, self.y + other) + return Vector2D(self.x + other, self.y + other) def __str__(self): - return 'Vector({}, {})'.format(self.x, self.y) + return 'Vector2D({}, {})'.format(self.x, self.y) ``` -So now we can add both `Vectors` and scalars numbers together: +So now we can add both `Vector2D` instances and scalars numbers together: ``` -v1 = Vector(2, 3) -v2 = Vector(4, 5) +v1 = Vector2D(2, 3) +v2 = Vector2D(4, 5) n = 6 print('{} + {} = {}'.format(v1, v2, v1 + v2)) @@ -141,7 +158,7 @@ print('{} + {} = {}'.format(v1, n, v1 + n)) ``` -Other nuemric and logical operators can be supported by implementing the +Other numeric and logical operators can be supported by implementing the appropriate method, for example: - Multiplication (`*`): `__mul__` @@ -157,6 +174,7 @@ for a full list of the arithmetic and logical operators that your classes can support. +<a class="anchor" id="equality-and-comparison-operators"></a> ## Equality and comparison operators @@ -183,7 +201,10 @@ compared using the standard comparison operators: ``` import functools +# Don't worry about this statement +# just yet - it is explained below @functools.total_ordering + class Label(object): def __init__(self, label, name, colour): self.label = label @@ -197,13 +218,16 @@ class Label(object): def __repr__(self): return str(self) + # implement Label == Label def __eq__(self, other): return self.label == other.label + # implement Label < 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. @@ -223,17 +247,23 @@ 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.). +The +[`@functools.total_ordering`](https://docs.python.org/3.5/library/functools.html#functools.total_ordering) +is a convenience +[decorator](https://docs.python.org/3.5/glossary.html#term-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.). + + +> Decorators are introduced in another practical. 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__`. +simply use the `@functools.total_ordering` decorator, and provide `__eq__`, +and just one of `__lt__`, `__le__`, `__gt__` or `__ge__`. Refer to the [official @@ -243,10 +273,11 @@ 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. +> comparing two items. This has been superseded by the rich comparison +> operators introduced here, and is no longer supported in Python 3. +<a class="anchor" id="the-indexing-operator"></a> ## The indexing operator `[]` @@ -257,8 +288,9 @@ 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`): +indexed by sequential integers (like a `list`) or by +[hashable](https://docs.python.org/3.5/glossary.html#term-hashable) values +(like a `dict`): - __Retrieval__ is performed by the `__getitem__` method @@ -267,9 +299,11 @@ indexed by sequential integers (like a `list`) or by hashable values (like a 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! +requirement for them to actually provide any form of data storage or +retrieval. However if you don't, you will probably confuse users of your code +who are used to how the `list` and `dict` types work. Whenever you deviate +from conventional behaviour, make sure you explain it well in your +documentation! The following contrived example demonstrates all three behaviours: @@ -284,7 +318,7 @@ class TwoTimes(object): def __getitem__(self, key): if key in self.__deleted: - raise KeyError('{} has been deleted!') + raise KeyError('{} has been deleted!'.format(key)) elif key in self.__assigned: return self.__assigned[key] else: @@ -309,8 +343,7 @@ print('TwoTimes[{}] = {}'.format('abc', tt['abc'])) ``` -For some unknown reason, the `TwoTimes` class allows us to override the value -for a specific key: +The `TwoTimes` class allows us to override the value for a specific key: ``` @@ -333,8 +366,8 @@ 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 +notation](https://docs.python.org/3.5/library/functions.html#slice), you +simply need to write your `__getitem__` and `__setitem__` methods so that they can detect `slice` objects: @@ -357,6 +390,10 @@ class TwoTimes(object): return [i * 2 for i in range(start, stop, step)] ``` + +Now we can "slice" a `TwoTimes` instance: + + ``` tt = TwoTimes(10) @@ -366,8 +403,6 @@ 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 @@ -379,6 +414,7 @@ print(tt[::2]) > module. +<a class="anchor" id="the-call-operator"></a> ## The call operator `()` @@ -392,16 +428,77 @@ For example, the `TimedFunction` class allows us to calculate the execution time of any function: +``` +import time + +class TimedFunction(object): + + def __init__(self, func): + self.func = func + + def __call__(self, *args, **kwargs): + print('Timing {}...'.format(self.func.__name__)) + + start = time.time() + retval = self.func(*args, **kwargs) + end = time.time() + + print('Elapsed time: {:0.2f} seconds'.format(end - start)) + return retval +``` + + +Let's see how the `TimedFunction` behaves: + + +``` +import numpy as np +import numpy.linalg as npla + +def inverse(data): + return npla.inv(data) + +tf = TimedFunction(inverse) +data = np.random.random((5000, 5000)) + +# Wait a few seconds after +# running this code block! +inv = tf(data) +``` + + +> The `TimedFunction` class is conceptually very similar to a +> [decorator](https://docs.python.org/3.5/glossary.html#term-decorator) - +> decorators are covered in another practical. + + +<a class="anchor" id="the-dot-operator"></a> ## 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. +the attributes and methods of an object. This is very powerful, but is also +quite a niche feature, and it is easy to trip yourself up, so if you wish to +use this in your own project, make sure that you carefully read (and +understand) [the +documentation](https://docs.python.org/3.5/reference/datamodel.html#customizing-attribute-access), +and test your code comprehensively! + + +For this example, we need a little background information. OpenGL includes +the native data types `vec2`, `vec3`, and `vec4`, which can be used to +represent 2, 3, or 4 component vectors respectively. These data types have a +neat feature called [_swizzling_][glslref], which allows you to access any +component (`x`,`y`, `z`, `w` for vectors, or `r`, `g`, `b`, `a` for colours) +in any order, with a syntax similar to attribute access in Python. + +[glslref]: https://www.khronos.org/opengl/wiki/Data_Type_(GLSL)#Swizzling +So here is an example which implements this swizzle-style attribute access on +a class called `Vector`, in which we have customised the behaviour of the `.` +operator: ``` @@ -410,25 +507,49 @@ class Vector(object): self.__xyz = list(xyz) def __str__(self): - return 'Vector({})'.format(', '.join(self.__xyz)) + return 'Vector({})'.format(self.__xyz) def __getattr__(self, key): + + # Swizzling behaviour only occurs when + # the attribute name is entirely comprised + # of 'x', 'y', and 'z'. + if not all([c in 'xyz' for c in key]): + raise AttributeError(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] -``` + # Restrict swizzling behaviour as above + if not all([c in 'xyz' for c in key]): + return super().__setattr__(key, value) + + if len(key) == 1: + value = (value,) + + idxs = ['xyz'.index(c) for c in key] + + for i, v in sorted(zip(idxs, value)): + self.__xyz[i] = v ``` -v = Vector((4, 5, 6)) -print('xyz: ', v.xyz) -print('yz: ', v.yz) -print('zxy: ', v.xzy) -print('y: ', v.y) + +And here it is in action: + + ``` +v = Vector((1, 2, 3)) +print('v: ', v) +print('xyz: ', v.xyz) +print('yz: ', v.zy) +print('xx: ', v.xx) -## Other special methods +v.xz = 10, 30 +print(v) +v.y = 20 +print(v) +``` -- GitLab