<jupyter_start><jupyter_text>08 Object Oriented Programming (OOP) CLASS MATERIAL 1. Classes 2. Methods  3. References 4. Inheritance 5. Static Variables vs. Instance Variables 6. Review Exercises  Lesson GoalCreate object types with associated variables and mehtods. Fundamental programming concepts - Writing and using classes - Pointers/references     1. Classes 1.1 Defining a Class 1.2 Anatomy of a Class  1.3 The Constructor  __Class__ : a “classification” of an object. e.g.“person” or “image.” __Object__ : a particular *instance* of a class. e.g. "Hemma" is an instance of “Person.” Objects have:- attributes- methods__Attributes__ : *Variables* that belong an object.e.g. person's name, height, and age. __Methods__ : *Functions* belong to an object i.e. actions that an object can do.e.g. run, jump, sit. Each object in a game needs data: a name, location, velocity ... Without classes, our Python code to store this data so far looks like a set of variables:```python 2.3 ballradius = 20 ball_vel = [0,0]ball_pos = [0,0] 2.4 paddlespad_width = 40pad_height = 120pad1_vel = [0,0]pad2_vel = [0,0]pad1_pos = [0,10]                       pad2_pos = [20,100] pad_pos = [pad1_pos, pad2_pos]pad_vel = [pad1_vel, pad2_vel] 2.5 scoresl_score = 0r_score = 0 ``` To use these variables to create an object, they are passed to a function, for example:```pythonpygame.draw.circle(window, red, (int(ball_pos[x]), int(ball_pos[y])), radius)pygame.draw.rect(window, white, pygame.Rect(int(pad1_pos[x]), int(pad1_pos[y]), pad_width, pad_height))pygame.draw.rect(window, white, pygame.Rect(int(pad2_pos[x]), int(pad2_pos[y]), pad_width, pad_height))``` If a program that has a set of variables like this for each shape / character / monster etc, then we can see that as the number of the objects in the game increases, the amount of data makes the code very long and very confusing.  The code can also become very repetitive. For example, a certain type of character, e.g. monsters might all share the same value of `max_speed` for example.   In a large video game, there may be hundreds of functions that deal with the main character. Adding a new variable to the character could potentially require the developer to go through *every* function and add it to the list of arguments. That would be a lot of work! Classes provide a better way to package data fields into *object types* so they can be managed easily.As such, this is called Object Oriented Programming (OOP).  1.1 Defining a ClassExample : Define a class representing the paddles in the Pong game: ```pythonclass Paddle():    """ This is a class that represents rectangular paddles. """    def __init__(self):        """ This method sets up the class attributes. """        self.name = "Paddle"        self.width = 40        self.height = 120        self.vel = [0, 0]        self.pos = [0, win_height//2 - pad_height//2]        ``` Here's another example, we define a class to hold all the fields for an address:  1.2 Anatomy of a ClassUnlike functions and variables, class names __should__ begin with an upper case letter. While it is possible to begin a class with a lower case letter, it is not considered good practice. The class attributes are set using a `.` dot. `self.` behaves like the pronoun 'my' or 自分の. Inside the class, we are talking about my name (自分の名前), my city (自分の都市), etc so we use `self.`Outside of the class, we don't use `self.` because just like the pronoun “my,” it means someone totally different when said by someone else.<jupyter_code>class Address():
    """ Hold all the fields for a mailing address. """
    def __init__(self):
        """ This method sets up the class attributes. """
        self.name = None
        self.line1 = None
        self.line2 = None
        self.ward = None
        self.city = None
        self.code = None<jupyter_output><empty_output><jupyter_text>1.3 The Constructor `def __init__(self)` : a special function called a constructor that is run automatically when a class object is created.- It must be named __`init`__- It must be called on the second line of the class.- It must have __two__ underscores before it *and* after it. - It must take `self` as an input parameter.  The class definition does not create an *instance* of the class. Just like defining a function does not call the function. e.g. We don't actually have an address object in the program yet.  Look at the example below:<jupyter_code># Create an address
office_address = Address()<jupyter_output><empty_output><jupyter_text>The class attributes are set using a `.` dot. `self.` behaves like the pronoun 'my' or 自分の. Outside of the class, we don't use `self.` because just like the pronoun “my,” it means someone totally different when said by someone else.<jupyter_code># Set the parameters OUTSIDE of the class
office_address.name = "<NAME>"
office_address.line1 = "Mechatronics Lab, Room d1S12"
office_address.line2 = "C Cluster, Kyotodaigaku Katsura,"
office_address.ward = "Nishikyoku"
office_address.city = "Kyoto"
office_address.code = "615-8410"

print(office_address.name)<jupyter_output><NAME><jupyter_text>An *instance* of the address class is created in line 2: the class Address name followed by parentheses. The __variable__ `office_address` is a *reference* to the `Address` class object that has just been created. The object name can be anything that follows normal naming rules (cannot begin with a number etc...).Will will discus this in greater depth later in the class.<jupyter_code>home_address = Address()  # Create an address
 
name = "<NAME>" # This does not set the address's name!
print(home_address.name)
Address.name = "<NAME>" # This does not set the address's name!
print(home_address.name)
home_address.name = "<NAME>" # This sets the address's name.
print(home_address.name)
print(office_address.name)
print(f"Address personal mail to {home_address.name}" )
print(f"Address official mail to {office_address.name}")<jupyter_output>Address personal mail to <NAME>
Address official mail to Dr Philamore<jupyter_text>Storing lots of data in a class makes it easy to pass the data into and out of a function.<jupyter_code># Print an address to the screen
def print_address(address):
    print(address.name)
    
    # If there is a line1 in the address, print it
    if address.line1:
        print(address.line1)
        
    # If there is a line2 in the address, print it
    if address.line2:
        print( address.line2 )
        
    print(f"city : {address.city}")

print_address(home_address)
print()
print_address(office_address)<jupyter_output><NAME>
city : None

Dr Philamore
Mechatronics Lab, Room d1S12
C Cluster, Kyotodaigaku Katsura,
city : Kyoto<jupyter_text>2. MethodsA method is a function that exists *inside* of a class. Consider a function:```pythondef move_pads(up, down, vel):    """ Moves the paddles when keyboard keys are pressed """    if pressed[ up ] & pressed[ down ]:         vel[y] = 0    elif pressed[ up ]:         vel[y] = -8    elif pressed[ down ]:         vel[y] = 8    else:         vel[y] = 0        ```__Inputs__ : up key, down key, paddle velocity. Expanding the earlier example of a Paddle class:```pythonclass Paddle():    """ This is a class that represents rectangular paddles. """    def __init__(self):        """ This method sets up the class attributes. """        self.name = "Paddle"        self.width = 40        self.height = 120        self.vel = [0, 0]        self.pos = [0, win_height//2 - self.height]        self.up = None        self.down = None            def move_pad(self):    """ Moves the paddles when keyboard keys are pressed """        if pressed[ self.up ] & pressed[ self.down = None ]:             self.vel[y] = 0        elif pressed[ self.up ]:             self.vel[y] = -8        elif pressed[ self.down ]:             self.vel[y] = 8        else:             self.vel[y] = 0        ``` Method definitions in a class look almost exactly like function definitions. The key difference is that the first input parameter is *always* `self`.  This parameter is required even if it is not used in the function. Notice also the addition of `self` before any parameter that is a __class attribute__. `x`, `y` and `win_height` are global variables.  Important points when creating methods for classes:- Attributes should be listed first, followed by methods.  - The first parameter of any method must be `self`.- Method definitions are indented by one tab space.  Methods may be called in a manner similar to referencing attributes from an object.Note that even through the method `move_pad` has one parameter, `self`, the call does not pass any arguments.We do not need to pass `self` when calling a method. ```python  create a paddle objectpaddle1 = Paddle() set the keys to control up and down motion of the paddlepaddle1.up = pygame.K_UPpaddle1.down = pygame.K_DOWN change the paddle velocity according to if the up or down key has been pressedpaddle1.move_pad()``` All other parameters (with the exception of default arguments), must be passed to the method.<jupyter_code>class MyAddress():
    """ Hold all the fields for a mailing address. """
    def __init__(self):
        """ This method sets up the class attributes. """
        self.name = "<NAME>"
        self.line1 = "Mechatronics Lab, Room d1S12"
        self.line2 = "C Cluster, Kyotodaigaku Katsura"
        self.ward = "Nishikyoku"
        self.city = "Kyoto"
        self.code = "615-8410"
        
    def format_print(self, delimiter='\n'):
        print(self.name + delimiter + 
              self.line1 + delimiter + 
              self.line2 + delimiter + 
              self.ward + delimiter + 
              self.city + delimiter + 
              self.code)
my_address = MyAddress()

my_address.format_print()

print()

my_address.format_print(',')<jupyter_output>Dr Philamore
Mechatronics Lab, Room d1S12
C Cluster, Kyotodaigaku Katsura
Nishikyoku
Kyoto
615-8410

Dr Philamore,Mechatronics Lab, Room d1S12,C Cluster, Kyotodaigaku Katsura,Nishikyoku,Kyoto,615-8410<jupyter_text>The constructor can also take input parameters. These input parameters are used to initialise the class:<jupyter_code>class MyAddress():
    """ Hold all the fields for a mailing address. """
    def __init__(self, new_name):
        """ This method sets up the class attributes. """
        self.name = new_name
        self.line1 = "Mechatronics Lab, Room d1S12"
        self.line2 = "C Cluster, Kyotodaigaku Katsura"
        self.ward = "Nishikyoku"
        self.city = "Kyoto"
        self.code = "615-8410"
# This creates an address
my_address = MyAddress("Hemma")
 
# Print the name to verify it was set
print(my_address.name)
 
# This line will give an error because
# a name is not passed in.
her_address = MyAddress()<jupyter_output>Hemma<jupyter_text>3. References 3.1 Functions and ReferencesRecall, we create an *instance* of a class using the class name followed by parentheses. e.g. `my_address = MyAddress()`The variable `my_address` is a __reference__ to the `Address` class object that has just been created. What does this mean in a practical sense?  Conside the example below:<jupyter_code>class Student():
    def __init__(self):
        self.name = ""
        self.score = 0
# create a reference to a Student object
farhad = Student()
farhad.name = "Farhad"
farhad.score = 100

# create a reference to ANOTHER Student object
ayako = Student()
ayako.name = "Ayako"
 
print(farhad.name, "has", farhad.score, "points.")
print(ayako.name, "has", ayako.score, "points.")<jupyter_output>Farhad has 100 points.
Ayako has 0 points.<jupyter_text>Nothing unusual here. The example below is almost the same, but has an important difference:<jupyter_code># create a reference to a Student object
farhad = Student()
farhad.name = "Farhad"
farhad.score = 100

# create a reference to THE SAME Student object
ayako = farhad
ayako.name = "Ayako"
 
print(farhad.name, "has", farhad.score, "points.")
print(ayako.name, "has", ayako.score, "points.")<jupyter_output>Ayako has 100 points.
Ayako has 100 points.<jupyter_text>A common error when working with objects is to assume that the variable bob *is* the Person object. This is not the case. The variable `farhad` is a __reference__ to the `Student` object. The variable `ayako` copies the reference, so is a reference to __the same__ `Student` object.  The reference stores the *memory address* of where the object is, and *not* the object itself.The address refers to the place in computer memory for where the object is stored. This address is a hexadecimal number which, if printed out, might look something like 0x1e504.The reference is also known as an *address*, *pointer* or *handle*.  If `farhad` actually was the object, then:-  `ayako = farhad` would create a copy of the `farhad` object and there would be two objects. - The output of the program would show `Farhad` and `Ayako` having 100 dollars *not* `Ayako` and `Ayako` having 100 dollars.   3.1 Functions and References Look at the example below. Line 1 creates a function that takes a number as an input parameter. `score` is a variable that contains a copy of the data that was passed in. Adding 100 to that number does not change the number stored in `farhad.score`.<jupyter_code>def add_points1(score):
    score += 100
    
add_points1(farhad.score)

print(farhad.score)<jupyter_output>100<jupyter_text>The print therefore prints `100`, not `200`.A local variable `score` is created within `add_points1`. Alternatively, the code below __does__ increase `farhad.score`<jupyter_code>def add_points2(student):
    student.score += 100
    
add_points2(farhad)

print(farhad.score)<jupyter_output>200<jupyter_text>What does this code do?The parameter `student` contains a *copy* of the memory address of the object, *not* the actual object. Data structures (lists) work the same way. A function that takes in a list as a parameter can modify elements of the list using the list index. The index is *reference/pointer* to the element.<jupyter_code>def change_my_list(my_list):
    """ Add 2 to each item in the list """
    for i in list(range(len(my_list))):
        my_list[i]+=2
        
a_list = [0, 1, 2]

change_my_list(a_list)

print(a_list)<jupyter_output>[2, 3, 4]<jupyter_text>4. Inheritance 4.1 `super` 4.2 OverridingIt is possible to create a class and inherit all of the attributes and methods of a parent class.For example, a class called `Person` could have all the attributes needed to represent a person in a game.<jupyter_code>class Person():
    def __init__(self):
        self.name = ""
        self.age = None
        self.is_sitting = True
 
    def sit(self):
        if self.is_sitting:
            print("You are already sitting.")
        else:
            self.is_sitting = True
            print("Sitting down")
            
    def stand(self):
        if not self.is_sitting:
            print("You are already standing.")
        else:
            self.is_sitting = False
            print("Standing up")
p = Person()
p.stand()
p.stand()
p.sit()
p.sit()<jupyter_output>Standing up
You are already standing.
Sitting down
You are already sitting.<jupyter_text>Now let's create a student. A student can do everything a person can do, *and additionally* a student can study. Without inheritance we have two options:- add the `study()` method to a `Person` object. This can confuse things as not all people study. - create a copy of the `Person` class and call it `Student`. We add the `study()` method to the `Student` class. This can cause problems if we make changes to the `Person` class. A developer would need to remember that they need to change not only the `Person` class, but also the `Student` class. Maintaining this synchronicity is time consuming and error-prone. Inheritance allows us to create child classes that __inherit__ all attributes and methods of the parent class. We can then add attributes and methods that are specific to the child class. If the parent class is changed, the child class will automatically inherit the changes.<jupyter_code>class Student(Person):            
    def study(self):
        print("You are studying.")
student = Student()

student.study()

student.sit()<jupyter_output>You are studying.
You are already sitting.<jupyter_text>`student` has inherited all attributes and methods from the `Person` class. The method `study` was added to `Student`. But what if we have a method in both the child and parent class?For example, to add initialisation variables to the child class we need an `__init__` methid.   4.1 `super`Using `super()` followed by a dot `.` and then a method name allows you to call the parent's version of the method.If you are writing a method for a child and want to call a parent method, normally it will be the first statement in the child method.Let's look at some examples: `Employee` calls the `__init__` function from parent class `Person` and adds the attribute, `job_title`:<jupyter_code>class Employee(Person):
    def __init__(self):
        # Call the parent/super class constructor first
        super().__init__()
 
        # Variables specific to the child class
        self.job_title = ""<jupyter_output><empty_output><jupyter_text>`Customer`:- calls the `__init__` function from parent class `Person` and adds the attribute, `email`.- calls the `sit` function from parent class `Person` and adds a print statement.<jupyter_code>class Customer(Person):
    def __init__(self):
        super().__init__()
        self.email = ""
 
    def stand(self):
        # Run the parent sit:
        super().stand()
        # Add more actions to the end 
        print("Customer e-mail:", self.email)<jupyter_output><empty_output><jupyter_text>4.2 OverridingMethods can be overridden by a child class to achive different functionality.<jupyter_code>class Baby(Person):
    def __init__(self):
        # Call the parent/super class constructor first
        super().__init__()
 
    def stand(self):
        # Here we override stand and do something else
        print(f"Well done baby {self.name}, you learned to stand!")
john = Person()
john.name = "<NAME>"
 
sam = Employee()
sam.name = "<NAME>"
sam.job_title = "Web Developer"
 
louis = Customer()
louis.name = "<NAME>"
louis.email = "<EMAIL>"

mia = Baby()
mia.name = "<NAME>"
john.stand() # Person
sam.stand() # Employee
louis.stand() # Customer
mia.stand() # Baby<jupyter_output>Well done baby Baby Mia, you learned to stand!<jupyter_text>5. Static Variables vs. Instance VariablesThe difference between static and instance variables can be confusing. __Instance Variable__ : The type of class variable we've used so far. Each instance of the class gets its own value. For example, each person has their own age. With instance variables, we can't just use the variable `age`. We need to specify whose age we are talking about. Also, if there are no people, then referring to an age will not make sense.__Static Variable__ : The value is the same for every single instance of the class. Even if there are no instances, there still is a value for a static variable. For example, we could have a `count` static variable for the number of `Person` objects in existence. If there are no people, this value is zero, but it still exists.<jupyter_code># Example of an instance variable
class ClassA():
    def __init__(self):
        self.y = 3
 
# Example of a static variable
class ClassB():
    x = 7
 
# Create class instances
a = ClassA()
b = ClassB()
 
# Two ways to print the static variable.
# The second way is the proper way to do it.
print(b.x)
print(ClassB.x)
 
# One way to print an instance variable.
# The second generates an error, because we don't know what instance
# to reference.
print(a.y)
print(ClassA.y)<jupyter_output>7
7
3<jupyter_text>It is possible to have a static variable, and an instance variable with the same name.<jupyter_code># Class with a static variable
class ClassB():
    x = 7
 
# Create a class instance
b = ClassB()
 
# This prints 7
print(b.x)
 
# This also prints 7
print(ClassB.x)
# Set x to a new value using the class name
ClassB.x = 8
 
# This also prints 8
print(b.x)
 
# This prints 8
print(ClassB.x)
# Set x to a new value using the instance b.
# It creates a new attribute instance, x. 
# The static variable is also called x.
# But they are two different variables. 
b.x = 9
 
# This prints 9
print(b.x)
 

# This prints 8. NOT 9!!!
print(ClassB.x)<jupyter_output><empty_output><jupyter_text>Summary- __Class__ : a “classification” of an object. e.g.“person” or “image.” - __Object__ : a particular *instance* of a class. e.g. "Hemma" is an instance of “Person.”- __Attributes__ : *Variables* that belong an object.e.g. person's name, height, and age. - __Methods__ : *Functions* belong to an object i.e. actions that an object can do.e.g. run, jump, sit.- Class names should begin with a capital letter e.g. Fish, Person- Every class should have a constructor method called `__init__` to set up the class attributes. - Inheritance allows us to create child classes that __inherit__ all attributes and methods of the parent class. - Using `super()` followed by a dot `.` and then a method name allows you to call the parent's version of the method.- If `super` is not used, the parent methid can be overwritten with a new methid of the same name. - __Instance Variable__ : Each instance of the class gets its own value. - __Static Variable__ : The value is the same for every single instance of the class.   6. Review ExercisesCompete the exercises below.Save your answers as .py files and email them to:<EMAIL>  Review Exercise 1: An instance of a classIn the cell below, write code to create an instance of this class and set its attributes.<jupyter_code># Review Exercise 1: An instance of a class
class Cat():
    def __init__(self):
        self.age = None
        self.name = None
        self.weight = None<jupyter_output><empty_output><jupyter_text>Review Exercise 2: Multiple objects Write code to create two different instances of this class and set attributes for both objects. *Hint: While a phone number is a number BUT it should be stored as string to keep leading zeros and dashes*.<jupyter_code># Review Exercise 2: Multiple objects 
class Person():
    def __init__(self):
        self.name = ""
        self.cell_phone = ""
        self.email = ""<jupyter_output><empty_output><jupyter_text>Review Exercise 3: Write a ClassFor the code below, write a class that has the appropriate class name and attributes that will allow the code to work.<jupyter_code># Review Exercise 3: Write a Class
my_fish = Fish()
my_fish.color = "green"
my_fish.name = "Eric"
my_fish.breed = "Saba"<jupyter_output><empty_output><jupyter_text>Review Exercise 4: Write your own ClassIn the cell below, define a class to represent a character in a simple game. Include attributes for:- position- name- strength<jupyter_code># Review Exercise 4: Write your own Class<jupyter_output><empty_output><jupyter_text>Review Exercise 5: Correct the ErrorsFind and correct the errors in the code below:a) The code should give the person `hemma` 100 units of money.b) The code should print `Farhad has 0 yen`.<jupyter_code># Review Exercise 5a: Correct the Errors
class Person():
    def __init__(self):
        self.name = ""
        self.money = 0
 
hemma = Person()
name = "Hemma"
money = 100
print(hemma.money)
# Review Exercise 5b: Correct the Errors
class Human():
    def __init__(self):
        self.name = ""
        self.money = 0
 
farhad = Human()
print(farhad.name, "has", money, "yen")<jupyter_output>has 100 yen
{'fraction_non_alphanumeric': 0.0864899257688229, 'fraction_numerical': 0.01200424178154825, 'mean_word_length': 3.743661971830986, 'pattern_counts': {'":': 0, '<': 96, '<?xml version=': 0, '>': 96, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '24', 'dir': 'jupyter-structured-clean-dedup', 'id': '22784', 'license': 'MIT', 'n_tokens_mistral': 7103, 'n_tokens_neox': 6715, 'n_words': 3013, 'path': '.ipynb_checkpoints/08_OOP__ClassMaterial-checkpoint.ipynb', 'repo_name': 'hphilamore/ILAS_PyEv2019', 'size': '23686'}