# Object Oriented Programming Python supports different kinds of data, each is an object, every object has: - a type - an internal data representation (primitive or composite) - a set of procedures for interaction with the object - an object is an instance of a type - `1234` is an instance of an `int` - `"hello"` is an instance of a string ## Object Oriented Programming (OOP) **Everything in python is an object** - **create new** objects - **manipulate** objects - **destroy** objects - explicitly using `del` or just *forget about them* (binding to a new variable) *Python system will reclaim destroyed or inaccessible objects – called “garbage collection”* ## Objects Objects are **data abstraction** that captures - **internal representation** - through data attributes - **interface** for interacting with object - through methods (aka procedures/functions) - defines behaviors but hides implementation ### Example: `[1,2,3,4]` has type list Lists are represented internally through linked list of cells. There are many ways to manipulate a list (mutating): - `L[i]`, `L[i:j]`, `+` - `len()`, `min()`, `max()`, `del(L[i])` - `L.append()`, `L.extend()`, `L.count()`, `L.index()`, `L.insert()`, `L.pop()`, `L.remove()`, `L.reverse()`, `L.sort()` Internal representation should be **private**, correct behavior may be compromised if you manipulate internal representation directly. ## Advantages of OOP - **bundle data into packages** together with procedures that work on them through well-defined interfaces - **divide-and-conquer** development: - implement and test behavior of each class separately - increased modularity reduces complexity - **reusing code is easier with classes:** - many Python modules define new classes - each class has a **separate environment (no collision on function names)** - inheritance allows subclasses to redefine or extend a selected subset of a superclass’ behavior ## Creating and Using Own Types With Classes ### Creating vs Using Classes - **creating** - defining the class name - defining class attributes - *someone wrote code to implement a list class* - **using** - creating new instances of objects - doing operations on the instances - `L=[1,2]` *and* `len(L)` ## Defining Own Types `class` keyword is used to define a new type: ```python class Coordinate(object): #define attributes here ``` `Coordinate()` is the type of object u want to define where in the parentheses will be ur class parent. Similar to `def`, indent code to indicate which statements are part of the class definition. `Coordinate is a a python object, all attributes are **inherited**: - `Coordinate` is a subclass of `object` -`object`is a superclass of `Coordinate` ## Attributes **Data** and **procedures/methods** that belong to the class. - **data attributes** - think of data as other objects that **make up the class** - *a coordinate is made up of two numbers* - **methods** (procedural attributes) - think of methods as functions that only work with this class - **how to interact with the object** - *can define a distance between two coordinate objects but there is no meaning to a distance between two list objects* ## Defining How To Create An Instance of A Class Before creating an instance of a class, **u have to define how to create an instance of it.** `__init__()` is specifically used to initialize data attributes: ```python class Coordinate(object): def __init__(self, x, y): self.x = x self.y = y ``` - **double underscore** `__` is a special method to create an instance - `self` is a p**arameter that refers to an instance of a class**, this is going to be a placeholder for any sort of instance when it creates an object - `x,y` is what **data initializes** a `Coordinate` object - `self.x` and `self.y` are two **data attributes** for every `Coordinate` object. ## Actually Creating An Instance of A Class After defining a way to create instances, we will begin to actually create them: ```python c = Coordinate(3,4) origin = Coordinate(0,0) print(c.x) print(origin.x) ``` - `Coordinate(3,4)` **creates a new object** of type `Coordinate` and **passes 3 and 4** to the `__init__` - the dot in `print(c.x)` is used to **access an attribute of instance c** - don't have to provide a separate argument for `self`, as python automatically assigns it by default. Meaning that `self` by default is assigned to `c` ## Methods A method in python is seen as a procedural attribute, it's like a **function that works only with this class.** - python always passes the object as first argument - **convention is to use** `self` **as the name of the first argument of all methods** - the `.` operator is used to **access any attribute** - a data attribute of an object - a method of an object ## Defining A Method For The Coordinate Class Methods behave like a function other than `self` and dot notation (take params, do operations, return): ```python def distance(self, other): # code here ``` This is the format to **define a method.** Inside the parentheses of a method it **refers to an existing instance called** `self` **and creates another parameter `other`,** combining the previous instance definition will look like this: ```python class Coordinate(object): def __init__(self, x, y): self.x = x self.y = y def distance(self, other): x_diff_sq = (self.x-other.x)**2 y_diff_sq = (self.y-other.y)**2 return (x_diff_sq + y_diff_sq)**0.5 ``` Instance definitions are always followed by a method, in this case it's `distance`. In `distance` parameter there will be `self` and `other`: - `self` is used to refer to any instance - `other` is used as another parameter to method `x_diff_sq` and `y_diff_sq` are variables to calculate the difference of x and y attributes squared. Return operator is also used under method block as it behaves similarly to functions. ## Using Methods Conventional way to use the class: ```python c = Coordinate(3,4) zero = Coordinate(0,0) print(c.distance(zero)) ``` `c` is the object to call the method `distance`. In details, `c` is going to look up the class `Coordinate`, under this class it's going to look up method `distance`. `self` is not included because `c` is assigned to it by default. Inside the parenthesis it gives parameter `zero`. This is also equivalent to (easier to understand although more tedious to write): ```python c = Coordinate(3,4) zero = Coordinate(0,0) print(Coordinate.distance(c, zero)) ``` `print(Coordinate.distance(c, zero))` is the same as `print(c.distance(zero))`. `Coordinate` here is being shown as the class, `distance` being the method and inside the parentheses it includes all of the variables **including** `self`. `zero` here can be seen as `other`. ## Print Representation Of An Object ``` >>> c = Coordinate(3,4) >>> print(c) <__main__.Coordinate object at 0x7fa918510488> ``` - uninformative print representation by default - define a `__str__` method for a class - Python calls the `__str__ `method when used with print on your class object ### **Goal:** Define a print method that shows actual coordinates: ``` >>> print(c) <3,4> ``` ## Defining Your Own Print Methods ```python class Coordinate(object): def __init__(self, x, y): self.x = x self.y = y def distance(self, other): x_diff_sq = (self.x-other.x)**2 y_diff_sq = (self.y-other.y)**2 return (x_diff_sq + y_diff_sq)**0.5 def __str__(self): return "<"+str(self.x)+","+str(self.y)+">" #notice that it's concatenated with the + operator ``` `__str__()` is the name of the special method to define a print. **Remember, ur printing a string, so it has to be in** `"<>"` **.** All of the strings that are in the print method should be concatenated with the `+` operator. ## Wrapping Your Head Around Types and Classes To clear up confusion for types and classes: - ask for the type of an object instance ``` >>> c = Coordinate(3,4) >>> print(c) <3,4> #return of __str__ method >>> print(type(c)) #prints the type of class c is <class __main__.Coordinate> #c is in class Coordinate ``` Notice the difference between `print(c)` and `print(type(c))`. `print(type())` is used to print the type of class the object's in. - this makes sense since ``` >>> print(Coordinate) <class __main__.Coordinate> #Coordinate is a class >>> print(type(Coordinate)) <type 'type'> #Coordinate class is a type of object ``` If we print `Coordinate` itself, it shows that it's a class. However, printing the type of `Coordinate` will just be seen as printing an object **(a** `Coordinate` **class is just an object)**, thus it shows `type`. - use `isinstance()` to check if an object is a `Coordinate` ``` >>> print(isinstance(c, Coordinate)) True ``` `isinstance()` is used to check whether a particular object is an instance of a class. ## Special Operators - `+`, `-`, `==`, `<`, `>`, `len()`, `print`, and many others ### [Basic Customization Operators](https://docs.python.org/3/reference/datamodel.html#basic-customization) Similar to `print`, u can override these to work with ur class. Define them with double underscores before/after: | Method Operators | Object Operators | | - | - | | `__add__(self, other)` | `self + other` | `__sub__(self, other)` | `self - other` | `__eq__(self, other)` | `self == other` | `__lt__(self, other)` | `self < other` | `__len__(self)` | `len(self)` | `__str__(self)` | `print self` This must be used when interacting with two objects of the same class. ### Example: Fractions **Goal: Create a new type to represent a number as a fraction:** - internal representation is two integers - numerator - denominator - interface (methods) how to interact with `Fraction` objects - add, subtract - print representation, convert to a float - invert the fraction *Author will post code and explanation later, try it out first!* ## The Power of OOP - bundle together objects that share - **common attributes** - **procedures** that operate on those attributes - use **abstraction** to make a distinction between **how to implement an object vs how to use the object** - build **layers of object abstractions** that inherit behaviors from other classes of objects - **create our own classes of objects** on top of python’s basic classes