<jupyter_start><jupyter_text>Chapter 04: Object Oriented ProgrammingThis lab sheet introduces object oriented programming. We have been using object oriented programming throughout the previous lab sheets. Here we will learn how to create our own objects (what this means become clear).  TutorialWork through the following:--- 1. Writing a class.[A video describing the concept.](https://youtu.be/-58By9KGWX4)[A video demo.](https://youtu.be/kcGzJ52MZNU)The main idea behind object orientated programming (OOP) is tocreate abstract structures that allow us to not worry about data. <NAME>came up with the concept and is quoted as saying: ‘I wanted to get rid ofdata’. Instead of keeping track of variables using lists and arrays andwriting specific functions for each operation we could be trying to do we usea system similar to the cellular structure in biology:![Biology](/{{root}}/img/biology.png)Here is an image showing the various things we will consider in this labsheet:![Classes](/{{root}}/img/classes.png)Creating a class in Python is similar to creating a function: we write downthe rules:<jupyter_code>class Student:
    """A base class"""<jupyter_output><empty_output><jupyter_text>We can then create 'instances' of this class:<jupyter_code>vince = Student()
zoe = Student()
vince
zoe<jupyter_output><empty_output><jupyter_text>The `at ...` is a pointer to the location of the instance in memory. If youre run the code that location will change.We have already seen examples of classes in Python:- Integers;- Strings;- Lists.There are many more and we are going to see how to build our own.**Experiment with building an instance of the Student class.** 2. Attributes[A video describing the concept.](https://youtu.be/TDErnRv0SP8)[A video demo.](https://youtu.be/c05rLmFf-Xk)The above student class is not very useful. We now see how to make ourobjects ‘hold’ information. The following code re-creates our previousstudent class but gives the class some 'attributes':<jupyter_code>class Student:
    courses = ['Biology', 'Mathematics', 'English']
    age = 12
    gender = "Female"<jupyter_output><empty_output><jupyter_text>Now our class itself has some information:<jupyter_code>Student.age<jupyter_output><empty_output><jupyter_text>This information is also passed on to any instances of the class:<jupyter_code>vince = Student()
vince.courses
vince.age
vince.gender<jupyter_output><empty_output><jupyter_text>We can use and/or modify those attributes just like any other Python variable:<jupyter_code>vince.age += 1
vince.age
vince.gender = 'Male'
vince.gender
vince.courses.append('Chemistry')
vince.courses<jupyter_output><empty_output><jupyter_text>**Create instances with attributes and experiment with them.**  3. Methods[A video describing the concept.](https://youtu.be/dKS18fQVPxY)[A video demo.](https://youtu.be/6AGuxUC2LUE)We will now see how to make classes 'do' things. These are called 'methods' and they are just functions 'attached' to classes.<jupyter_code>class Student:
    """A class to represent a student"""
    courses = ['Biology', 'Mathematics', 'English']
    age = 12
    gender = "Female"
    def have_a_birthday(self, years=1):
        """Increment the age"""
        self.age += years  # self corresponds to the instance
vince = Student()
vince.have_a_birthday()
vince.age
vince.have_a_birthday(years=10)
vince.age<jupyter_output><empty_output><jupyter_text>There are various 'special' methods names that act in particular ways.One of these is `__init__`, this method is called when an instance iscreated ('initialised'):<jupyter_code>class Student:
    """A class to represent a student"""
    def __init__(self, courses, age, gender):
        self.courses = courses
        self.age = age
        self.gender = gender
    def have_a_birthday(self, years=1):
        """Increment the age"""
        self.age += years  # self corresponds to the instance<jupyter_output><empty_output><jupyter_text>Now we can easily create instances with given attributes:<jupyter_code>vince = Student(["Maths"], 32, "Male")
zoe = Student(["Biology"], 31, "Female")
vince.courses, vince.age, vince.gender
zoe.courses, zoe.age, zoe.gender<jupyter_output><empty_output><jupyter_text>There are various other 'special' methods, we will see one of them in the worked example.**Create instances of the Student class with these new methods.**  4. Inheritance[A video describing the concept.](https://youtu.be/f3Wh4MDEog8)[A video demo.](https://youtu.be/fg4krAA5QXg)One final (very important) aspect of object oriented programming is theconcept of inheritance. This allows us to create new classes from other ones.In practice this saves replicating code as we can change only certain methodsas required.To do this we simply create the new class as usual but pass it the old class:```python   class NewClass(OldClass):       ...   ```For example, let us create a student who we know is born on the 29th ofFebruary (a date that only occurs once every 4 years):<jupyter_code>class LeapYearStudent(Student):
    """A class for a student born on the 29th of February"""
    # Note that we do not have to rewrite the init method
    def have_a_birthday(self, years=1):
        self.age += int(years / 4)
    def complain(self):
        """Return a string complaining about birthday"""
        # This is a new method that the Student class does not have     
        return "I wish I was not born on the 29th of Feb"<jupyter_output><empty_output><jupyter_text>Here is how this new class behaves:<jupyter_code>geraint = LeapYearStudent(["Maths"], 22, "Male")
geraint.have_a_birthday()
geraint.age  # Still  22
geraint.have_a_birthday(8)
geraint.age
geraint.complain()<jupyter_output><empty_output><jupyter_text>**Experiment with the above code: how would it work if leap year was every 3 years?** --- Worked example[A video describing the concept.](https://youtu.be/nlIz2QtBCTk)[A video demo.](https://youtu.be/BRLbh-6QU_Q)Let us assume we want to study linear expressions. These are expressionsof the form:\\[    ax+b\\]We are interested, for example, in what the value of \\(x\\) for which alinear expression is 0. This is called the 'root' and is the solution to thefollowing equation:\\[    ax+b=0\\]This is obviously an easy thing to study but we're going to assume it's notand build a class to represent and manipulate linear expressions.<jupyter_code>class LinearExpression:
    """A class for a linear expression"""
    def __init__(self, a, b):
        self.a = a
        self.b = b
    
    def root(self):
        """Return the root of the linear expression"""
        return - self.b / self.a
    
    def __add__(self, linexp):
        """A special method: lets us have addition between expressions"""
        return LinearExpression(self.a + linexp.a, self.b + linexp.b)

    def __repr__(self):
        """A special method: changes the way an instance is displayed"""
        return "Linear expression: " + str(self.a) + "x + " + str(self.b)
exp = LinearExpression(2, 4)
exp  # This output is given by the `__repr__` method
exp.a
exp.b
exp.root()
exp2 = LinearExpression(5, -2)
exp2
exp + exp2  # This works because of the `__add__` method<jupyter_output><empty_output><jupyter_text>This class works just fine but we quickly arrive at a problem:<jupyter_code>exp1 = LinearExpression(2, 4)
exp2 = LinearExpression(-2, 4)
exp3 = exp1 + exp2
exp3
exp3.root()<jupyter_output><empty_output><jupyter_text>We get an error because our `root` method is attempting to divide by 0.Let's fix that:<jupyter_code>class LinearExpression:
    """A class for a linear expression"""
    def __init__(self, a, b):
        self.a = a
        self.b = b
        
    def root(self):
        """Return the root of the linear expression"""
        if self.a != 0:
            return - self.b / self.a
        return False
    
    def __add__(self, linexp):
        """A special method: let's us have addition between expressions"""
        return LinearExpression(self.a + linexp.a, self.b + linexp.b)
    
    def __repr__(self):
        """A special method: changes the default way an instance is displayed"""
        return "Linear expression: " + str(self.a) + "x + " + str(self.b)
exp3 = LinearExpression(0, 8)
exp3.root()<jupyter_output><empty_output><jupyter_text>Let us now use this to verify the following simple fact. If \\(f(x) =a_1x+b_1\\) and \\(g(x) = a_2x+b_2\\), then the root of \\(f(x) + g(x)\\) isgiven by:\\[    \frac{a_1x_1 + a_2x_2}{a_1+a_2}\\]where \\(x_1\\) is the root of \\(f\\) and \\(x_2\\) is the root of \\(g\\)(if they exist).First let's write a function that checks this for a given set of\\(a_1, a_2, b_1, b_2\\).<jupyter_code>def check_result(a1, a2, b1, b2):
    """Check that the relationship holds"""
    f = LinearExpression(a1, b1)
    g = LinearExpression(a2, b2)
    k = f + g
    x1 = f.root()
    x2 = g.root()
    x3 = k.root()
    if (x1 is not False) and (x2 is not False) and (x3 is not False):
        # Assuming our three expressions have a root
        return (a1 * x1 + a2 * x2) / (a1 + a2) == x3
    return True  # If f, g have no roots the relationship is still true
check_result(2, 3, 4, 5)<jupyter_output><empty_output><jupyter_text>We will verify this by randomly sampling values for \\(a_1, a_2, b_1, b_2\\).<jupyter_code>import random  # Importing the random module
N = 1000  # THe number of samples
checks = []
for _ in range(N):
    a1 = random.randint(-10, 10)
    a2 = random.randint(-10, 10)
    b1 = random.randint(-10, 10)
    b2 = random.randint(-10, 10)
    checks.append(check_result(a1, a2, b1, b2))
all(checks)<jupyter_output><empty_output><jupyter_text>--- ExercisesHere are a number of exercises that are possible to carry out using the code concepts discussed:- Classes: creating a set of rules that describe an abstract "thing"- Attributes: variables on a class- Methods: functions on a class- Inheritance: creating new classes from old classes --- Exercise 1**Debugging exercise**   The following is an attempt to write a class for rectangle, find   and fix all the bugs.   ```python   class Rectangle:       """A class for a rectangle""       def __init__(width, length)           self.width = width           self.length = width       def obtain_area(self:           """Obtain the area of the rectangle"""           return self.width * self.length       def is_square():           """Check if the rectangle is a square"""           return self.width == self.length   ```<jupyter_code>class Rectangle:
    # """A class for a rectangle""  Wrong number of "
    """A class for a rectangle"""
    #def __init__(width, length)  Missing self and :
    def __init__(self, width, length):
        self.width = width
        #self.length = width  # Wrong assignment
        self.length = length
    #def obtain_area(self:  Missing bracket
    def obtain_area(self):
        """Obtain the area of the rectangle"""
        return self.width * self.length
    #def is_square():  Missing self
    def is_square(self):
        """Check if the rectangle is a square"""
        return self.width == self.length<jupyter_output><empty_output><jupyter_text>Creating a non square rectangle:<jupyter_code>rectangle = Rectangle(5, 4)
rectangle.obtain_area(), rectangle.is_square()<jupyter_output><empty_output><jupyter_text>Creating a square rectangle:<jupyter_code>square = Rectangle(3, 3)
square.obtain_area(), square.is_square()<jupyter_output><empty_output><jupyter_text>--- Exercise 2Build a class for a quadratic expression:\\[ax^2+bx+c\\]Include a method for adding two quadratic expressions together and also amethod to calculate the roots of the expression.<jupyter_code>import math

class QuadraticExpression:
    """A class for a quadratic expression"""
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c
    def root(self):
        """Return the root of the linear expression"""
        determinant = self.b ** 2 - 4 * self.a * self.c
        if determinant >= 0:
            x1 = - (self.b + math.sqrt(determinant))/ (2 * self.a)
            x2 = - (self.b - math.sqrt(determinant))/ (2 * self.a)
            return x1, x2
        return False
    def __add__(self, quadexp):
        """A special method: let's us have addition between expressions"""
        return QuadraticExpression(self.a + quadexp.a, self.b + quadexp.b, self.c + quadexp.c)
    def __repr__(self):
        """A special method: changes the default way an instance is displayed"""
        return "Quadratic expression: " + str(self.a) + "x ^ 2 + " + str(self.b) + "x + " + str(self.c)
quad = QuadraticExpression(1, 5, 2)
quad
quad.root()
quad2 = QuadraticExpression(4, 5, 2)
quad + quad2<jupyter_output><empty_output><jupyter_text>--- Exercise 3If rain drops were to fall randomly on a square of side length \\(2r\\) theprobability of the drops landing in an inscribed circle of radius \\(r\\)would be given by:\\[    P = \frac{\text{Area of circle}}{\text{Area of square}}=\frac{\pi r ^2}{4r^2}=\frac{\pi}{4}\\]![Circle](/{{root}}/img/circle.png)Thus, if we can approximate \\(P\\) then we can approximate \\(\pi\\) as\\(4P\\). In this question we will write code to approximate \\(P\\) usingthe random library.First of all, create a class for a rain drop (make sure youunderstand the code!):```pythonclass Drop():    def __init__(self, r=1):        self.x = (.5 - random.random()) * 2 * r        self.y = (.5 - random.random()) * 2 * r        self.incircle = (self.y) ** 2 + (self.x) ** 2 <= (r) ** 2```Note that the above uses the following equation for a circle centred at\\((0,0)\\) of radius \\(r\\):\\[    x^2+y^2≤r^2\\]To approximate \\(P\\) simply create \\(N=1000\\) instances of Drops andcount the number of those that are in the circle.  Use this to approximate\\(\pi\\).(This is an example of a technique called Monte Carlo Simulation.)<jupyter_code>import random
import math  # This is not necessarily needed but will be useful for some comparisons later on.


class Drop():
    """ 
    A class for a rain drop falling in a random location 
    on a square 
    """
    def __init__(self, r=1):
        self.x = (.5 - random.random()) * 2 * r
        self.y = (.5 - random.random()) * 2 * r
        self.incircle = (self.y) ** 2 + (self.x) ** 2 <= (r) ** 2  
        # This returns the boolean corresponding to whether or not the point is in the circle


def approxpi(N=1000):
    """
    Function to return an approximation for pi using montecarlo simulation
    """
    numberofpointsincircle = 0
    for i in range(N):  # A loop to drop sufficient points
        drop = Drop()  # Generate a new drop
        if drop.incircle:  # Check if drop is in circle
            numberofpointsincircle += 1
    return 4 * numberofpointsincircle / float(N)
approxpi()<jupyter_output><empty_output><jupyter_text>---- Exercise 4In a similar fashion to question 8, approximate the integral\\(\int_{0}^11-x^2\;dx\\).Recall that the integral corresponds to the area under a curve.Furthermore this diagram might be helpful:![Grid](/{{root}}/img/gridwithplot.png)<jupyter_code>class Point():
    """
    A class for a point falling in a random location on a square

    Attributes:
        x: the x coordinate of the point
        y: the y coordinate of the point
        incircle: a boolean indicating whether or not the point is under the graph
    """
    def __init__(self, r=1):
        self.x = random.random()
        self.y = random.random()
        self.undergraph = 1 - (self.x) ** 2 >= self.y  
        # This returns the boolean checking whether or not the point is under the graph


def approxint(N=1000):
    """
    Function to return an approximation for the integral using montecarlo simulation

    Arguments: N (default=1000) which is the number of points

    Outputs: An approximation of the integral
    """
    numberofpointsundergraph = 0
    for i in range(N):  # A loop to drop sufficient points
        point = Point()  # Generate a new drop
        if point.undergraph:  # Check if drop is in circle
            numberofpointsundergraph += 1
    return numberofpointsundergraph / float(N)
approxint()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11151725419738553, 'fraction_numerical': 0.014683105135989097, 'mean_word_length': 3.5368184373243396, 'pattern_counts': {'":': 0, '<': 92, '<?xml version=': 0, '>': 92, 'https://': 10, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '22', 'dir': 'jupyter-structured-clean-dedup', 'id': '640189', 'license': 'MIT', 'n_tokens_mistral': 5115, 'n_tokens_neox': 4891, 'n_words': 1818, 'path': 'nbs/chapters/04-Object-oriented-programming.ipynb', 'repo_name': 'geraintpalmer/cfm', 'size': '16143'}