# M9: Classes and Objects ### 1. The Basics of OOP #### Interests: * Handle the rapidly increasing size and complexity of software systems * Make it easier to modify and maintain these large and complex systems over time * Exceptional amount of "organizational power" to our thinking #### OOP vs Procedural programming * In procedural programming paradigm the focus is on writing __functions__ or procedures, which operate on data. * In OOP, the focus instead is on the creation of objects which combine both data and the functions that operate on that data, e.g. `Turtle`, `str`, `Point`. * Usually, each object definition corresponds to some object or concept in the real world, and the functions that operate on (the data encapsulated in) that object correspond to the ways those real-world objects can interact. #### Glossary > **class** A user-defined compound type. A class can also be thought of as a template for the objects that are instances of it. (The iPhone is a class. By December 2010, estimates are that 50 million instances had been sold!) > **constructor** A class can also be seen as a "factory" for making objects of a certain kind. Every class thus provides a constructor method, called by the same name as the class, for making new instances of this kind. If the class has an initializer method, this method is used to get the attributes (i.e. the state) of the new instance properly set up. > **initializer** A special method in Python (called __init__) that is invoked automatically to set a newly created object's attributes to their initial (factory-default) state. > **instance** An object whose type is of some class. The words instance and object are used interchangeably. > **instance variable** Since the attribute values of an object are specific to that particular object (i.e., another object of the same class may have another value for that attribute), they are sometimes also referred to as instance variables. > **instantiate** To create an instance of a class, and to run its initializer. > **instance method** A function that is defined inside a class definition and is invoked on instances of that class. > **object** A compound data type that is often used to model a thing or concept in the real world. It bundles together the data and the operations that are relevant for that kind of data. The words instance and object are used interchangeably. > __deep copy__ To copy the contents of an object as well as any embedded objects, and any objects embedded in them, and so on; implemented by the deepcopy function in the copy module. > __deep equality__ Equality of values, or two references that point to (potentially different) objects that have the same value. > __shallow copy__ To copy the contents of an object, including any references to embedded objects; implemented by the copy function in the copy module. > __shallow equality__ Equality of references, or two references that point to the same object. > __string converter method__ A special method in Python (called __str__) that produces an informal string representation of an object. ### 2. User-defined compound data types #### Creating a class * It may be helpful to think of a class as a **factory** for making objects. The class itself isn't an instance of a point, but it contains the machinery to make point instances. * Every time we call the **constructor**, we're asking the factory to make us a new object. * Instantiation = creation and initialization * If the first line after the class header is a string, it becomes the **docstring** of the class, and will be recognized by various tools. (This is also the way docstrings work in functions.) * Every class should have a method with the special name `__init__`. * This initializer method is automatically called whenever a new object (or instance) of class Point is created. * It gives the programmer the opportunity to set up the attributes required within the new instance by giving them their initial state/values. * The `self` parameter (convention) is automatically set to reference the newly created object that needs to be initialized. * Object instances have both **attributes** (the data contained in the instance) and **methods** (the operations that act on that data) * A method behaves like a function but it is invoked on a specific instance, e.g. `t.right(90)` which turns a Turtle object `t` 90 degrees to the right. Like data attributes, methods are accessed using the dot notation. ```python class Point: """ The Point class represents and manipulates x,y coordinates. """ def __init__(self, x=0, y=0): """ Create a new point at coordinates x, y """ self.x = x self.y = y def distance_from_origin(self): """ Compute my distance from the origin """ return pow((self.x ** 2) + (self.y ** 2),1/2) ``` ```python p = Point() # Instantiate an object of type Point q = Point() # Make a second point object >>> print(p.x, p.y, q.x, q.y) # Each point object has its own x and y 0 0 0 0 >>> p = Point(3, 4) >>> p.x 3 >>> p.distance_from_origin() 5.0 >>> q = Point(5, 12) >>> q.y 12 >>> q.distance_from_origin() 13.0 >>> r = Point() >>> r.x 0 ``` ### 3.Instances as arguments and parameters * Be aware that a variable only holds a reference to an object, so passing `tess` into a function creates an alias: both the caller and the called function now have a reference, but there is only one turtle! ```python def print_point(pt): print("({0}, {1})".format(pt.x, pt.y)) ``` ### 4. Converting an instance to a string * If we call our new method `__str__` instead of `to_string`, the Python interpreter will use our code whenever it needs to convert a `Point` to a `str`. ```python class Point: # ... def __str__(self): return "({0}, {1})".format(self.x, self.y) ``` ### 5. Instances as return values ```python class Point: # ... def halfway(self, target): """ Return the halfway point between myself and the target """ mx = (self.x + target.x)/2 my = (self.y + target.y)/2 return Point(mx, my) ``` ``` >>> print(Point(3, 4).halfway(Point(5, 12))) (4.0, 8.0) ``` ### 6. A change of perspective * The original syntax for a function call, `print_time(current_time)`, suggests that the function is the active agent. It says something like, "Hey, print_time! Here's an object for you to print." * In OOP, the objects are considered the active agents instead. An invocation like `current_time.print_time()` says "Hey current_time! Please print yourself!" * Example: `l.sort()` vs `sorted(l)` ### 7. Objects can have state * Objects are most useful when we also need to keep some state that is updated from time to time. * Consider a `Turtle` object. * Its state consists of things like its position, its heading, its color, and its shape. * A method like `left(90)` updates the turtle's heading, forward changes its position, and so on. * For a **bank account** object, a main component of the state would be the current balance, and perhaps a log of all transactions. * The methods would allow us to query the current balance, deposit new funds, or make a payment. * Making a payment would include an amount, and a description, so that this could be added to the transaction log. We'd also want a method to show the transaction log. ### 8. Rectangles ```python class Rectangle: """ The Rectangle class represents rectangles in a Cartesian plane. """ def __init__(self, pos, w, h): """ Initialize rectangle at position pos, with width w, height h """ self.corner = pos self.width = w self.height = h def __str__(self): return "({0}, {1}, {2})".format(self.corner, self.width, self.height) box = Rectangle(Point(0, 0), 100, 200) bomb = Rectangle(Point(100, 80), 5, 10) # In some video game ``` ```python >>> print("box: ", box) box: ((0, 0), 100, 200) >>> print("bomb: ", bomb) bomb: ((100, 80), 5, 10) >>> print(box.corner.x) 0 ``` ### 9. Objects are mutable ```python def grow(self, delta_width, delta_height): """ Grow (or shrink) this object by the deltas """ self.width += delta_width self.height += delta_height def move(self, dx, dy): """ Move this object by the deltas """ self.corner.x += dx self.corner.y += dy ``` ```python >>> r = Rectangle(Point(10,5), 100, 50) >>> print(r) ((10, 5), 100, 50) >>> r.grow(25, -10) >>> print(r) ((10, 5), 125, 40) >>> r.move(-10, 10) print(r) ((0, 15), 125, 40) ``` ### 10. Sameness * If we say,*"Alice and Bob have the same mother"*, we mean that her mother and his are the same person. * If we say, however, *"Alice and Bob have the same car"*, we probably mean that her car and his are the same make and model, but that they are two different cars. * When we talk about objects, there is a similar ambiguity. For example, if two `Point` are the same, does that mean they contain the same data (coordinates) or that they are actually the same object? * We can use the `is` operator to find out if two references refer to the same object: ```python >>> p1 = Point(3, 4) >>> p2 = Point(3, 4) >>> p1 is p2 False ``` * In the example above, even though p1 and p2 contain the same coordinates, they are not the same object. #### Shallow equality * If we assign `p1` to a new variable `p3`, however, then the two variables are aliases of (refer to) the same object: ```python >>> p3 = p1 >>> p1 is p3 True ``` * This type of equality is called **shallow equality** because it compares only the references, not the actual contents of the objects. * To compare the contents of the objects — deep equality — we can write a function called `same_coordinates`: ```python def same_coordinates(p1, p2): return (p1.x == p2.x) and (p1.y == p2.y) ``` * Of course, if two variables refer to the same object (as is the case with p1 and p3), they have _both_ shallow _and_ deep equality. #### Beware of `==` * Python has a powerful feature that allows a designer of a class to decide what an operation like `==` or `<` should mean, similar to `__str__` * So we conclude that even though the two lists (or tuples, etc.) are distinct objects with different memory addresses, for lists the `==` operator tests for deep equality, while in the case of `Points` it makes a shallow test. #### Example (interactive schema) : [click here](https://cscircles.cemc.uwaterloo.ca/visualize#code=class+Point%3A%0A++++%22%22%22+The+Point+class+represents+and+manipulates+x,y+coordinates.+%22%22%22%0A%0A++++def+__init__(self,+x%3D0,+y%3D0)%3A%0A++++++++%22%22%22+Create+a+new+point+at+coordinates+x,+y+%22%22%22%0A++++++++self.x+%3D+x%0A++++++++self.y+%3D+y%0A%0A++++def+distance_from_origin(self)%3A%0A++++++++%22%22%22+Compute+my+distance+from+the+origin+%22%22%22%0A++++++++return+pow((self.x+**+2)+%2B+(self.y+**+2),1/2)%0A++++%0A++++def+__eq__(p1,+p2)%3A%0A++++++++return+(p1.x+%3D%3D+p2.x)+and+(p1.y+%3D%3D+p2.y)%0A+++++++++%0Ap1+%3D+Point(3,+4)%0Ap2+%3D+Point(3,+4)%0Aprint(%22Est-ce+que+p1+et+p2+ont+les+m%C3%AAmes+adresses+m%C3%A9moire%3A%22,p1+is+p2)%0Aprint(p1%3D%3Dp2)+%0A&mode=display&raw_input=&curInstr=16) ### 11. Copying * __Aliasing__ (different variables referring to a same object) can make a program difficult to read because changes made in one place might have unexpected effects in another place. It is hard to keep track of all the variables that might refer to a given object. * Copying an object is often an alternative to aliasing. The copy module contains a function called copy that can duplicate any object: ```python >>> import copy >>> p1 = Point(3, 4) >>> p2 = copy.copy(p1) >>> p1 is p2 False >>> same_coordinates(p1, p2) True ``` * For something like a Rectangle, which contains a internal reference to a Point (to represent its upper-leftcorner), a simple __shallow copy__ doesn't do quite the right thing. ```python >>> import copy >>> b1 = Rectangle(Point(0, 0), 100, 200) >>> b2 = copy.copy(b1) >>> b1.move(10,10) >>> print(b2.corner) (10,10) ``` * In the example above, although we didn't explicitly move b2, we can see that its corner object has changed as a side-effect of moving b1. This behavior is confusing and error-prone. The problem is that the shallow copy has created an alias to the Point that represents the corner. * We thus need __deep copy__ to completely separate b1 from b2: ```python >>> b1 = Rectangle(Point(0, 0), 100, 200) >>> b2 = copy.deepcopy(b1) >>> b1.move(10,10) >>> print(b1.corner) (10,10) >>> print(b2.corner) (0,0) ``` # Annexe: Restructuration ## 1. Différences entre égalité et référence * Pour ceux qui n'ont pas assisté à la fin de la séance avec l'explication sur l'exercice "Une classe simple", je vous invite à le compléter chez vous avant de démarrer la mission. * Cet exercice est crucial pour comprendre la notion de référence et d'égalité * Conseil: utiliser [Python Tutor](https://cscircles.cemc.uwaterloo.ca/visualize#code=class+Pair%3A%0A++++%22%22%22%0A++++Une+paire+d'entiers%0A++++%22%22%22%0A%0A++++def+__init__(self,+x%3DNone,+y%3DNone)%3A%0A++++++++%22%22%22%0A++++++++%40pre+-%0A++++++++%40post+cr%C3%A9e+une+paire+(a,b)+compos%C3%A9e+de+x+et+y,%0A++++++++++++++ou+une+paire+non-initialis%C3%A9e+si+aucune+valeur%0A++++++++++++++de+x+et+de+y+n'est+donn%C3%A9+lors+de+l'appel+au+constructeur%0A++++++++%22%22%22%0A++++++++self.a+%3D+x+++%23+le+premier+%C3%A9l%C3%A9ment+de+la+paire%0A++++++++self.b+%3D+y+++%23+le+second+%C3%A9l%C3%A9ment+de+la+paire%0A%0Ap0+%3D+Pair()++++++%23%231%23%23%0Ap1+%3D+Pair(0,0)+++%23%232%23%23%0Ap2+%3D+Pair(1,1)+++%23%233%23%23%0Ap0.b+%3D+3+++++++++%23%234%23%23%0Ap1.a+%3D+10++++++++%23%235%23%23%0Ap2.a+%3D+p1.a++++++%23%236%23%23%0Ap2+%3D+p1++++++++++%23%237%23%23%0A+++&mode=display&raw_input=&curInstr=0) ou l'outil debug de Thonny pour visualiser ce qu'il se passe en mémoire ![](https://i.imgur.com/TY6PEyq.png) ## 2. Mise au point sur `self` #### A quoi sert-il ? * Utilisé en premier paramètre de toute méthode d'instance, il fait référence à l’instance (= l'objet) en cours, celui qui l'appelle. * Il permet par exemple de faire la différence entre de simples paramètres (`a` et `b`) et des variables d'instance (ou attributs) `self.a` et `self.b` ```python def __init__(self,a=0,b=0): self.a = a self.b = b ``` * Il permet de distinguer les fonctions, des méthodes d'instance: ```python class Student : """ NB. Description, Pre and Post should be completed """ def __init__(self,n) : self.name = n self.test1 = None self.test2 = None # UNE METHODE D'INSTANCE def average_score(self) : return (self.test1 + self.test2) / 2 # UNE FONCTION, en dehors de la définition de la classe def average_score_bis(student): return (student.test1 + student.test2) / 2 stud = Student("Kim") stud.test1, stud.test2 = 4, 10 res1 = stud.average_score() res2 = average_score_bis(stud) print(res1==res2) ``` * Dans le cas des listes, il en va de même entre `sort` et `sorted` ;-) #### Oui, c'est une convention entre développeurs * Le premier paramètre de toutes les méthodes est une instance, mais il n’a pas de nom obligatoire. * Ce code marche parfaitement : ```python class Pair: """ Une classe représentant une paire d'entiers """ def __init__(kreatur,a=0,b=0): kreatur.a = a kreatur.b = b def __str__(dumbledore): return "{},{}".format(dumbledore.a,dumbledore.b) p = Pair() print(p) ``` * Il ne passera probablement pas une peer code review, mais il est valide. > ### Mais soyez conventionnels, utilisez `self` :wink: ## 3. Les méthodes spéciales #### Sont une convention vis à vis de Python! * Les méthodes spéciales (ou "magiques") prennent la forme `__methodespeciale__` en Python * Les fonctionnalités "spéciales" peuvent être de nature diverse et agissent souvent de manière invisible : * `__file__` indique l'adresse d'un fichier Python * `__eq__` est exécuté quand l'expression `a == b` est exécutée * `__str__` est exécuté quand `print()` est exécuté * `__init__` est exécuté à la construction d'un objet, pour initialiser ses attributs, e.g. quand `s = Student("Kim")` est exécuté * Dans le code suivant, qu'est-ce qui est affiché à la console ? ;-) ```python class Pair: """ Une classe représentant une paire d'entiers """ def init(self, a=42, b=42): self.a = a self.b = b def __init__(kreatur,a=0,b=0): kreatur.a = a kreatur.b = b def str(dumbledore): print("{},{}".format(dumbledore.a,dumbledore.b)) p = Pair() print(p) print(p.a) ```