.. _ch05-python-oop: =============================================================== A short example of object-oriented programming (OOP) in Python =============================================================== Python supports object-oriented programming (OOP). Some goals of OOP, among many others, are: * To package data and code together (encapsulation) * Organization of the code such that relevant parts are bundled together * Facilitate and ease interaction with more complex derived data types -------------------------------------- An example: Polynomial interpolation -------------------------------------- As a small example we'll start by wrapping a univariate polynomial interpolation routine into a class. Initially this class will work for arbitrary points sets, though we'll specialize it later. A straightforward implementation is: .. literalinclude:: ./codes/PolyInterp.py :language: python :linenos: :download:`Download this code <./codes/PolyInterp.py>` In this example we can see a few salient features of OOP within Python. The class ``PolyInterp`` has several functions (methods) defined within it. The first, and most important, is the ``__init__`` function, which accomplishes the following: * ``__init__`` defines all member data of the class. In this case the variables are as follows: * ``nodes``: Point locations where interpolants will be anchored to * ``N``: The number of interpolation locations * ``deg``: The degree of the underlying polynomial (this is always ``N-1``) * ``V``: The Vandermonde matrix associated to the given node set * ``__init__`` performs any set up required to instantiate a new object of the class. That is, ``__init__`` is the constructor For ``__init__``, and all other methods defined in the class, the first argument is always ``self``. This gives a way for the method to act on member variables of the class that belong to distinct objects. That is, different objects instantiated from the same class will have different internal data (this is encapsulation). Note however, that when instantiating an object the ``self`` argument is implied, and need not be written (nor could it be). Similarly when calling methods on existing objects the ``self`` argument is implied. Alternatively, you can call a class method and pass an object as ``self``. Consider the following snippet: .. code-block:: python >>> import numpy as np >>> from PolyInterp import PolyInterp >>> equi = PolyInterp(np.linspace(-1,1,9)) >>> data = np.random.rand(9) >>> # The following two are completely equivalent >>> equi.interp(data) >>> PolyInterp.interp(equi,data) As a touchstone to something we did before, consider having a NumPy array called ``arr``. We could get the maximum value of this array by calling either ``arr.max()`` or ``np.max(arr)``. Running this code using the default setting of 9 equispaced nodes in the interval :math:`[-1,1]` should produce the following output and figure. .. code-block:: console $ python PolyInterp.py Max error for equispaced: 1.0451573170836823 .. _figEquiInterp: .. figure:: ./_figures/equiInterp.png :align: center Interpolation of Runge's function using an equispaced grid of points. Note that the vertical axis is truncated to avoid the oscillations near boundaries dominating the figure. ----------------------------------------- Inheritance and Chebyshev interpolation ----------------------------------------- Specifying the nodes for interpolation everytime is a bit tedious. Perhaps more importantly, there are node sets with very different behaviors regarding interpolation quality. The equispaced nodes we used in the previous example are incredibly bad. A much better set is given by the `Chebyshev nodes `_: .. math:: x_k = \cos\left(\frac{2k-1}{2N}\pi\right),\qquad k=1,\ldots,N Rather than generating these nodes everytime we need them, we can create a class specifically for Chebyshev interpolation. However, we've already written a bit of code for interpolation, and it seems wasteful to re-write all the parts that are the same. A nice solution is to use *inheritance*. We'll create ``ChebyInterp`` as a specialization of the ``PolyInterp`` class. In this way ``ChebyInterp`` will inherit all of the member functions and data members from the parent class ``PolyInterp``, and we can override items as needed. The code to accomplish this is as follows: .. literalinclude:: ./codes/ChebyInterp.py :language: python :linenos: :download:`Download this code <./codes/ChebyInterp.py>` The primary lines of interest are 15 and 19. When declaring the ``ChebyInterp`` class on line 15 we pass the ``PolyInterp`` class to indicate that it is contained within this new one. Then, inside the ``__init__`` function we construct the Chebyshev nodes, and forward them to the ``__init__`` function for the underlying ``PolyInterp`` class (line 19). Most of this is just a main function to play around with the classes we've created. The definition of the ``ChebyInterp`` class actually required only four lines! Running this code using the default setting of 9 nodes should produce the following output and figure. .. code-block:: console $ python PolyInterp.py Max error for equispaced: 1.0451573170836823 Max error for Chebyshev: 0.17073385326790647 .. _figChebyInterp: .. figure:: ./_figures/chebyInterp.png :align: center Interpolation of Runge's function using both equispaced nodes and Chebyshev nodes. Note that the Chebyshev interpolant strongly resists oscillation near the boundaries compared to the interpolant over equispaced points There is plenty more to learn about objected oriented programming in Python. These examples are meant to be rather small and easily approachable. A good starting point for deeper study is the tutorial on the Python website: * ``_ ------------- Exercises ------------- #. Try running the ``ChebyInterp`` example for different numbers of nodes. Note that the main function will pull in a command line argument to make this easier. What can you observe? #. Try growing the interval where the interpolants are evaluated. Does the superior performance of Chebyshev interpolation continue now that we are extrapolating the function? #. Add a method to the ``PolyInterp`` class to evaluate the derivative of the interpolant. Observe that we can use it on ``ChebyInterp`` objects without changing the ``ChebyInterp`` class definition at all.