Created
May 9, 2013 19:21
-
-
Save embray/5549832 to your computer and use it in GitHub Desktop.
Notebook for my PyLunch talk on classes. This contains some content that we didn't get to yet in PyLunch, as well as some new content that was added after this was first presented.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"metadata": { | |
"name": "Classes" | |
}, | |
"nbformat": 3, | |
"nbformat_minor": 0, | |
"worksheets": [ | |
{ | |
"cells": [ | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"from __future__ import print_function\n", | |
"from pprint import pprint\n", | |
"import numpy as np" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 1 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"class Circle(object):\n", | |
" \"\"\"Represents a circle with a given radius.\"\"\"\n", | |
" \n", | |
" def __init__(self, radius):\n", | |
" self.radius = radius\n", | |
" \n", | |
" def circumference(self):\n", | |
" return 2 * np.pi * self.radius\n", | |
" \n", | |
" def area(self):\n", | |
" return np.pi * self.radius ** 2" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 2 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Classes themselves are first-class objects in Python.\n", | |
"They have attributes just like any other object that can be accessed via the `.` operator.\n", | |
"For example:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"Circle.__name__" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 3, | |
"text": [ | |
"'Circle'" | |
] | |
} | |
], | |
"prompt_number": 3 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"But the Circle *class* just represents an abstract _class_ of objects (hence the term \"class\").\n", | |
"The Circle class does not have a definite radius" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"Circle.radius" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"ename": "AttributeError", | |
"evalue": "type object 'Circle' has no attribute 'radius'", | |
"output_type": "pyerr", | |
"traceback": [ | |
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", | |
"\u001b[1;32m<ipython-input-4-f0a2aafaa67c>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mCircle\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mradius\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", | |
"\u001b[1;31mAttributeError\u001b[0m: type object 'Circle' has no attribute 'radius'" | |
] | |
} | |
], | |
"prompt_number": 4 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"That's because we designed our Circle class so that we specify a radius when we create an *instance* of\n", | |
"the Circle class. All other properties of the circle (circumference, area) are are given by _methods_\n", | |
"of the class that depend on and make use of a specific radius\n", | |
"\n", | |
"To create an instance of Circle we actually _call_ the Circle class like it's a function, passing in the\n", | |
"radius as the first argument, and assign the result to a variable:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle = Circle(radius=3)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 5 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# my_circle is an object of type Circle\n", | |
"my_circle" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 6, | |
"text": [ | |
"<__main__.Circle at 0x3b098b0>" | |
] | |
} | |
], | |
"prompt_number": 6 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"We can access our circle's radius attribute using the '.' operator as usual (now this *does* work\n", | |
"because my_circle is a *specific* instance of a `Circle` with a definite radius:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.radius" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 7, | |
"text": [ | |
"3" | |
] | |
} | |
], | |
"prompt_number": 7 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"You can also call the `area()` and `circumference()` *methods* on the object. \"Method\" is just a special term for a function that's attached to an object:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.area()" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 8, | |
"text": [ | |
"28.274333882308138" | |
] | |
} | |
], | |
"prompt_number": 8 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.circumference()" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 9, | |
"text": [ | |
"18.84955592153876" | |
] | |
} | |
], | |
"prompt_number": 9 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"This act of calling _methods_ of an object should seem familiar. This is the exact same mechanism that you're using when you call things like `.lower()` on a string. `lower()` is a _method_ of strings:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"mystring = 'HELLO'\n", | |
"mystring.lower()" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 10, | |
"text": [ | |
"'hello'" | |
] | |
} | |
], | |
"prompt_number": 10 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"This notebook won't go into great detail on the meaning of the class definition, but I want to point out something we showed in today's PyLunch session about the `self` argument that is the first argument to *every* class method. When you call something like `my_circle.area()` what happens is that this takes the `Circle` *instance* in `my_circle` and *implicitly* passes it to `Circle.area()` as the first argument (named `self` by convention).\n", | |
"\n", | |
"In other words this:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.area()" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 11, | |
"text": [ | |
"28.274333882308138" | |
] | |
} | |
], | |
"prompt_number": 11 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"is exactly identical to this:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"Circle.area(my_circle)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 12, | |
"text": [ | |
"28.274333882308138" | |
] | |
} | |
], | |
"prompt_number": 12 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"In fact, all objects in Python have a `.__class__` attribute that stores a reference to the original class that we used when we created the object (for example when we called `my_circle = Circle(3)` one of the things that happens implicitly is Python assigns `my_circle.__class__ = Circle`).\n", | |
"\n", | |
"So in fact what really happens under the hood when you call `my_circle.area()` is this:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.__class__.area(my_circle)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 13, | |
"text": [ | |
"28.274333882308138" | |
] | |
} | |
], | |
"prompt_number": 13 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"`my_circle.area()` is essentially just \"syntactic sugar\" for the above mess. It should be clear which usage is preferable ;)\n", | |
"\n", | |
"Note: As we showed in the PyLunch this is equally true for builtin types in Python like `str` (strings):" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# What actually happens when you call \"HELLO\".lower():\n", | |
"\"HELLO\".__class__.lower(\"HELLO\")" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 14, | |
"text": [ | |
"'hello'" | |
] | |
} | |
], | |
"prompt_number": 14 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"We can also update the radius of the circle and compute new results. The fact that this is possible is the default behavior, though it *is* possible to design a class such that its instances are *immutable* (think tuples for example), so that you can't just change a circle's radius--if you want a circle with a different radius you have to create a new one. But for now we'll just let *our* circles be mutable:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.radius = 4\n", | |
"my_circle.radius" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 15, | |
"text": [ | |
"4" | |
] | |
} | |
], | |
"prompt_number": 15 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.area()" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 16, | |
"text": [ | |
"50.26548245743669" | |
] | |
} | |
], | |
"prompt_number": 16 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.circumference()" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 17, | |
"text": [ | |
"25.132741228718345" | |
] | |
} | |
], | |
"prompt_number": 17 | |
}, | |
{ | |
"cell_type": "heading", | |
"level": 2, | |
"metadata": {}, | |
"source": [ | |
"String representations" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"We've seen already that objects we create from our own classes have a sort of default string representation that isn't terribly illuminating:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 18, | |
"text": [ | |
"<__main__.Circle at 0x3b098b0>" | |
] | |
} | |
], | |
"prompt_number": 18 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"It does tell us that this is a thing called `Circle` (and that it's defined in the `__main__` module--if this were in a `.py` file like, for example `shapes.py` this \"fully qualified\" name of the class would be `shapes.Circle`). The string of hex digits after the \"at\" is the actual memory address of this circle object. If we create a new instance of `Circle` it will have a different address every time (regardless of what we give for its radius)." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle_2 = Circle(4)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 19 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle_2" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 20, | |
"text": [ | |
"<__main__.Circle at 0x3b09b30>" | |
] | |
} | |
], | |
"prompt_number": 20 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Perhaps we'd like to have a slightly more useful representation of our circles for use when exploring them in the terminal, so that we don't have to type `my_circle.radius` every time we want to know something about our circles. We've already seen before that objects *can* have more useful default representations. Take Numpy arrays for example:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_array = np.arange(10)\n", | |
"my_array" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 21, | |
"text": [ | |
"array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])" | |
] | |
} | |
], | |
"prompt_number": 21 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"To get the same sort of behavior we define a special method on our `Circle` class called `__repr__`. It takes only one argument (`self`) and it *must* return a string. That string can be whatever you want your object's representation to be. Let's redefine `Circle` with a `__repr__`:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# A Circle class with a friendler, more informative __repr__\n", | |
"\n", | |
"class Circle(object):\n", | |
" \"\"\"Represents a circle with a given radius.\"\"\"\n", | |
" \n", | |
" def __init__(self, radius):\n", | |
" self.radius = radius\n", | |
" \n", | |
" def __repr__(self):\n", | |
" # NOTE: This an subsequent examples are using \"new style\" string formatting\n", | |
" # available in Python 2.6 and up; read more about it here if you haven't seen\n", | |
" # this before and are curious: http://www.python.org/dev/peps/pep-3101/\n", | |
" # (this is the actual PEP that defined the new formatting; it's actually pretty\n", | |
" # readable, provides lots of examples, and justification for the new style versus\n", | |
" # the old % formatting syntax most of you are familiar with)\n", | |
" return '<Circle(radius={radius})>'.format(radius=self.radius)\n", | |
" \n", | |
" def circumference(self):\n", | |
" return 2 * np.pi * self.radius\n", | |
" \n", | |
" def area(self):\n", | |
" return np.pi * self.radius ** 2" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 22 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Now let's make a circle again with our new `Circle` class definition:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle = Circle(3)\n", | |
"my_circle" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 23, | |
"text": [ | |
"<Circle(radius=3)>" | |
] | |
} | |
], | |
"prompt_number": 23 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Now we know whenever a `Circle` object is displayed something a little more useful about it. We could go even further than this and display its area and circumference as well since these are fast to compute (though I wouldn't recommend putting anything complicated/time-consuming in `__repr__`):" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# A Circle class with an even *more* informative __repr__\n", | |
"\n", | |
"class Circle(object):\n", | |
" \"\"\"Represents a circle with a given radius.\"\"\"\n", | |
" \n", | |
" def __init__(self, radius):\n", | |
" self.radius = radius\n", | |
" \n", | |
" def __repr__(self):\n", | |
" return '<Circle(radius={radius}, circumference={circumference}, area={area})>'.format(\n", | |
" radius=self.radius, circumference=self.circumference(), area=self.area())\n", | |
" \n", | |
" def circumference(self):\n", | |
" return 2 * np.pi * self.radius\n", | |
" \n", | |
" def area(self):\n", | |
" return np.pi * self.radius ** 2" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 24 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# We have to make a new circle instance again because our *previous* my_circle is still using the old definition of the Circle class\n", | |
"my_circle = Circle(3)\n", | |
"my_circle" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 25, | |
"text": [ | |
"<Circle(radius=3, circumference=18.8495559215, area=28.2743338823)>" | |
] | |
} | |
], | |
"prompt_number": 25 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"One cool feature *specific* to IPython Notebook is that it recognizes other kinds of `__repr__` like methods on objects. For example if you define a method on your class called `_repr_html_`, instead of displaying the string returned by `__repr__` it will insert some HTML representing your object into the notebook. For example we could make a `_repr_html_` for circles like so:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"class Circle(object):\n", | |
" \"\"\"Represents a circle with a given radius.\"\"\"\n", | |
" \n", | |
" def __init__(self, radius):\n", | |
" self.radius = radius\n", | |
" \n", | |
" def __repr__(self):\n", | |
" return '<Circle(radius={radius}, circumference={circumference}, area={area})>'.format(\n", | |
" radius=self.radius, circumference=self.circumference(), area=self.area())\n", | |
" \n", | |
" def _repr_html_(self):\n", | |
" return ('<div style=\"width: {scale}px; height: {scale}px; '\n", | |
" 'border: 1px solid black; border-radius: {scale}px; '\n", | |
" 'text-align: center; line-height: {scale}px;\">$ r={radius} $</div>'.format(\n", | |
" scale=self.radius * 100, radius=self.radius))\n", | |
" \n", | |
" def circumference(self):\n", | |
" return 2 * np.pi * self.radius\n", | |
" \n", | |
" def area(self):\n", | |
" return np.pi * self.radius ** 2" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 26 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle = Circle(3)\n", | |
"my_circle" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"html": [ | |
"<div style=\"width: 300px; height: 300px; border: 1px solid black; border-radius: 300px; text-align: center; line-height: 300px;\">$ r=3 $</div>" | |
], | |
"output_type": "pyout", | |
"prompt_number": 27, | |
"text": [ | |
"<Circle(radius=3, circumference=18.8495559215, area=28.2743338823)>" | |
] | |
} | |
], | |
"prompt_number": 27 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"To give a quick preview of subclassing, we could even define a colored circle subclass:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"class ColoredCircle(Circle):\n", | |
" \"\"\"Represents a circle with a given radius and a color.\"\"\"\n", | |
" \n", | |
" def __init__(self, radius, color='white'):\n", | |
" super(ColoredCircle, self).__init__(radius)\n", | |
" self.color = color\n", | |
" \n", | |
" def _repr_html_(self):\n", | |
" return ('<div style=\"width: {scale}px; height: {scale}px; '\n", | |
" 'border: 1px solid black; border-radius: {scale}px; '\n", | |
" 'text-align: center; line-height: {scale}px; '\n", | |
" 'background-color: {color}\">$ r={radius} $</div>'.format(\n", | |
" scale=self.radius * 100, radius=self.radius, color=self.color))" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 28 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_colored_circle = ColoredCircle(1, 'red')\n", | |
"my_colored_circle" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"html": [ | |
"<div style=\"width: 100px; height: 100px; border: 1px solid black; border-radius: 100px; text-align: center; line-height: 100px; background-color: red\">$ r=1 $</div>" | |
], | |
"output_type": "pyout", | |
"prompt_number": 29, | |
"text": [ | |
"<Circle(radius=1, circumference=6.28318530718, area=3.14159265359)>" | |
] | |
} | |
], | |
"prompt_number": 29 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# As an aside, note that in the ColoredCircle definition we did *not* define area() or circumference();\n", | |
"# that's because they're automatically inherited from the _base class_ Circle\n", | |
"my_colored_circle.area()" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 30, | |
"text": [ | |
"3.141592653589793" | |
] | |
} | |
], | |
"prompt_number": 30 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# Astropy Tables make good use of this feature (skip this example if you don't have Astropy)\n", | |
"from astropy.table import Table\n", | |
"table = Table([[1, 2, 3], [4, 5, 6]])\n", | |
"table" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"html": [ | |
"<table><tr><th>col0</th><th>col1</th></tr><tr><td>1</td><td>4</td></tr><tr><td>2</td><td>5</td></tr><tr><td>3</td><td>6</td></tr></table>" | |
], | |
"output_type": "pyout", | |
"prompt_number": 32, | |
"text": [ | |
"<Table rows=3 names=('col0','col1')>\n", | |
"array([(1, 4), (2, 5), (3, 6)], \n", | |
" dtype=[('col0', '<i4'), ('col1', '<i4')])" | |
] | |
} | |
], | |
"prompt_number": 32 | |
}, | |
{ | |
"cell_type": "heading", | |
"level": 2, | |
"metadata": {}, | |
"source": [ | |
"Properties" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"On some level it makes sense that `.area()` and `.circumference()` are methods of the `Circle` class that you have to call--they perform a calculation on the radius, after all. However, we could have just as easily defined our circle in terms of its area, or in terms of its circumference instead of its radius. So on a more abstract level area and circumference are just as intrinsic to a circle as its radius and should not be treated any differently. What if we wanted to define a `Circle` class such that `.area` and `.circumference` are *attributes* of a circle just like `.radius` is, and don't have to be \"called\".\n", | |
"\n", | |
"There are a couple ways to do this. This first is to simply define `self.area` and `self.circumference` attributes in the `__init__` function just like did with `self.radius`, and remove the `circumference()` and `area()` methods:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# Circle class with area and circumference as simple attributes computed in terms of the radius\n", | |
"class Circle(object):\n", | |
" \"\"\"Represents a circle with a given radius.\"\"\"\n", | |
" \n", | |
" def __init__(self, radius):\n", | |
" self.radius = radius\n", | |
" self.circumference = 2 * np.pi * radius\n", | |
" self.area = np.pi * radius ** 2\n", | |
" \n", | |
" def __repr__(self):\n", | |
" return '<Circle(radius={radius}, circumference={circumference}, area={area})>'.format(\n", | |
" radius=self.radius, circumference=self.circumference, area=self.area)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 33 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle = Circle(3)\n", | |
"my_circle" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 34, | |
"text": [ | |
"<Circle(radius=3, circumference=18.8495559215, area=28.2743338823)>" | |
] | |
} | |
], | |
"prompt_number": 34 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.area" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 35, | |
"text": [ | |
"28.274333882308138" | |
] | |
} | |
], | |
"prompt_number": 35 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Great. But this has a big problem--what happens when we redefine the circle's radius?" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.radius = 5\n", | |
"my_circle.radius" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 36, | |
"text": [ | |
"5" | |
] | |
} | |
], | |
"prompt_number": 36 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.area" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 37, | |
"text": [ | |
"28.274333882308138" | |
] | |
} | |
], | |
"prompt_number": 37 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"The area didn't update! That's because the area was only set when we first instantiated the object. Overriding the `.radius` attribute simply defines a new radius, but the class doesn't magically know that the area and circumference must be tied to the radius somehow. Now we could have made `Circle` class so that circles are *immutable* as we discussed earlier. Then it would be impossible to override the radius (or the area, etc.) of a circle object once it's created. And that would be one way around this. But for now let's stick with the example of a mutable circle.\n", | |
"\n", | |
"A better way to deal with this is to use *properties*. Without going into detail about the way this works, for *any* method on a class that takes (`self`) as the *only* argument (that is, you call it without any arguments like we did with `my_circle.area()`) we can make it act like an *attribute* by putting `@property` on top of the method definitions like so:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# A Circle class with .area and .circumference as properties\n", | |
"class Circle(object):\n", | |
" \"\"\"Represents a circle with a given radius.\"\"\"\n", | |
" \n", | |
" def __init__(self, radius):\n", | |
" self.radius = radius\n", | |
" \n", | |
" def __repr__(self):\n", | |
" return '<Circle(radius={radius}, circumference={circumference}, area={area})>'.format(\n", | |
" radius=self.radius, circumference=self.circumference, area=self.area)\n", | |
" \n", | |
" @property\n", | |
" def circumference(self):\n", | |
" return 2 * np.pi * self.radius\n", | |
" \n", | |
" @property\n", | |
" def area(self):\n", | |
" return np.pi * self.radius ** 2" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 38 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle = Circle(3)\n", | |
"my_circle" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 39, | |
"text": [ | |
"<Circle(radius=3, circumference=18.8495559215, area=28.2743338823)>" | |
] | |
} | |
], | |
"prompt_number": 39 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.area" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 40, | |
"text": [ | |
"28.274333882308138" | |
] | |
} | |
], | |
"prompt_number": 40 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# Now we can redefine the circle's radius and still get useful results for .area and .circumference\n", | |
"my_circle.radius = 5\n", | |
"my_circle.area" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 41, | |
"text": [ | |
"78.53981633974483" | |
] | |
} | |
], | |
"prompt_number": 41 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.circumference" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 42, | |
"text": [ | |
"31.41592653589793" | |
] | |
} | |
], | |
"prompt_number": 42 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"The way this works is every time you do `my_circle.area` it *calls* the underlying `area()` function and returns the floating point result. By making it a property it just allows us to omit the empty parentheses (exercise: try doing this yourself and see what happens if you try to call `my_circle.area()` now that `.area` is a property).\n", | |
"\n", | |
"**NOTE:** Do not use properties for just *any* method that takes no arguments thinking that allowing you to drop the `()` is a convenient shortcut. As a conceptual matter, only use it for things that could be considered some kind of \"intrinsic\" property of your object (hence the term *property*). Admittedly this is a bit fuzzy and depends entirely on the type of thing you're trying to represent. But in general don't make things properties unless it seems \"natural\" that it should work that way. In other words, don't use them until you think you need them." | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Some notes on `type`\n", | |
"<img src=\"http://shaunie.me/wp-content/uploads/2013/03/wpid-redpill.png\" alt=\"take the red pill\" />" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"We've already discussed a little bit about a thing called `type()` which, at first glance, looks to be a function that tells you what \"class\" an object has:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"type(2)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 43, | |
"text": [ | |
"int" | |
] | |
} | |
], | |
"prompt_number": 43 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"type(my_circle)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 44, | |
"text": [ | |
"__main__.Circle" | |
] | |
} | |
], | |
"prompt_number": 44 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"def hello():\n", | |
" print(\"Hello world!\")\n", | |
" \n", | |
"type(hello)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 46, | |
"text": [ | |
"function" | |
] | |
} | |
], | |
"prompt_number": 46 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"But we also saw when we discussed this a few weeks ago that there's apparently more to `type` than meets the eye. You would think that if you called\n", | |
"`type(Circle)` (or type of any other class object) that we might get something called 'class' in return, just like the `type()` of a function is function. But in fact:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"type(Circle)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 47, | |
"text": [ | |
"type" | |
] | |
} | |
], | |
"prompt_number": 47 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"The \"type\" of a class object *is* `type` itself! That suggests that `type()` is more than just a function that tells you the type of an object. In fact, `type` is not a function at all:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"type(type)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 48, | |
"text": [ | |
"type" | |
] | |
} | |
], | |
"prompt_number": 48 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"We've already seen that all objects have a `.__class__` attribute pointing back to the original class of which they are an instance:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"my_circle.__class__" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 49, | |
"text": [ | |
"__main__.Circle" | |
] | |
} | |
], | |
"prompt_number": 49 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"But we've also dicussed that classes themselves are first-class objects in Python with all the same basic attributes and capabilities of any other object like a circle instance. To be specific, classes also have a `.__class__`:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"Circle.__class__" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 50, | |
"text": [ | |
"type" | |
] | |
} | |
], | |
"prompt_number": 50 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"str.__class__" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 51, | |
"text": [ | |
"type" | |
] | |
} | |
], | |
"prompt_number": 51 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"In fact, `type` is not a function, but it is in some sense a class itself--it is the base class of *all* classes in Python (remember--not *objects* in general; `type` is *not* a base class of an instance like `my_circle`, but it is the base class of the class `Circle` itself).\n", | |
"\n", | |
"Of course, this has to end somewhere--there can't be an infinite regress of classes of classes of classes. So that's why it is simply a matter of *definition* that:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"type(type) is type" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 52, | |
"text": [ | |
"True" | |
] | |
} | |
], | |
"prompt_number": 52 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"(as an aside, it is possible to concoct a pathological class that has an infinite regress of base clasess, but that will lead Python quickly to a maximum recursion depth error)." | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"So since `type` is itself sort of a class, calling `type()` is actually calling the `type` class's constructor, in a sense. Just like how when we call `Circle(3)` this is actually calling the `Circle.__init__` constructor. The main difference is that it has a *special case* that when you only pass in one argument (like `type(Circle)`) it just returns the class of that argument.\n", | |
"\n", | |
"So that raises the question: What does the *full* constructor for `type` do, and what arguments does it take?\n", | |
"\n", | |
"The answer, of course, is that since `type` is the class of classes, calling the `type()` returns a new *instance* of `type`. That instance is itself a class object--or just a \"class\". What Python is doing under the hood when you create a new class definition starting with something like, `class Circle(object):...`, it actually takes information gleaned from that class definition and passes it all to `type()` to create the actual class.\n", | |
"\n", | |
"The full signature of the `type` constructor is:\n", | |
"\n", | |
" type(name, bases, members)\n", | |
"\n", | |
"`name` is just a string containing the name of the class, such as `'Circle'`. This is the same string that's returned when you do `Circle.__name__`, for example. `bases` must be a tuple object containing all the *base classes* of the class. In most cases this is just `(object,)`--the default base class for all classes. Though if we're making a subclass, such as our `ColoredCircle` example, `bases` would be `(Circle,)`. Basically, it's anything between the `()` in the top line of the class definition.\n", | |
"\n", | |
"Finally, `members` must be a `dict` object. It's created by reading everything inside the class definition. For example, for each function defined in the class like `def area(self):` and `def circumference(self):` Python creates the function object for `area()`, reads the *name* of that function (`'area'`), and creates an entry in this dictionary like `members['area'] = the_actual_area_function`.\n", | |
"\n", | |
"That was a lot to process, so let's look at the `Circle` definition again and break down exactly what happens when Python reads it. To start with, the original definition of `Circle`, one more time:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"class Circle(object):\n", | |
" \"\"\"Represents a circle with a given radius.\"\"\"\n", | |
" \n", | |
" def __init__(self, radius):\n", | |
" self.radius = radius\n", | |
" \n", | |
" def circumference(self):\n", | |
" return 2 * np.pi * self.radius\n", | |
" \n", | |
" def area(self):\n", | |
" return np.pi * self.radius ** 2" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 53 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Now let's break down line by line what Python does internally when it encounters this (in reality this is done in C but this is Python pseudocode for what it actually does):" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# class Circle(object):\n", | |
"# \n", | |
"# Ah, a class definition. Its name is Circle and it has object as a base class.\n", | |
"# Save the name and bases into some temporary variables, and set up a dictionary to\n", | |
"# contain the class members:\n", | |
"\n", | |
"name = 'Circle'\n", | |
"bases = (object,)\n", | |
"members = {}" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 54 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# \"\"\"Represents a circle with a given radius.\"\"\"\n", | |
"# \n", | |
"# Special case--if I find a string immediately after the beginning of the `class` definition\n", | |
"# this is the docstring for the class:\n", | |
"\n", | |
"members['__doc__'] = \"Represents a circle with a given radius.\"" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 55 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# def __init__(self, radius):\n", | |
"# self.radius = radius\n", | |
"#\n", | |
"# A function definition--run the `def` statement in the local context and add it to members dict:\n", | |
"\n", | |
"def __init__(self, radius):\n", | |
" self.radius = radius\n", | |
" \n", | |
"members[__init__.__name__] = __init__" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 56 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# def circumference(self):\n", | |
"# return 2 * np.pi * self.radius\n", | |
"# \n", | |
"# def area(self):\n", | |
"# return np.pi * self.radius ** 2\n", | |
"#\n", | |
"# Two more function definitions--do the same thing:\n", | |
"\n", | |
"def circumference(self):\n", | |
" return 2 * np.pi * self.radius\n", | |
"\n", | |
"members[circumference.__name__] = circumference \n", | |
"\n", | |
"def area(self):\n", | |
" return np.pi * self.radius ** 2\n", | |
"\n", | |
"members[area.__name__] = area" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 57 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# Just so that we're clear what's going on, let's take one last look at what we have\n", | |
"# from the class definition:\n", | |
"\n", | |
"print(\" Class name:\", name)\n", | |
"print(\" Base classes:\", bases)\n", | |
"print(\"Class members:\")\n", | |
"pprint(members)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "stream", | |
"stream": "stdout", | |
"text": [ | |
" Class name: Circle\n", | |
" Base classes: (<type 'object'>,)\n", | |
"Class members:\n", | |
"{'__doc__': 'Represents a circle with a given radius.',\n", | |
" '__init__': <function __init__ at 0x053D7230>,\n", | |
" 'area': <function area at 0x053D7630>,\n", | |
" 'circumference': <function circumference at 0x053D7830>}\n" | |
] | |
} | |
], | |
"prompt_number": 58 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# Now that we've reached the end of the class definition and gotten everything we need out of it,\n", | |
"# we pass everything to type() and assign the resulting class object to a local variable with the\n", | |
"# *same name* as the class:\n", | |
"\n", | |
"locals()[name] = type(name, bases, members)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 59 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# And now we have a Circle class\n", | |
"Circle" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 60, | |
"text": [ | |
"__main__.Circle" | |
] | |
} | |
], | |
"prompt_number": 60 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# And we can make circle instances as usual\n", | |
"my_circle = Circle(5)\n", | |
"my_circle.area()" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 61, | |
"text": [ | |
"78.53981633974483" | |
] | |
} | |
], | |
"prompt_number": 61 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# If you don't believe me, try calling type() without assigning the result to a variable\n", | |
"# and see what it returns.\n", | |
"# You can do this as many times as you want and it will keep creating new Circle classes,\n", | |
"# it just doesn't assign them to any local variables\n", | |
"type(name, bases, members)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 62, | |
"text": [ | |
"__main__.Circle" | |
] | |
} | |
], | |
"prompt_number": 62 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"a_circle = type(name, bases, members)(radius=3)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [], | |
"prompt_number": 63 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"print(a_circle.__class__)\n", | |
"print(my_circle.__class__)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "stream", | |
"stream": "stdout", | |
"text": [ | |
"<class '__main__.Circle'>\n", | |
"<class '__main__.Circle'>\n" | |
] | |
} | |
], | |
"prompt_number": 64 | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# Remember, a_circle was defined with a 'Circle' class we *just* created\n", | |
"# two cells ago with the type() call. It is *not* the same 'Circle' class\n", | |
"# we were using when we defined my_circle:\n", | |
"\n", | |
"a_circle.__class__ == my_circle.__class__" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"output_type": "pyout", | |
"prompt_number": 65, | |
"text": [ | |
"False" | |
] | |
} | |
], | |
"prompt_number": 65 | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Everything shown above is more or less exactly what's happening under the hood when we make a class\n", | |
"definition in Python. The end result is pretty much indistinguishable. But obviously using the\n", | |
"`class Circle(object):...` statment is a lot easier to read and write ;)\n", | |
"\n", | |
"Having some basic working understanding of all this will allow us to move on to talking about metaprogramming, and in particular *metaclasses* ;)\n", | |
"If you're wondering what to call `type`--the class of classes--you can refer to it as a \"metaclass\". I've already alluded to the fact that we can\n", | |
"create our own metaclasses--such as the example of a class that has an infinite regress of base classes. But we can also use this capability to\n", | |
"do *useful* things. That will be a topic for another time, however." | |
] | |
} | |
], | |
"metadata": {} | |
} | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment