#!/usr/bin/env python # coding: utf-8 # In[ ]: # # Python Classes and Object-Oriented Programming # # Below is a class definition named `Shape()`. It stores the data attribute `shape` which is just a string. The only member function implmented in this class is the special `__str__` function which returns the string `I am a shape` when called on an object instantiated from the class `Shape()`. # In[1]: class Shape(): def __init__(self): self.shape = 'shape' def __str__(self): return "I am a " + self.shape # Now we will instantiation the class and store it in an object with the variable name `s`. Then we call the print statement giving `s` as and argument and it automatically runs the `__str__` function and returns its result. # In[2]: s = Shape() print(s) # Now we will derive a class from `Shape()` called `Polygon()`. A `Polygon()` *is a* `Shape()` and therefore all of the functions defined in `Shape()` will work on an instantiated object of `Polygon()`. # # We will also implement some new functions, not defined in `Shape()` that can computute the perimeter of a polygon and return the number of edges. # # There is also a new data attribute called `side_lengths` which is initialized to `None`. We can't set its exact value yet because we don't have enough information about the polygon, e.g. How many sides it has, but we need to define it in order to define `compute_perimeter()` because it requires `side_lengths` as an argument. Same goes for `get_number_of_sides`. # In[3]: class Polygon(Shape): def __init__(self): self.shape = 'polygon' self.side_lengths = None def compute_perimeter(self): return sum(self.side_lengths) def get_number_of_edges(self): return len(self.side_lengths) # Now we can instantiation a `Polygon()` object called `p` and call `print()` on it. It returns the function call from `__str__` that is *only* defined in `Shape()` because a `Polygon()` *is a* `Shape()`. # In[4]: p = Polygon() print(p) # We can now derived another class from `Polygon()`, this time we are specialized enough that we know a `Rectange()` has 4 sides, so we can give a default value for the data attribute `side_length`. # In[5]: class Rectangle(Polygon): def __init__(self): self.shape = 'rectangle' self.side_lengths = [1, 1, 1, 1] # And now we can call `compute_perimeter()` on the object `rect` that was intantiated from `Rectangle()` even though we didn't define the function in the `Rectangle()` class. We can do this because a `Rectangle()` *is a* `Polygon()`. # In[6]: rect = Rectangle() rect.compute_perimeter() # Likewise, we can derive a `Triangle()` class from `Polygon()` and it *inherits* the member functions from both `Polygon()` and `Shape()`. Again, a `Triangle()` *is a* `Polygon()` which *is a* `Shape()`. # In[7]: class Triangle(Polygon): def __init__(self): self.shape = 'triangle' self.side_lengths = [2, 2, 2] # Examples of calling functions on the object `t` instantiated from `Triangle()`. # In[8]: t = Triangle() t.compute_perimeter() # In[9]: t.get_number_of_edges() # In[10]: print(t) # Everything in Python is an object. Including the lists we've already been using. This might help you understand the `.sort()` function a little better now. # In[11]: x = [20, 4, 100] x.sort() x # You can also define classes that take arguments similar to the way regular functions do. Both classes and functions that are members of a class must use `self` as thier first argument. After that you can use arguments as you normally would, including default arguments and variable arguments. # In[12]: class Car(): def __init__(self, number_of_doors=2): self.number_of_doors = number_of_doors def __str__(self): if self.number_of_doors == 2: return "I am a coupe." if self.number_of_doors == 4: return "I am a sedan." else: return "I don't know what kind of car I am." # An example of instantiating the `Car()` class with different arguments that alter it's behavior. # In[13]: c = Car() # In[14]: print(c) # In[15]: s = Car(4) print(s) # In[16]: s = Car(3) print(s)