<jupyter_start><jupyter_text>Introduction to Python   [Classes](https://docs.python.org/3/tutorial/classes.html) and OOP+ Classes provide means of bundling data and functionality together.  + Creating a new class is equivalent to creating a new type of object, allowing new instances of that type to be made.  + Each class instance can have attributes attached to it for maintaining its state.  + Class instances can also have methods for modifying its state.   The class signature is:    class ClassName(Superclass - optional):                .        .        .          Let's start with an example<jupyter_code>class MyClass:
    """
    class documentation string
    """
    
    class_attribute1 = 1234
    class_attribute2 = 5678
    
    def class_method(self):
        return 'Hello!'
print(type(MyClass))
x = MyClass()
y = MyClass()
print(type(x))
print(type(y))
dir(x)
print(x.__hash__())
print(y.__hash__())
x.class_attribute1
x.class_attribute2
x.class_method()
x.class_attribute1 = 4321
x.class_attribute1
y.class_attribute1
x.new_attribute = "any new attribute"
z = [1,2,3]
dir(x)
dir(y)<jupyter_output><empty_output><jupyter_text>Another example, with __init__() method:<jupyter_code>class MyComplex:
    def __init__(self, realpart, imagpart):
        self.r = realpart
        self.i = imagpart
        print("__init__ has run")
z = MyComplex(2,-4)
j = MyComplex(13,12)
z.r
z.i
dir(j)<jupyter_output><empty_output><jupyter_text>Adding methods:<jupyter_code>class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height
        self.perimeter = 2 * (width+height)
        if width < height:
            self.invert_sides()
            
    def invert_sides(self):
        self.width, self.height = self.height, self.width
        print('inverted height and width')
my_rectangle = Rectangle(50,10)
print(my_rectangle.width)
print(my_rectangle.height)
print(my_rectangle.area)
print(my_rectangle.perimeter)
my_rectangle.invert_sides()
print(my_rectangle.width)
print(my_rectangle.height)
dir(my_rectangle)<jupyter_output><empty_output><jupyter_text>What is printed when we call the "print" primitive in an object?<jupyter_code>my_rectangle.__repr__()<jupyter_output><empty_output><jupyter_text>_print()_ calls the internal __repr__() method.<jupyter_code>print(my_rectangle)<jupyter_output><__main__.Rectangle object at 0x7fc39c6df990><jupyter_text>Another class example:<jupyter_code>class Triangle:
    def __init__(self, side1, side2, side3):
        self.side1 = side1
        self.side2 = side2
        self.side3 = side3
        print('running __init__\n')
        self.type_of_triangle()
        
    def __repr__(self):
        return f"I am a Triangle with sides {self.side1}, {self.side2} and {self.side3}"
        
    #def __repr__(self):
    #    return "42"
        
    def type_of_triangle(self):
        if self.side1 == self.side2 and self.side1 == self.side3:
            print('I am equilateral')
            self.mytype = 'equilateral'
        elif self.side1 == self.side2 or \
             self.side1 == self.side3 or \
             self.side2 == self.side3:
            print('I am isosceles')
            self.mytype = 'isosceles'
        else:
            print('I am scalene')
            self.mytype = 'scalene'
    
tri = Triangle(5,5,5)
tri.__init__(5,5,5)
print(tri.mytype)
print(tri.side1)
tri.type_of_triangle()
print(tri)
dir(tri)<jupyter_output><empty_output><jupyter_text>Operator overloading<jupyter_code>class Thing:
    def __init__(self, value):
        self.__value = value
        
    def __gt__(self, other):
        return self.__value > other.__value
    
    def __lt__(self, other):
        return self.__value < other.__value
    
something = Thing(100)
nothing = Thing(10)
something > nothing
something < nothing
something + nothing # erro
class Thing:
    def __init__(self, value):
        self.__value = value
        
    def __gt__(self, other):
        return self.__value > other.__value
    
    def __lt__(self, other):
        return self.__value < other.__value
    
    def __add__(self, other):
        return self.__value + other.__value
    
something = Thing(100)
nothing = Thing(10)
something + nothing
nothing + something<jupyter_output><empty_output><jupyter_text>[Class inheritance](https://medium.com/swlh/classes-subclasses-in-python-12b6013d9f3)<jupyter_code>class Dog:
    def __init__(self, name, race='Royal Street Dog'):
        self.name = name
        self.race = race
        self.tricks = []

    def add_trick(self, trick):
        if not trick in self.tricks:
            self.tricks.append(trick.title())
        else:
            print('I have learnt that already!')
d = Dog('Fido','<NAME>')
e = Dog('Buddy','Cocker')
f = Dog('Rex')
print(d.name)
print(d.race)
print(e.name)
print(e.race)
print(f.name)
print(f.race)
d.add_trick('Roll')
d.add_trick('Pretending dead')
d.tricks
d.add_trick('Ask for food')
d.tricks
e.tricks
f.add_trick('Bark')
f.tricks<jupyter_output><empty_output><jupyter_text>Creating a subclass using inheritance<jupyter_code>import time

class SuperDog(Dog):
    
    def __init__(self, name, race):
        Dog.__init__(self, name, race)
        self.food = False
        self.trained = False
        self.last_meal = time.time()
        
    def is_hungry(self):
        if time.time() - self.last_meal < 20:
            print('Not hungry')
        else:
            print(f'Yes, my last meal was {(time.time() - self.last_meal)/60:.2f} min. ago')
    
    def train(self):
        self.trained = True
    
    def feed(self):
        self.last_meal = time.time()
        print('The food was delicious. Thanks!')
f = SuperDog('Raghu','Labrador')
f.is_hungry()
f.is_hungry() #calling after some time...
f.feed()
f.is_hungry()
f.tricks
f.add_trick('Give Five')
f.tricks
f.trained
f.train()
f.trained<jupyter_output><empty_output><jupyter_text>[Multiple inheritance](https://www.techbeamers.com/python-multiple-inheritance/)<jupyter_code>class A: 
    def rk(self): 
        print(" In class A") 
        
class B(A): 
    def rk(self): 
        print(" In class B") 
        
class C(A): 
    def rk(self): 
        print("In class C") 
  
# classes ordering 
class D(B, C): 
    pass
     
r = D() 
r.rk()<jupyter_output>In class B<jupyter_text>Another example:<jupyter_code>class Animal:
    def __init__(self, weight=0, height=0):
        self.weight = weight
        self.height = height
class Carnivore(Animal):
    def __init__(self, weight=0, height=0):
        super().__init__(weight, height)

    def say(self):
        raise NotImplementedError
TRex = Carnivore(2000)
TRex.weight
class Wolf(Carnivore):
    def __init__(self, weight, height):
        super().__init__(weight, height)
        
    def say(self):
        print("Bark! Bark!")
Jack = Wolf(8, 45)
print(Jack.weight)
print(Jack.height)
print(Jack.say())
class Pet(Animal):
    def __init__(self, tutor, weight=0, height=0):
        super().__init__(weight, height)
        self.tutor = tutor
fish = Pet('John', 35)

print(fish.tutor)
print(fish.weight)
print(fish.height)
class Cat(Carnivore, Pet):
    def __init__(self, weight, height, tutor):
        super(Carnivore, self).__init__(self, weight, height)
        print(self.weight)
        print(self.height)
        Pet.__init__(self, tutor, self.weight, self.height)
        print(self.tutor)
        
    def say(self):
        print("Meaw!")<jupyter_output><empty_output><jupyter_text>[C3 & MRO](https://en.wikipedia.org/wiki/C3_linearization)<jupyter_code>Cat.__mro__
Garfield = Cat(4, 25, 'John')
print(f"Weight: {Garfield.weight}\nHeight: {Garfield.height}\nTutor: {Garfield.tutor}")
Garfield.say()<jupyter_output>Weight: 4
Height: 25
Tutor: John
Meaw!<jupyter_text>Modifying an existing class<jupyter_code>class MyInteger(int):
    def __init__(self,number):
        super().__init__() 
    
    def __add__(self,other):
        return self * other
    
    def __sub__(self,other):
        return self + other
    
    def square(self):
        return self * self
a = MyInteger(2)
b = MyInteger(5)
dir(a)
a * b
a + b  #?????
a - b
a.square()
help(MyInteger.__mul__)
help(MyInteger.__str__)<jupyter_output>Help on wrapper_descriptor:

__str__(self, /)
    Return str(self).<jupyter_text>[Special Methods for Classes](https://docs.python.org/3/reference/datamodel.htmlspecial-method-names)  [examples](https://www.pythonlikeyoumeanit.com/Module4_OOP/Special_Methods.html) MethodSignatureExplanationReturns string for a printable representation of object__repr__(self)repr(x) invokes x.__repr__(), this is also invoked when an object is returned by a consoleReturns string representation of an object__str__(self)str(x) invokes x.__str__() MethodSignatureExplanationAdd__add__(self, other)x + y invokes x.__add__(y)Subtract__sub__(self, other)x - y invokes x.__sub__(y)Multiply__mul__(self, other)x * y invokes x.__mul__(y)Divide__truediv__(self, other)x / y invokes x.__truediv__(y)Power__pow__(self, other)x ** y invokes x.__pow__(y) MethodSignatureExplanationLength__len__(self)len(x) invokes x.__len__()Get Item__getitem__(self, key)x[key] invokes x.__getitem__(key)Set Item__setitem__(self, key, item)x[key] = item invokes x.__setitem__(key, item)Contains__contains__(self, item)item in x invokes x.__contains__(item)Iterator__iter__(self)iter(x) invokes x.__iter__()Next__next__(self)next(x) invokes x.__next__()  A more complete example:<jupyter_code>def strike(text):
    """ Renders string with strike-through characters through it.

        `strike('hello world')` -> '̶h̶e̶l̶l̶o̶ ̶w̶o̶r̶l̶d'

        Notes
        -----
        \u0336 is a special strike-through unicode character; it
        is not unique to Python."""
    return ''.join('\u0336{}'.format(c) for c in text)

class ShoppingList:
    def __init__(self, items):
        self._needed = set(items)
        self._purchased = set()

    def __repr__(self):
        """ Returns formatted shopping list as a string with
            purchased items being crossed out.

            Returns
            -------
            str"""
        if self._needed or self._purchased:
            remaining_items = [str(i) for i in self._needed]
            purchased_items = [strike(str(i)) for i in self._purchased]
            # You wont find the • character on your keyboard. I simply
            # googled "unicode bullet point" and copied/pasted it here.
            return "• " + "\n• ".join(remaining_items + purchased_items)

    def add_new_items(self, items):
        self._needed.update(items)

    def mark_purchased_items(self, items):
        self._purchased.update(set(items) & self._needed)
        self._needed.difference_update(self._purchased)

food = ShoppingList(["milk", "flour", "salt", "eggs"])
print(food)
food.mark_purchased_items(["flour", "salt"])
print(food)
def __add__(self, other):
    """ Add the unpurchased and purchased items from another shopping
        list to the present one.

        Parameters
        ----------
        other : ShoppingList
            The shopping list whose items we will add to the present one.
        Returns
        -------
        ShoppingList
            The present shopping list, with items added to it."""
    new_list = ShoppingList([])
    # populate new_list with items from `self` and `other`
    for l in [self, other]:
        new_list.add_new_items(l._needed)

        # add purchased items to list, then mark as purchased
        new_list.add_new_items(l._purchased)
        new_list.mark_purchased_items(l._purchased)
    return new_list

# set `__add__` as a method of `ShoppingList`
setattr(ShoppingList, "__add__", __add__)
food = ShoppingList(["milk", "flour", "salt", "eggs"])
food.mark_purchased_items(["flour", "salt"])

office_supplies = ShoppingList(["staples", "pens", "pencils"])
office_supplies.mark_purchased_items(["pencils"])

clothes = ShoppingList(["t-shirts", "socks"])

# combine all three shopping lists
food + office_supplies + clothes
class MyList:
    def __init__(self, *args):
        if len(args) == 1 and hasattr(args[0], '__iter__'):
            # handles `MyList([1, 2, 3])
            self._data = list(args[0])
        else:
            # handles `MyList(1, 2, 3)`
            self._data = list(args)

    def __getitem__(self, index):
        out = self._data[index]
        # slicing should return a `MyList` instance
        # otherwise, the individual element should be returned as-is
        return MyList(out) if isinstance(index, slice) else out

    def __setitem__(self, key, value):
        self._data[key] = value

    def __len__(self):
        return len(self._data)

    def __repr__(self):
        """ Use the character | as the delimiter for our list"""
        # `self._data.__repr__()` returns '[ ... ]',
        # thus we can slice to get the contents of the string
        # and exclude the square-brackets, and add our own
        # delimiters in their place
        return "|" + self._data.__repr__()[1:-1] + "|"

    def __contains__(self, item):
        return item in self._data

    def append(self, item):
        self._data.append(item)
# MyList can accept any iterable as its
# first (and only) input argument
x = MyList("hello")
x
# MyList accepts an arbitrary number of arguments
x = MyList(1, 2, 3, 4, 5)
x
print(len(x))
# getting an item
x[0]
# slicing returns a MyList instance
x[2:4]
# setting an item
x[0] = -1
x
# checking membership
10 in x
MyList()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12132189856572527, 'fraction_numerical': 0.011385479816649415, 'mean_word_length': 3.1775787523162444, 'pattern_counts': {'":': 0, '<': 60, '<?xml version=': 0, '>': 59, 'https://': 6, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '14', 'dir': 'jupyter-structured-clean-dedup', 'id': '73517', 'license': 'Apache-2.0', 'n_tokens_mistral': 4617, 'n_tokens_neox': 4436, 'n_words': 1085, 'path': 'Notebooks/09_Classes.ipynb', 'repo_name': 'LukasdelasCasas/Python_Course', 'size': '13535'}