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 tests 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.