Classes ======= Classes are just another (more advanced) way to store data. The good thing about them is that they can store a lot of information within only one variable (like :ref:`lists:Lists` or :ref:`dictionaries:Dictionaries`). The advantages are it is easier to access the data and classes can also define custom functions (called methods) which are sometimes refered to as behaviours that each object will possess. More on :ref:`methods<classes:Instance method>` later. Classes vs. objects ------------------- The most common confusion is the distinction between a class and an object. They are intimately tied together, however they are not the same thing. - **Class**: the *general* thing. e.g., "Human" - In our programs, we get to define what a Human is. We get to say, "every human, in general, will have attributes like eye colour, height, weight, and a name". - For every Human object we create, we want to store that same information for all of them. - We can also define a set of behaviours (functions called methods) that every Human object we create will be able to perform. - **Object**: A *specific* instance of a thing. e.g., "Jeff" - In our program we get to create Humans and modify (set) their attributes according to their specific eye colour, height, weight and name. Jeff (object) **is a** Human (class) Attributes and Behaviours ------------------------- Classes are defined to give their objects *attributes* and *behaviours*. Object Attributes ^^^^^^^^^^^^^^^^^ These are really just *variables* stored for each instance of a class (each object). We can also call these attributes *fields*. So for a particular object, the specific information we store about it like age and eye-color, is called: - an attribute - a variable - a field You can use these terms interchangably. Object Behaviours ^^^^^^^^^^^^^^^^^ Every object of a particular class is given abilities or "behaviours". These are called "methods". Methods are just functions that belong to every instance (object) of a class. Here are some examples of some methods:: p = Person("Jeff", 56) p.jump() p.laugh() p.throw(ball) Those could all be "behaviours" that the ``Person`` named Jeff can do by virtue of being a ``Person`` object, if we decided to create those methods in the class. Unified Modeling Language (UML) ------------------------------- :: User # Name of the class ---- name: str # list the attributes first password: str email: str ---- post(message: str) -> None # behaviours (methods) .. image:: https://online.visual-paradigm.com/images/features/main/01-online-class-diagram-example.png __init__ method --------------- The init method is what is called when you create an object. It will attach values and data to a single object using the ``self`` pointer. .. code-block:: python class User: def __init__(self, username, password, email): self.username = username self.password = password self.email = email Accessing Object Attributes --------------------------- .. code-block:: python user1 = User("EraserMan", "password123", "blah@gmail.com") print(user1.username) print(user1.password) print(user1.email) user1.username = "MrBlah" print() print(user1.username) print(user1.password) print(user1.email) :: Output: EraserMan password123 blah@gmail.com MrBlah password123 blah@gmail.com **Another example:** .. code-block:: python class Person: def __init__(self, name: str, age: int): self.name = name self.age = age jeff = Person("Jeff", 35) print(f"This is {jeff.name}, they are {jeff.age} years old!") Object pointers --------------- For **primitive datatypes**, literal values are stored in variables. .. code-block:: python a = 5 b = a # COPY the VALUE of `a` and store it in `b` print(a) # 5 print(b) # 5 a += 3 print(a) # 8 print(b) # 5 For, **objects** the memory location (pointer) is stored in variables. .. code-block:: python class Person: pass a = Person() # create a person object, store the pointer in variable `a` a.name = "Frank" print(a.name) # "Frank" b = a # Copies the POINTER, not the data print(b.name) # "Frank" print(a) # <__main__.Person object at 0x0000016E2BE44550> print(b) # <__main__.Person object at 0x0000016E2BE44550> (same memory location) a.name = "Sally" print(a.name) # "Sally" print(b.name) # "Sally" Both variables ``a`` and ``b`` *point* to the same object. Loop through a list of objects ------------------------------ .. code-block:: python class Person: def __init__(self, name: str, age: int): self.name = name self.age = age # --- OK --- # Store each person object in a vriable, then add them to a list. # This has its limitations. jeff = Person("Jeff", 35) sally = Person("Sally", 30) ben = Person("Ben", 22) people = [jeff, sally, ben] # --- BETTER --- # Create the person objects directly in a list people = [Person("Jeff", 35), Person("Sally", 30), Person("Ben", 22)] for p in people: print(f"This is {p.name}, they are {p.age} years old!") Instance method --------------- Each instance (object) of a class (with instance methods) will have certain "behaviours" or actions that they will be able to perform. This is different from using a normal function *on* an object. With instance methods, we think of the object *itself* performing the action. .. code-block:: python p = Person("Jeff") # Fundtion: THe object being acted UPON greeting = get_introduction(p) # Method: The object ITESELF performing the action greeting = p.get_introduction() The end result is the same, it's just a matter of organizing our code. The function will be *outside* the class, where the method will be contained *within* the class. It may be best to write all functions related as methods within the class itself. This helps keep all related code in the same place. Two examples of instance methods. One without parameters, one with. .. code-block:: python class Person: def __init__(self, name, height, strength): self.name = name def introduce(self): print(f"Hello, my name is {self.name}.") def complement(self, person): print(f"{person.name}, I love your hair!") # Usage p1 = Person("Jeff") p2 = Person("Sally") p1.introduce() p2.complement(p1) # output: # "Hello, my name is Jeff." # "Jeff, I love your hair!" .. note:: Technically *all* instance methods have at least one parameter. The ``self`` pointer. In Python, we must explicitly define each instance method to include it. Encapsulation ------------- .. warning:: Encapsulation section under construction. Proceed at your own risk. What is an API? ^^^^^^^^^^^^^^^ At some point in your career you will create code, not for a general end-user, but for **other software developers**. Imagine you love coding in Python but you wish you could write 2D games. There are no easy ways to do it, but you set out to create to create a clone of `space invaders <https://en.wikipedia.org/wiki/Space_Invaders>`_. You notice some common patterns like drawing circles and rectangles and create some functions to be able to draw a rectangle with ease. `Pygame is born <https://www.pygame.org/>`_. As time goes on, you add more features and functions that make creating a game as easy as possible. You upload your source code to `GitHub <https://github.com>`_, and now hundreds of people are using the code you wrote. All the functions that people are using to draw shapes, create sprites, and perform collision detection **IS** the API. The API hides all the complicated and unsightly details of drawing a polygon. If you wanted to draw a polygon, it would require roughly `30 lines of code <https://github.com/pygame/pygame/blob/6a69cddc4f7cd94e81dd83d15f452471c44a090c/src_py/draw_py.py#L529>`_. The API allowes people to simply use ``pygame.draw.polygon(surface, color, points, width)`` when they want to draw a polygon. What makes a good API? ^^^^^^^^^^^^^^^^^^^^^^ A good API: - hides complex code and complex processes from the developer. - minimizes the number of lines of code someone needs to write to accomplish something. - ensures the code looks as close to english as possible. Building an API ^^^^^^^^^^^^^^^ Pick a particular **thing** that operates within your program and bring yourself through the following steps. For illustrative purposes, we will consider a simple ``WaterBottle``. State """"" - What about the object *can* change? - What about the object *cannot* change? Every ``WaterBottle`` will have the following attributes. .. code-block:: text capacity: int contents: int **Capicity** will refer to the maximum volume the bottle can hold and **contents** refers to what volume it is *currently* holding. For example, a bottle can have a maximum capacity of ``350 mL``, but only currently holding ``90 mL`` of water. Some attributes throughout the life of the bottle can be changed (``contents``), and somethings should not be (``capacity``). At the end of the day, the developer makes the decision concerning what attributes can change or not. Behaviours """""""""" - What does the object do? - What can be done to the object? - How do these behaviours modify or alter the object's state? - Do any of these behaviours depend on outside information? - Do they return a value? Our ``WaterBottle`` will need the ability to be filled up and poured out. Those can be made into methods. .. code-block:: text fill() pour() How would these abilities alter or modify the bottle's state? They would change the current capacity. Do these methods require outside information? Yes, we need to know how much to fill and how much to pour out. .. code-block:: text fill(amount: int) pour(amount: int) Do these behaviours return anything? Perhaps. If you wanted to, you could pour out some water into a cup. .. code-block:: python # hypothetical, speculative code bottle = Bottle() cup = Cup() cup.contents = bottle.pour(50) # or cup.fill(bottle.pour(50)) Verification """""""""""" - How is someone supposed to use your code? - Come up with a few lines of code showing how to interact with the API. - Create an object - Use some methods - What is the result supposed to be? - Do you like the way the code looks? Is is easy to use and remember? - If you need to modify the API, do so. .. note:: older stuff below. When writing code professionally, one person may write a class or a function and many other people will use it. This is why we need to document our code properly. Additionally, the person who creates the class (or function) is the one who knows about all the little behind-the-scenes details of how things work. They will also know certain methods can only work with a specific set of data. Let's imagine we have a ``Person`` class with a ``name`` and an ``age``:: class Person: def __init__(self, name, age): self.name = name self.age = age def greet(self): return f"Hello, I am {self.name} and I'm {self.age} years old." def can_vote(self): return self.age >= 18 Once we have a ``Person`` object created, whoever uses our ``Person`` class will be able to change the age to whatever they want. They can change it to something silly at best or break the program at worst. .. code-block:: python :linenos: :emphasize-lines: 6 person = Person("Johnny", 34) person.name # "Johnny" person.age # 34 person.greet() # "Hello, I am Johnny and I'm 34 years old." person.age = "Blah Blah Blah" # silly person.greet() # "Hello, I am Johnny and I'm Blah Blah Blah years old." # broken person.can_vote() # TypeError!!!!!!! The problem is **NOT** with the ``can_vote()`` method which was called on line ``12``. The problem is that the data for ``age`` was allowed to be changed to something *invalid* in the first place (line ``6``). For Java, this specific problem would not happen, but, other bad data can sneak in. For example, if we try to set the age to a negative number, this would not be valid data for someone's age. So even in Java, we would need to **encapsulate** the attribute to ensure it never is allowed to be given something invalid. So how do we fix this? There are three steps: - Make the attribute private - Make a getter and setter method - In the setter method, validate the data Making attributes private ^^^^^^^^^^^^^^^^^^^^^^^^^ The first thing to do is "hide" the atribute or make it **private**. In Python, we simply underscore the object attribute's name: .. code-block:: python :emphasize-lines: 4 class Person: def __init__(self, name, age): self.name = name self._age = age # The internal methods that use the age attribute # need to be modified because we put an underscore. def greet(self): return f"Hello, I am {self.name} and I'm {self._age} years old." def can_vote(self): return self._age >= 18 In Python **nothing** is truly ever private, be we can do things to indicate to other programmers that they should not touch the attribute without potential negative consequences. In Java, declaring a fieid "private" absoultely hides it from other programmers. Make getters and setters ^^^^^^^^^^^^^^^^^^^^^^^^ Once we have "hidden" the attribute, we need to provide other programmers access to it with an intermediary methods called **setters** and **getters**. Setters "set" the attribute value for the programmer, and Getters fetch and return the attribute value to the programmer. - All *setters* start with ``set_`` - All *getters* start with ``get_`` .. code-block:: python :emphasize-lines: 6, 9 class Person: def __init__(self, name, age): self.name = name self._age = age def get_age(self): return self._age def set_age(self, new_value): self._age = new_value Now we can access the attribute by using the following methods: .. code-block:: python :emphasize-lines: 3,6 person = Person("Johnny", 34) person.name # "Johnny" person.get_age() # 34 person.greet() # "Hello, I am Johnny and I'm 34 years old." person.set_age("Blah Blah Blah") # silly person.greet() # "Hello, I am Johnny and I'm Blah Blah Blah years old." # broken person.can_vote() # TypeError STILL!!!!!!! As you can see, we provided encapsulation but we still haven't ensured that only *valid* data is saved to the attribute. Provide data validation ^^^^^^^^^^^^^^^^^^^^^^^ So the problem is now that the ``.set_age()`` method allowed the age to be set to an *invalid* value. We can fix that: .. code-block:: python :emphasize-lines: 7 class Person: def __init__(self, name, age): self.name = name self._age = age def set_age(self, new_value): assert new_value >= 0, "The age value cannot be negative." self._age = new_value If we call the method with bad data like ``person.set_age(-20)``, the assertion fails, the program will break and will display:: AssertionError: The age value cannot be negative. Additionally if we call the method with a non integer (``person.set_age("twelve")``), the program will raise a ``TypeError`` for us:: TypeError: '>=' not supported between instances of 'str' and 'int' So you might be thinking, "So what? There's still is an error. Now it just happens sooner". The key distinction is now the error happens at the point of the *actual* error, which was setting the value to some invalid data. In reality, the ``.can_vote()`` method can possibly be called three months after the bad data was set. This is a code time-bomb ready to go off. If we ever write software for a bank or some other critical business, we can't afford to write code that will explode at some unforseen time or have undesired side-effects or consequences in the future. Aggregate class --------------- An *Aggregate class* is one which contains, or is made up of another class. The aggregate class contains components that are other classes. .. code-block:: python class Engine: def __init__(self, size: float): self.size = size class Car: """Aggregate class. It contains an Engine object.""" def __init__(self, color: str, engine_size: float): self.color = color self.engine = Engine(engine_size) car = Car("blue", 4.0) print(car.color) # "blue" print(car.engine.size) # 4.0 Class field (variable) ---------------------- .. code-block:: python :emphasize-lines: 5,11,20 from typing import List class Pizza: num_pizzas = 0 # class field (variable) def __init__(self, name: str, toppings: List[str]): self.name = name self.toppings = toppings self.id = Pizza.num_pizzas Pizza.num_pizzas += 1 # update the class field def __str__(self) -> str: return f"{self.name}, toppings: {self.toppings}, id: {self.id}" def main(): pepperoni = Pizza("Pepperoni", ["cheese", "pepperoni"]) print(pepperoni) print(Pizza.num_pizzas) # 1 if __name__ == "__main__": main() Class method ------------ .. code-block:: python :linenos: :emphasize-lines: 16,20,26,29 from typing import List class Pizza: num_pizzas = 0 def __init__(self, name: str, toppings: List[str]): self.name = name self.toppings = toppings self.id = Pizza.num_pizzas Pizza.num_pizzas += 1 def __str__(self) -> str: return f"{self.name}, toppings: {self.toppings}, id: {self.id}" @classmethod def pepperoni(cls): return cls("Pepperoni", ["cheese", "pepperoni"]) @classmethod def cheese(cls): return cls("Cheese", ["cheese"]) def main(): pepperoni = Pizza.pepperoni() print(pepperoni) cheese = Pizza.cheese() print(cheese) cheese_another = Pizza.cheese() print(cheese_another) print(Pizza.num_pizzas) if __name__ == "__main__": main() Inheritance ----------- .. code-block:: python :linenos: :emphasize-lines: 12,24,29 class Animal: def __init__(self, name: str): self.name = name def __str__(self): return self.name def make_sound(self): print("<generic animal sound>") class Dog(Animal): # Dog inherits from Animal def __init__(self, name: str, breed: str): super().__init__(name) self.breed = breed def __str__(self): return f"{self.name} the {self.breed}" def make_sound(self): print("Woof!") class Squirrel(Animal): # Squirrel inherits from Animal def make_sound(self): print("Squeek") class Cat(Animal): # Cat inherits from Animal pass def main(): d = Dog("Rover", "Dalmatian") s = Squirrel("Sammy") c = Cat("Bella") print(d) # Rover the Dalmatian print(s) # Sammy print(c) # Bella d.make_sound() # Woof! s.make_sound() # Squeek c.make_sound() # <generic animal sound> if __name__ == "__main__": main() Polymorphism ------------ *Polymorphism* is when a sub-class overrides an inherited method. .. code-block:: python :linenos: :emphasize-lines: 17,20,25 class Animal: def __init__(self, name: str): self.name = name def __str__(self): return self.name def make_sound(self): print("<generic animal sound>") class Dog(Animal): def __init__(self, name: str, breed: str): super().__init__(name) self.breed = breed def __str__(self): # overrides inherited method return f"{self.name} the {self.breed}" def make_sound(self): # overrides inherited method print("Woof!") class Squirrel(Animal): def make_sound(self): # overrides inherited method print("Squeek") class Cat(Animal): pass def main(): d = Dog("Rover", "Dalmatian") s = Squirrel("Sammy") c = Cat("Bella") print(d) # Rover the Dalmatian print(s) # Sammy print(c) # Bella d.make_sound() # Woof! s.make_sound() # Squeek c.make_sound() # <generic animal sound> if __name__ == "__main__": main() Refactor multiple classes ------------------------- Function/Method Overloading --------------------------- .. code-block:: python # Method overloading (Python version) # optional/default parameters # Key-word arguments # Allows us to create one function or method with different # ways to call it. def draw_rectangle(x, y, w=100, h=100, color=arcade.color.BLUE): pass draw_rectangle(10, 50) # draws rectangle at 50, 50 of width 100, height 100 draw_rectangle(100, 200, 5) draw_rectangle(500, 200, 50, 50) draw_rectangle(10, 10, 5, 5, arcade.color.WHITE) draw_rectangle(75, 10, color=arcade.color.WHITE) Docstrings (Classes) -------------------- For more information on Docstrings in general, check out :ref:`function docstrings <functions:docstrings>`. For classes, this is the way we will be wrirting our docstrings:: from typing import List, Optional class ExampleClass: """The summary line for a class docstring should fit on one line. If the class has public attributes, they may be documented here in an ``Attributes`` section and follow the same formatting as a function's ``Args`` section. Alternatively, attributes may be documented inline with the attribute's declaration (see __init__ method below). Attributes: attr1 (str): Description of `attr1`. attr2 (:obj:`int`, optional): Description of `attr2`. """ def __init__(self, param1: str, param2: Optional[int] = None, param3: List[str]): """Example of docstring on the __init__ method. The __init__ method may be documented in either the class level docstring, or as a docstring on the __init__ method itself. Either form is acceptable, but the two should not be mixed. Choose one convention to document the __init__ method and be consistent with it. Note: Do not include the `self` parameter in the ``Args`` section. Args: param1: Description of `param1`. param2 (optional): Description of `param2`. Multiple lines are supported. param3: Description of `param3`. """ self.attr1 = param1 self.attr2 = param2 self.attr3 = param3 def example_method(self, param1: int, param2: str) -> bool: """Class methods are similar to regular functions. Note: Do not include the `self` parameter in the ``Args`` section. Args: param1: The first parameter. param2: The second parameter. Returns: True if successful, False otherwise. """ return True