{ "metadata": { "name": "", "signature": "sha256:2480e6348d3cd8708e3df5d61dbfaa57ed879c0e8a135ef0a70070795f06a4e2" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Object-oriented programming" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Introduction" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Object-oriented programming is a way of programming that groups together related data and functions into **objects**, and makes it easier to design more complex programs. In traditional (procedural) programming, you can typically think of a program as an set of instructions that is read from top to bottom, with the exception of functions, which allow you to avoid repetitive code. In object-oriented programming, one instead thinks of a program mainly as setting up a number of 'types' of objects, and doing operations on them.\n", "\n", "Note that you have already been using objects! Every Python object is an object:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "s = 'hello'\n", "s.upper()" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 1, "text": [ "'HELLO'" ] } ], "prompt_number": 1 }, { "cell_type": "code", "collapsed": false, "input": [ "l = [4,2,1]\n", "l.sort()\n", "l" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 2, "text": [ "[1, 2, 4]" ] } ], "prompt_number": 2 }, { "cell_type": "markdown", "metadata": {}, "source": [ "In these cases, ``upper`` and ``sort`` are both **methods**, and ``s`` and ``l`` are **instances** of the ``str`` and ``list`` **class** respectively (we will discuss these terms below)." ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Classes, instances, and methods" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Classes are most easily explained by example, so let's dive right in and look at a **class**, which is used to define an **object**:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "class Person(object):\n", " \n", " def __init__(self, name):\n", " self.name = name\n", " \n", " def say_hello(self):\n", " print(\"Hello, my name is \" + self.name)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 3 }, { "cell_type": "markdown", "metadata": {}, "source": [ "and let's try and use it:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "tom = Person('Tom')\n", "tom.say_hello()\n", "me = Person(\"Markus\")\n", "me.say_hello()\n", "me.name, Person.name" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Hello, my name is Tom\n", "Hello, my name is Markus\n" ] }, { "ename": "AttributeError", "evalue": "type object 'Person' has no attribute 'name'", "output_type": "pyerr", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0mme\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mPerson\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Markus\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mme\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msay_hello\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0mme\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mPerson\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mAttributeError\u001b[0m: type object 'Person' has no attribute 'name'" ] } ], "prompt_number": 11 }, { "cell_type": "markdown", "metadata": {}, "source": [ "There is a lot of new syntax here which you won't have likely seen before, but also some syntax which will look more familiar. First, let's have a look at the class definition. The class is defined using the following syntax:\n", "\n", " class Person(object):\n", " ...\n", "\n", "This looks similar to the definition for a function, except that it doesn't directly contain code, but it then contains a series of functions:\n", "\n", " class Person(object):\n", " \n", " def __init__(self, ...):\n", " ...\n", "\n", " def say_hello(self, ...):\n", " ...\n", "\n", "So in effect, a class is a collection of functions. What is special about these functions? If you look at the full class definition above, you will see that the first argument to all the functions is ``self``, which is the object itself. Why is this useful?\n", "\n", "At this point, we need to clarify some more terminology: an **instance** of a class is a particular object represented by the **class** - that is, while ``Person`` is the class (i.e. the general definition of the idea of a 'person'), an instance is a *particular* person, e.g. Tom:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "tom = Person('Tom')" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 12 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here ``tom`` is an instance of the ``Person`` class. Now let's look at the first defined **method**, called ``__init__``:\n", "\n", " def __init__(self, name):\n", " self.name = name\n", "\n", "The term **method** is basically used for a function that is attached to a class. The ``__init__`` method is a method that is automatically called when creating an instance of the class (for those familiar with the terminology, it is equivalent to a constructor). What is basically happening here is that ``self`` is the actual instance that is being created, and the method then takes the name of the person and assigns it to the ``name`` **attribute** of the instance:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "tom = Person('Tom')\n", "print(tom.name)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Tom\n" ] } ], "prompt_number": 13 }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, the ``tom`` instance has an attribute ``name`` that has been set to the name that was passed. Now let's look at the second method:\n", "\n", " def say_hello(self):\n", " print \"Hello, my name is \" + self.name\n", "\n", "This looks more like a normal function, but takes the same ``self`` argument. It then prints out a string containing the value of the ``name`` attribute *of this instance*. Note that while all methods should take the ``self`` argument, this argument doesn't need to be passed because when calling a method, this is automatically done:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "tom.say_hello,Person.say_hello" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 17, "text": [ "(>,\n", " )" ] } ], "prompt_number": 17 }, { "cell_type": "code", "collapsed": false, "input": [ "tom.say_hello()" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Hello, my name is Tom\n" ] } ], "prompt_number": 18 }, { "cell_type": "markdown", "metadata": {}, "source": [ "is equivalent to:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "Person.say_hello(tom)\n", "Person.say_hello(me)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Hello, my name is Tom\n", "Hello, my name is Markus\n" ] } ], "prompt_number": 20 }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the last example, we passed the instance explicitly to the function, but of course the first notation is much cleaner and simpler. Now let's look at the following example:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "alice = Person(\"Alice\")\n", "bob = Person(\"Bob\")\n", "alice.say_hello()\n", "bob.say_hello()" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Hello, my name is Alice\n", "Hello, my name is Bob\n" ] } ], "prompt_number": 21 }, { "cell_type": "markdown", "metadata": {}, "source": [ "as you can see, when calling ``say_hello``, the result will depend to the actual object that the method is attached to.\n", "\n", "Since they are essentially functions, methods can of course take arguments, which can be any Python object(s). For example, we can make a new ``say_hello_to`` method that will take another ``Person`` instance and say hello to them:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "class Person(object):\n", " \n", " def __init__(self, name):\n", " self.name = name\n", " \n", " def __str__(self):\n", " return \"The Person {}\".format(self.name)\n", " \n", " def __repr__(self):\n", " return \"{}({})\".format(self.__class__.__name__,\n", " repr(self.name))\n", " \n", " def say_hello(self):\n", " print(\"Hello, my name is \" + self.name)\n", " \n", " def say_hello_to(self, other):\n", " print(\"Hello \" + other.name + \", my name is \" + self.name)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 47 }, { "cell_type": "code", "collapsed": false, "input": [ "alice = Person(\"Alice\")\n", "bob = Person(\"Bob\")\n", "alice.say_hello_to(bob)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Hello Bob, my name is Alice\n" ] } ], "prompt_number": 48 }, { "cell_type": "code", "collapsed": false, "input": [ "print(repr(alice))" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Person('Alice')\n" ] } ], "prompt_number": 49 }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we will see below, you can actually pass any object to ``say_hello_to`` as long as it has a ``name`` attribute." ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Inheritance" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One of the powerful features of object-oriented programming is **inheritance**, which means that it is possible to define classes based on other classes. For example, we can define:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from scipy.integrate import simps\n", "\n", "class Scientist(Person):\n", " \n", " def integrate(self, x, y):\n", " return simps(y, x=x)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 50 }, { "cell_type": "markdown", "metadata": {}, "source": [ "This looks similar to before, but this time when defining the class, we've used ``Person`` instead of ``object``. This means that by default, ``Scientist`` will behave like a ``Person`` instance, but it then has some additional methods defined:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "alice = Scientist(\"Alice\")\n", "alice.integrate([1,2,3], [4,5,6])" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 51, "text": [ "10.0" ] } ], "prompt_number": 51 }, { "cell_type": "markdown", "metadata": {}, "source": [ "The ``say_hello_to`` method takes any object that has a ``name`` attribute, so it can say hello to another person:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "alice" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 52, "text": [ "Scientist('Alice')" ] } ], "prompt_number": 52 }, { "cell_type": "code", "collapsed": false, "input": [ "bob = Person(\"Bob\")\n", "alice.say_hello_to(bob)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Hello Bob, my name is Alice\n" ] } ], "prompt_number": 28 }, { "cell_type": "markdown", "metadata": {}, "source": [ "or scientist:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "eve = Scientist(\"Eve\")\n", "alice.say_hello_to(eve)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Hello Eve, my name is Alice\n" ] } ], "prompt_number": 29 }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Attributes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As mentioned above, attributes are variables attached to the object. It is worth mentioning that attributes are *not* static, so they can be changed from outside the class:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "p = Person('Tom')\n", "p.say_hello()" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Hello, my name is Tom\n" ] } ], "prompt_number": 53 }, { "cell_type": "code", "collapsed": false, "input": [ "p.name = 'Alice'\n", "p.say_hello()" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Hello, my name is Alice\n" ] } ], "prompt_number": 54 }, { "cell_type": "markdown", "metadata": {}, "source": [ "and it is also possible to create new attributes from the outside:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "p.age = 97\n", "p.name\n", "p.age" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 55, "text": [ "97" ] } ], "prompt_number": 55 }, { "cell_type": "markdown", "metadata": {}, "source": [ "This last practice (creating attributes in an object) is typcally a bad idea, because you're essentially violating the object's \u201cnamespace\u201d, the names the object manages, and it's not hard to break things in this way.\n", "\n", "Then again, python is a language for adults. It doesn't keep you from doing dumb things (though you might have to become really evil to do some particularly dumb things) because that would keep you from doing smart things." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A good way to think about objects is to say:\n", "\n", "* attributes = state\n", "* methods = behaviour" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Why use objects?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So far, the programs you have needed to write have not been very complex, but as you write more and more complex analysis, classes may come in useful in some situations. For example, if you are doing particle physics, you might want to use a class to represent a ``Particle``, which then has common operations and calculations you might want to do with a particle. If you are doing Astronomy, you could use a ``Star`` or ``Galaxy`` class that would be used to represent these objects. You can even use objects without defining actual methods, but just as a convenience to contain several variables - if you need to always handle cartesian point in your code, you could define:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "class Point(object):\n", " def __init__(self, x, y, z):\n", " self.x = x\n", " self.y = y\n", " self.z = z" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 56 }, { "cell_type": "code", "collapsed": false, "input": [ "pt = Point(1, 2, 3)\n", "pt.x" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 57, "text": [ "1" ] } ], "prompt_number": 57 }, { "cell_type": "markdown", "metadata": {}, "source": [ "After which if you want to pass a point to a function, you can pass it in a single variable instead of three:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "def find_separation(p1, p2):\n", " return np.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2 + (p1.z - p2.z)**2)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 58 }, { "cell_type": "markdown", "metadata": {}, "source": [ "as opposed to:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "def find_separation(x1, y1, z1, x2, y2, z2):\n", " return np.sqrt((x1 - x2)**2 + (y1 - y2)**2 + (z1 - z2)**2)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 59 }, { "cell_type": "markdown", "metadata": {}, "source": [ "This might not look like a big difference, but now imagine that you also wanted to pass 3-d velocities, then you would need to call the function with 12 arguments!\n", "\n", "This is not to say that you should *always* use objects, but if you start to think of your program instead of what objects and basic operations are being represented, you may be able to write it more simply than if you were using only functions and procedural code. You may also be able to re-use classes for different projects if they are general enough!" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Exercise" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. Write a ``Particle`` class that can be used to represent a particle with a mass, a 3-d position, and a 3-d velocity.\n", "\n", "2. Write a method that can be used to compute the kinetic energy of the particle\n", "\n", "3. Write a method that takes another particle as an argument and finds the distance between the two particles\n", "\n", "4. Write a method that given a time interval ``dt`` will update the position of the particle to the new position based on the current position and velocity.\n", "\n", "5. Write a ``ChargedParticle`` class that inherits from the ``Particle`` class, but also has an attribute for the charge of the particle.\n", "\n", "6. Write examples of using these classes to test that the methods are working correctly." ] }, { "cell_type": "code", "collapsed": false, "input": [ "\n", "# your solution here\n" ], "language": "python", "metadata": {}, "outputs": [] } ], "metadata": {} } ] }