Lecture 18 — Classes, Part 1
=============================

Overview
--------

-  Define our own types and associated functions

-  Encapsulate data and functionality

-  Raise the “level of abstraction” in our code

-  Make code easier to write and test

-  Reuse code

Potential Examples
------------------

In each of these, think about what data you might need to store to
represent the “object” and what functionality you might need to apply to
the data.

-  Date

-  Time

-  Point

-  Rectangle

-  Student

-  Animal

-  Molecule

An Example from Earlier in the Semester
---------------------------------------

-  Think about how difficult it was to keep track of the information
   about each restaurant in the Yelp data.

-  You had to:

   -  Remember the indices of (a) the restaurant name, (b) the latitude
      and longitude, (c) the type of restaurant, (d) the address, etc.

   -  Form a separate list inside the list for the ratings.

   -  Write additional functions to exploit this information

-  If we used a class to represent each restaurant:

   -  All of the information about the restaurant would be stored and
      accessed as named attributes

   -  Information about the restaurants would be accessed through
      functions that we write for the class.

Point2d Class
-------------

-  Simplest step is to just tell Python that ``Point2d`` will exist as a
   class, deferring the addition of information until later.

   ::

       class Point2d(object):
           pass  

-  The Python reserved word ``pass`` says that this is the end of the
   class definition.

   -  We will not need this later when we put information into the
      class.

Attributes
----------

-  Classes do not get interesting until we put something in them.

-  The first thing we want is variables so that we can put data into a
   class.

   -  In Python these variables are often called *attributes*.

   -  Other languages call them *member variables*.

-  We will see three different ways to specify attributes.

Assigning Attributes to Each Instance
-------------------------------------

-  Points have an ``x`` and a ``y`` location, so we can write, for
   example,

   ::

       from math import sqrt
       p = Point2d()
       p.x = 10
       p.y = 5
       dist_from_origin = sqrt(p.x**2 + p.y**2)

-  We have to do this for each class instance.

-  This is prone to mistakes:

   -  Could forget to assign the attributes

   -  Could accidentally use different names for what is intended to be
      the same attribute.

-  Example of an error

   ::

       q = Point2d()
       q.x = -5
       dist_from_origin = sqrt(q.x**2 + q.y**2)    # q.y does not exist

Defining the Attributes Inside the Class
----------------------------------------

-  The simplest way to make sure that all variables that are instances
   of a class have the appropriate attributes is to define them inside
   the class.

-  For example, we could redefine our class as

   ::

       class Point2d(object):
           x = 0
           y = 0

-  All instances of ``Point2d`` now have two attributes, ``x`` and
   ``y``, and they are each initialized to 0.

-  We no longer need the ``pass`` because there is now something in the
   class.

Defining the Attributes Through An Initializer / Constructor
------------------------------------------------------------

-  We still need to initialize ``x`` and ``y`` to values other than
   ``0``:

   ::

       p = Point2d()
       p.x = 10
       p.y = 5  

-  What we’d really like to do is initialize them at the time we
   actually create the ``Point2d`` object:

   ::

       p = Point2d(10,5)  

-  We do this through a special function called an *initializer* in
   Python and a *constructor* in some other programming languages.

-  Inside the class this looks like

   ::

       class Point2d(object):
           def __init__( self, x0, y0 ):
               self.x = x0
               self.y = y0

-  Our code to create the point now becomes

   ::

       p = Point2d(10,5)  

-  Notes:

   -  Python uses names that start and end with two ``'_'`` to indicate
      functions with special meanings.  More on this later in the lecture.

   -  The name ``self`` is special notation to indicate that the object
      itself is passed to the function.

-  If we’d like to initialize the point to :math:`(0,0)` without passing
   these values to the constructor every time then we can specify
   default arguments

   ::

       class Point2d(object):
           def __init__( self, x0=0, y0=0 ):
               self.x = x0
               self.y = y0

   allowing the initalization

   ::

       p = Point2d()  


Methods — Functions Associated with the Class
---------------------------------------------

-  We create functions that operate on the class objects inside the
   class definition:

   ::

       import math

       class Point2d(object):
           def __init__( self, x0, y0 ):
               self.x = x0
               self.y = y0

           def magnitude(self):
               return math.sqrt(self.x**2 + self.y**2)

           def dist(self, o):
               return math.sqrt( (self.x-o.x)**2 + (self.y-o.y)**2 )

   these are called *methods*

-  This is used as

   ::

       p = Point2d(0,4)
       q = Point2d(5,10)
       leng = q.magnitude()
       print("Magnitude {:.2f}".format( leng ))
       print("Distance is {:.2f}".format( p.dist(q)))

-  The method ``magnitude`` takes a single argument, which is the
   ``Point2d`` object called ``self``.  Let's examine this:

   -  The call ``q.magnitude()`` appears to have no arguments, but
      when Python sees this, it turns it into its equivalent:

      ::

         Point2d.magnitude(q)

      which is completely legal Python syntax.  

   -  The name ``self`` is not technically special in Python, but it
      is used by convention to refer to the object that the method is
      "called upon".  This is  `q`` in the call ``q.magnitude()`` 

-  The method ``dist`` takes two ``Point2d`` objects as
   arguments.  The example call

   ::

       p.dist(q)

   becomes

   ::

       Point2d.dist(p,q)

   so now argument ``p`` maps to parameter ``self`` and argument ``q``
   maps to parameters ``o``


Lecture Exercises, Part 1
-------------------------

Our lecture exercises for today will involve adding to the ``Point2d``
class and testing it.  Make sure you have downloaded the
``Point2d.py`` file from the Piazza site.

We will allow some time to work on the first lecture exercise.



Operators and Other Special Functions
-------------------------------------

-  We’d like to write code that uses our new objects in the most
   intuitive way possible.

-  For our point class, this involves use of operators such as

   ::

       p = Point2d(1,2)
       q = Point2d(3,5)
       r = p+q
       s = p-q
       t = -s

-  Notice how in each case, we work with the ``Point2d`` variables
   (objects) just like we do with int and float variable (objects).

-  We implement these by writing the special functions ``__add__``,
   ``__sub__``, and ``__neg__``

-  For example, inside the ``Point2d`` class we might have

   ::

       def __add__(self,other):
            return Point2d(self.x + other.x, self.y+other.y)

   Very important:  this creates a new ``Point2d`` object.

-  When Python sees ``p+q``, it turns it into the function call

   ::

       Point2d.__add__(p,q)

   which is exactly the syntax of the function definition we created. 

-  We have already seen this with operators on integers and strings.
   As examples,

   ::

      5+6

   is equivalent to

   ::

      int.__add__(5,6)

   and

   ::

      str(13)

   is equivalent to

   ::

      int.__str__(13)


-  Implicit in this discussion is the notion that ``int`` is in fact a
   class in Python.  The same is true of ``str`` and ``float`` and
   ``list``. 

-  Note that we can also define boolean operators such as ``==`` and ``!=``
   through the special functions ``__eq__`` and ``__neq__``


Classes and Modules
-------------------

-  Each class should generally be put into its own module, or several
   closely-related classes should be combined in a single module.

   -  We are already doing this with ``Point2d``.

-  Doing so is good practice for languages like C++ and Java, where
   classes are placed in separate files.

-  Testing code can be included in the module or placed in a separate
   module.

-  We will demonstrate this in class and post the result on the course
   website.


More Lecture Exercises
----------------------

At this point we will stop and take a bit of time to work on the next
part of the lecture exercises.


When to Modify, When to Create New Object
-----------------------------------------

-  Some methods, such as ``scale``, modify a single ``Point2d`` object

-  Other methods, such as our operators, create new ``Point2d`` objects
   without modifying existing ones.

-  The choice between this is made on a method-by-method basis by
   thinking about the meaning — the *semantics* — of the behavior of the
   method.

Programming Conventions
-----------------------

-  Don’t create attributes outside the class.

-  Don’t directly access or change attributes except through class
   methods.

   -  Languages like C++ and Java have constructions that enforce this.

   -  In languages like Python it is not a hard-and-fast rule.

-  Class design is often most effective by thinking about the required
   methods rather than the required attributes.

   -  As an example, we rarely think about how the Python ``list`` and
      ``dict`` classes are implemented.

Time Example
------------

-  In the remainder of the lecture, we will work through an extended
   example of a ``Time`` class

-  By this, we mean the time of day, measured in hours, minutes and
   seconds.

-  We’ll brainstorm some of the methods we might need to have.

-  We’ll then consider several different ways to represent the time
   internally:

   -  Hours, minutes and seconds

   -  Seconds only

   -  Military time

-  Despite potential internal differences, the methods — or at least the
   way we call them — will remain the same

   -  This is an example of the notion of *encapsulation*, which we will
      discuss more in Lecture 19.

-  At the end of lecture, the resulting code will be posted and
   exercises will be generated to complete the class definition.

Summary
-------

-  Define new types in Python by creating classes

-  Classes consist of *attributes* and *methods*

-  Attributes should be defined and initialized through the special
   method call ``__init__``. This is a *constructor*

-  Other special methods allow us to create operators for our classes.

-  We looked at a *Point2d* and *Time* example.