<jupyter_start><jupyter_text>Object-Oriented Programming As you probably know, Python is an object-oriented language, and so it has very strong support for objects. In fact, everything in Python is an object. Here is the bare minimum about Python objects.   Defining a new class Let's start with a concrete example: polynomials with integer coefficients.We define a class `Polynomial` with two 'special' double underscore methods and one normal method. This class will have an attribute `x` that is specified at the time of creating new instances of the class.- The `__init__` method initializes properties of any new instance of `Polynomial`- The `__repr__` method provides an accurate string representation of `Polynomial`. For example, if we print an instance of `Polynomial`, the `__repr__` method will be used. If you don't specify a `__repr__` (or `__str__`) special method, the default name when printing only gives the address in memory.- The `__repr__` is for displaying the object in the environment, and `__str__` is for printing, they can be defferent.There are many more special methods, as described in the [official documentation](https://docs.python.org/3.5/reference/datamodel.html). We will not go there.<jupyter_code>class Polynomial:
    '''
        A toy example class of integer-coefficient polynomial
        Coefficients are stored in ascending degree, no trailing zeros
    '''
    def __init__(self, coeff):
        nz_pos = np.where(np.array(coeff) != 0)[0]
        if len(nz_pos) == 0:
            self.coeff = np.array(0, dtype=int)
        else:
            self.coeff = np.array(coeff[:nz_pos.max()+1], dtype=int) 
        
    def __repr__(self):
        return 'Polynomial with coeff {0}'.format(self.coeff.tolist())
        
    def __str__(self):
        if np.all(self.coeff == 0):
            return '0'
        else:
            monomial = lambda c, i: int(c!=0) * '{0}{1}'.format(
                str(c) if (c != 1 or i == 0) else '', 
                'x'*(i>0) + ('^'+str(i))*(i>1) 
            )
            monimials = [monomial(c,i) for i, c in enumerate(self.coeff) if monomial(c,i)]
            return ' + '.join([m for m in monimials if m])
        
    def degree(self):
        '''Return the degree of polynomial'''
        return self.coeff.size - 1
    
    def __add__(self, other):
        '''Operator Overloading: Add a polynomial or a integer on right'''
        if isinstance(other, int):
            coeff = self.coeff.copy()
            coeff[0] += other
        elif isinstance(other, Polynomial):
            n = max(self.degree(), other.degree()) + 1
            coeff = np.zeros(n, dtype=int)
            coeff[:self.degree()+1] += self.coeff
            coeff[:other.degree()+1] += other.coeff
        else:
            return NotImplementedError()
        return Polynomial(coeff)
    
    def __mul__(self, other):
        '''Operator Overloading: Multiply a polynomial or a integer on right'''
        if isinstance(other, int):
            coeff = other * self.coeff
        elif isinstance(other, Polynomial):
            coeff = np.convolve(self.coeff, other.coeff)
        else:
            return NotImplementedError()
        return Polynomial(coeff)
    
    def __eq__(self, other):
        if isinstance(other, Polynomial):
            return np.array_equal(self.coeff, other.coeff)
        else:
            return NotImplementedError()
    
    @classmethod
    def random(cls, low, high, max_deg):
        '''Return a random polynomial with coefficients in [low, high) with degree at most max_deg'''
        coeff = np.random.randint(low=low, high=high, size=max_deg+1)
        return Polynomial(coeff)
help(Polynomial)<jupyter_output>Help on class Polynomial in module __main__:

class Polynomial(builtins.object)
 |  A toy example class of integer-coefficient polynomial
 |  Coefficients are stored in ascending degree, no trailing zeros
 |  
 |  Methods defined here:
 |  
 |  __add__(self, other)
 |      Operator Overloading: Add a polynomial or a integer on right
 |  
 |  __eq__(self, other)
 |      Return self==value.
 |  
 |  __init__(self, coeff)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  __mul__(self, other)
 |      Operator Overloading: Multiply a polynomial or a integer on right
 |  
 |  __repr__(self)
 |      Return repr(self).
 |  
 |  __str__(self)
 |      Return str(self).
 |  
 |  degree(self)
 |      Return the degree of polynomial
 |  
 |  ----------------------------------------------------------------------
 |  Class methods defined here:
 |  
 |  random(low, high, max_deg) from builtins.type
 |      Return a random polynomial with coefficients in [low, high) with [...]<jupyter_text>Creating an instance of class<jupyter_code>poly1, poly2 = Polynomial([1,2,3]), Polynomial([4,5,6,7])<jupyter_output><empty_output><jupyter_text>Attribute and Method access<jupyter_code>print(poly1.coeff)
print('{0} has degree {1}'.format(poly1, poly1.degree()))
print('{0} has degree {1}'.format(poly2, poly2.degree()))<jupyter_output>[1 2 3]
1 + 2x + 3x^2 has degree 2
4 + 5x + 6x^2 + 7x^3 has degree 3<jupyter_text>Operator overloading<jupyter_code>print('sum is {0}'.format(poly1 + poly2))
print('product is {0}'.format(poly1 * poly2))
poly1 == Polynomial([1,2,3])<jupyter_output>sum is 5 + 7x + 9x^2 + 7x^3
product is 4 + 13x + 28x^2 + 34x^3 + 32x^4 + 21x^5<jupyter_text>Class method<jupyter_code>rand_poly = Polynomial.random(low=0, high=3, max_deg=5)
print(rand_poly)<jupyter_output>2x + x^2 + x^5<jupyter_text>Class Inheritance Custom defined classes are not hashable once `==` operator is overloaded.If `==` is not overloaded, it is hashable but not in your expected way: two polynomials with same coefficients will not be considered equal<jupyter_code>poly3 = Polynomial([1,2,3])
set([poly1, poly2, poly3])<jupyter_output><empty_output><jupyter_text>What if I want to use my customized class as the key of `dict` / in a `set`?Overload the `__hash__` method! Let's make this work by class inheritance.<jupyter_code>class Polynomial_hashable(Polynomial):
    '''A hashable polynomial'''
    def __hash__(self):
        return hash(tuple(self.coeff))
poly1 = Polynomial_hashable([1,2,3])
poly2 = Polynomial_hashable([4,5,6,7])
poly3 = Polynomial_hashable([1,2,3])
set([poly1, poly2, poly3])
class P_ary_Polynomial(Polynomial_hashable):
    '''Hashable p-ary polynomial'''
    def __init__(self, coeff, p):
        # Make sure p is a positive integer
        self.p = int(p)
        assert self.p > 0
        coeff = np.mod(np.array(coeff, dtype=int), self.p)
        # Call initialization of parent class
        super().__init__(coeff)
        
    def __repr__(self):
        return '{0}-ary Polynomial with coeff {1}'.format(self.p, self.coeff.tolist())
        
    def __add__(self, other):
        '''Operator Overloading: Add a polynomial or a integer on right'''
        if isinstance(other, int):
            coeff = self.coeff.copy()
            coeff[0] += other
        elif isinstance(self, P_ary_Polynomial) and self.p == other.p:
            n = max(self.degree(), other.degree()) + 1
            coeff = np.zeros(n, dtype=int)
            coeff[:self.degree()+1] += self.coeff
            coeff[:other.degree()+1] += other.coeff
        elif self.p != other.p:
            raise Exception('Cannot operate in different polynomial rings')
        else:
            return NotImplemented
        return P_ary_Polynomial(np.mod(coeff, self.p), self.p)
    
    def __mul__(self, other):
        '''Operator Overloading: Multiply a polynomial or a integer on right'''
        if isinstance(other, int):
            coeff = other * self.coeff
        elif isinstance(self, P_ary_Polynomial) and self.p == other.p:
            coeff = np.convolve(self.coeff, other.coeff)
        elif self.p != other.p:
            raise Exception('Cannot operate in different polynomial rings')
        else:
            return NotImplemented
        return P_ary_Polynomial(np.mod(coeff, self.p), self.p)
    
    def __eq__(self, other):
        if isinstance(self, P_ary_Polynomial):
            return np.array_equal(self.coeff, other.coeff) and self.p == other.p
        else:
            return NotImplemented
        
    def __hash__(self):
        return hash((self.p, tuple(self.coeff)))
poly1 = P_ary_Polynomial([1,2,3], 2)
poly2 = P_ary_Polynomial([3,5,6,7], 2)
poly3 = P_ary_Polynomial([1,3,4], 3)
poly4 = P_ary_Polynomial([1,3,4], 5)
set([poly1, poly2, poly3, poly4])<jupyter_output><empty_output><jupyter_text>* polynomials with same displayed coefficient but different base are not equal* polynomials with same input coefficient but different base are not equal<jupyter_code>poly1 == poly3, poly3 == poly4<jupyter_output><empty_output><jupyter_text>Polynomials with same base can operate add and multiply<jupyter_code>poly1 + poly2, poly1 * poly2<jupyter_output><empty_output><jupyter_text>Polynomials with different bases cannot operate add and multiply<jupyter_code>poly1 * poly3<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12011481563258998, 'fraction_numerical': 0.017443144181938618, 'mean_word_length': 3.03878733838609, 'pattern_counts': {'":': 0, '<': 37, '<?xml version=': 0, '>': 40, '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': '346459', 'license': 'MIT', 'n_tokens_mistral': 2899, 'n_tokens_neox': 2836, 'n_words': 837, 'path': 'notebooks/05_Object_Oriented_Programming.ipynb', 'repo_name': 'Joshua1989/python_scientific_computing', 'size': '9058'}