<jupyter_start><jupyter_text>Classes and Object Oriented Programming In an earlier section we discussed *classes* as a way of representing an abstract object, such as a polynomial. The resulting code<jupyter_code>class Polynomial(object):
    """Representing a polynomial."""
    explanation = "I am a polynomial"
    
    def __init__(self, roots, leading_term):
        self.roots = roots
        self.leading_term = leading_term
        self.order = len(roots)
        
    def display(self):
        string = str(self.leading_term)
        for root in self.roots:
            if root == 0:
                string = string + "x"
            elif root > 0:
                string = string + "(x - {})".format(root)
            else:
                string = string + "(x + {})".format(-root)
        return string
    
    def multiply(self, other):
        roots = self.roots + other.roots
        leading_term = self.leading_term * other.leading_term
        return Polynomial(roots, leading_term)
    
    def explain_to(self, caller):
        print("Hello, {}. {}.".format(caller,self.explanation))
        print("My roots are {}.".format(self.roots))<jupyter_output><empty_output><jupyter_text>allowed polynomials to be created, displayed, and multiplied together. However, the language is a little cumbersome. We can take advantage of a number of useful features of Python, many of which carry over to other programming languages, to make it easier to use the results. Remember that the `__init__` function is called when a variable is created. There are a number of special class functions, each of which has two underscores before and after the name. This is another Python *convention* that is effectively a rule: functions surrounded by two underscores have special effects, and will be called by other Python functions internally. So now we can create a variable that represents a specific polynomial by storing its roots and the leading term:<jupyter_code>p_roots = (1, 2, -3)
p_leading_term = 2
p = Polynomial(p_roots, p_leading_term)
p.explain_to("Alice")
q = Polynomial((1,1,0,-2), -1)
q.explain_to("Bob")<jupyter_output>Hello, Alice. I am a polynomial.
My roots are (1, 2, -3).
Hello, Bob. I am a polynomial.
My roots are (1, 1, 0, -2).<jupyter_text>Another special function that is very useful is `__repr__`. This gives a *representation* of the class. In essence, if you ask Python to `print` a variable, it will print the *string* returned by the `__repr__` function. This was the role played by our `display` method, so we can just change the name of the function, making the `Polynomial` class easier to use. We can use this to create a simple string representation of the polynomial:<jupyter_code>class Polynomial(object):
    """Representing a polynomial."""
    explanation = "I am a polynomial"
    
    def __init__(self, roots, leading_term):
        self.roots = roots
        self.leading_term = leading_term
        self.order = len(roots)
        
    def __repr__(self):
        string = str(self.leading_term)
        for root in self.roots:
            if root == 0:
                string = string + "x"
            elif root > 0:
                string = string + "(x - {})".format(root)
            else:
                string = string + "(x + {})".format(-root)
        return string
    
    def explain_to(self, caller):
        print("Hello, {}. {}.".format(caller,self.explanation))
        print("My roots are {}.".format(self.roots))
p = Polynomial(p_roots, p_leading_term)
print(p)
q = Polynomial((1,1,0,-2), -1)
print(q)<jupyter_output>2(x - 1)(x - 2)(x + 3)
-1(x - 1)(x - 1)x(x + 2)<jupyter_text>The final special function we'll look at (although there are [many more](https://docs.python.org/2/library/operator.html), many of which may be useful) is `__mul__`. This allows Python to *multiply* two variables together. We did this before using the `multiply` method, but by using the `__mul__` method we can multiply together two polynomials using the standard `*` operator. With this we can take the product of two polynomials:<jupyter_code>class Polynomial(object):
    """Representing a polynomial."""
    explanation = "I am a polynomial"
    
    def __init__(self, roots, leading_term):
        self.roots = roots
        self.leading_term = leading_term
        self.order = len(roots)
        
    def __repr__(self):
        string = str(self.leading_term)
        for root in self.roots:
            if root == 0:
                string = string + "x"
            elif root > 0:
                string = string + "(x - {})".format(root)
            else:
                string = string + "(x + {})".format(-root)
        return string
    
    def __mul__(self, other):
        roots = self.roots + other.roots
        leading_term = self.leading_term * other.leading_term
        return Polynomial(roots, leading_term)
    
    def explain_to(self, caller):
        print("Hello, {}. {}.".format(caller,self.explanation))
        print("My roots are {}.".format(self.roots))
p = Polynomial(p_roots, p_leading_term)
q = Polynomial((1,1,0,-2), -1)
r = p*q
print(r)<jupyter_output>-2(x - 1)(x - 2)(x + 3)(x - 1)(x - 1)x(x + 2)<jupyter_text>We now have a simple class that can represent polynomials and multiply them together, whilst printing out a simple string form representing itself. This can obviously be extended to be much more useful.  Inheritance As we can see above, building a complete class from scratch can be lengthy and tedious. If there is another class that does much of what we want, we can build on top of that. This is the idea behind *inheritance*. In the case of the `Polynomial` we declared that it started from the `object` class in the first line defining the class: `class Polynomial(object)`. But we can build on any class, by replacing `object` with something else. Here we will build on the `Polynomial` class that we've started with.A *monomial* is a polynomial whose leading term is simply 1. A monomial *is* a polynomial, and could be represented as such. However, we could build a class that knows that the leading term is always 1: there may be cases where we can take advantage of this additional simplicity.We build a new monomial class as follows:<jupyter_code>class Monomial(Polynomial):
    """Representing a monomial, which is a polynomial with leading term 1."""
    
    def __init__(self, roots):
        self.roots = roots
        self.leading_term = 1
        self.order = len(roots)<jupyter_output><empty_output><jupyter_text>Variables of the  `Monomial` class *are* also variables of the `Polynomial` class, so can use all the methods and functions from the `Polynomial` class automatically:<jupyter_code>m = Monomial((-1, 4, 9))
m.explain_to("Caroline")
print(m)<jupyter_output>Hello, Caroline. I am a polynomial.
My roots are (-1, 4, 9).
1(x + 1)(x - 4)(x - 9)<jupyter_text>We note that these functions, methods and variables may not be exactly right, as they are given for the general `Polynomial` class, not by the specific `Monomial` class. If we *redefine* these functions and variables inside the `Monomial` class, they will *override* those defined in the `Polynomial` class. We do not have to override all the functions and variables, just the parts we want to change:<jupyter_code>class Monomial(Polynomial):
    """Representing a monomial, which is a polynomial with leading term 1."""
    explanation = "I am a monomial"
    
    def __init__(self, roots):
        self.roots = roots
        self.leading_term = 1
        self.order = len(roots)
        
    def __repr__(self):
        string = ""
        for root in self.roots:
            if root == 0:
                string = string + "x"
            elif root > 0:
                string = string + "(x - {})".format(root)
            else:
                string = string + "(x + {})".format(-root)
        return string
m = Monomial((-1, 4, 9))
m.explain_to("Caroline")
print(m)<jupyter_output>Hello, Caroline. I am a monomial.
My roots are (-1, 4, 9).
(x + 1)(x - 4)(x - 9)<jupyter_text>This has had no effect on the original `Polynomial` class and variables, which can be used as before:<jupyter_code>s = Polynomial((2, 3), 4)
s.explain_to("David")
print(s)<jupyter_output>Hello, David. I am a polynomial.
My roots are (2, 3).
4(x - 2)(x - 3)<jupyter_text>And, as `Monomial` variables are `Polynomials`, we can multiply them together to get a `Polynomial`:<jupyter_code>t = m*s
t.explain_to("Erik")
print(t)<jupyter_output>Hello, Erik. I am a polynomial.
My roots are (-1, 4, 9, 2, 3).
4(x + 1)(x - 4)(x - 9)(x - 2)(x - 3)<jupyter_text>In fact, we can be a bit smarter than this. Note that the `__init__` function of the `Monomial` class is identical to that of the `Polynomial` class, just with the `leading_term` set explicitly to `1`. Rather than duplicating the code and modifying a single value, we can *call* the `__init__` function of the `Polynomial` class directly. This is because the `Monomial` class is built on the `Polynomial` class, so knows about it. We regenerate the class, but only change the `__init__` function:<jupyter_code>class Monomial(Polynomial):
    """Representing a monomial, which is a polynomial with leading term 1."""
    explanation = "I am a monomial"
    
    def __init__(self, roots):
        Polynomial.__init__(self, roots, 1)
        
    def __repr__(self):
        string = ""
        for root in self.roots:
            if root == 0:
                string = string + "x"
            elif root > 0:
                string = string + "(x - {})".format(root)
            else:
                string = string + "(x + {})".format(-root)
        return string
v = Monomial((2, -3))
v.explain_to("Fred")
print(v)<jupyter_output>Hello, Fred. I am a monomial.
My roots are (2, -3).
(x - 2)(x + 3)
{'fraction_non_alphanumeric': 0.1052685213492306, 'fraction_numerical': 0.010700091715071844, 'mean_word_length': 3.03370324702014, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 38, 'https://': 1, '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': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '409001', 'license': 'MIT', 'n_tokens_mistral': 3053, 'n_tokens_neox': 2884, 'n_words': 1146, 'path': '11-more-classes.ipynb', 'repo_name': 'LaGuer/maths-with-python', 'size': '9813'}