<jupyter_start><jupyter_text>MSDS 430 Module 9 Python Assignment  In this assignment you will complete the following exercises and submit your notebook and html file to Canvas. Your files should include all output, i.e. run each cell and save your file before submitting.  In this exercise, we will go over a brief introduction and examples around the Object-Oriented Programming (OOP) topic of **Inheritance** in Python:  InheritanceIn Python you have the ability to create a new class as an extension of another class. When **extending** a class, we call the original class the **parent class** and the new class the **child class**. The child class has the same internal structure as the parent class.This will be a lot easier to see in an example.  Example Classes: Pet & DogIn this example we will first create a `class` **Pet**. Then, we will create a class **Dog** that extends the **Pet** class.<jupyter_code>class Pet:
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    def __str__(self):
        return (self.name + " is " + str(self.age) + " year(s) old." )

class Dog(Pet):
    
    def walking(self):
        print(self.name, "is walking.")

    def sitting(self):
        print(self.name, "is sitting.")<jupyter_output><empty_output><jupyter_text>In the above example, the Pet class is first defined. Then the dog class extends the parent class **Pet** using the `class ChildClass(ParentClass)` :-```` class Dog(Pet): ````The **Pet** class has a `species` class variable, the initialization method `__init__` and the `__str__` method. In the **Dog** class the two methods `walking` and `sitting` have been defined.Now, lets create some Pet and Dog instances to see how this works.  Lets try creating a Dog object. We don't see an initialization method in the Dog class. So, lets just create a Dog object with no parameters.<jupyter_code>d1 = Dog()<jupyter_output><empty_output><jupyter_text>Hmm.. that was interesting. Take a look at the TypeError:```TypeError: __init__() missing 2 required positional arguments: 'name' and 'age'```The `__init__` method is expecting 2 arguments a name and an age. But, we don't see it in the Dog class definition. How did that happen?This is because Dog 'inherits' the attributes and methods of the Pet class. We know that Dog is a subclass or child class of Pet. The Pet class does have an `__init__` method that takes two parameters name and age.  Lets create a Dog object with a name and age.<jupyter_code>d1 = Dog("Jack",1)
print(d1)<jupyter_output>Jack is 1 year(s) old.<jupyter_text>So, we were able to create a `Dog` object using name and age parameter. We were also able to print it. So, far all the methods we used were inherited from the parent class (**Pet**). Now, lets try using the methods we defined just for the **Dog** class.<jupyter_code>d1.sitting()

d1.walking()<jupyter_output>Jack is sitting.
Jack is walking.<jupyter_text>Ok, Jack can sit and walk as we expected from a **Dog** object.Lets create a **Pet** object with the name "Buddy" who is 2 years of age. Then we will check whether Buddy can sit or walk?<jupyter_code>p1 = Pet("Buddy", 2)
p1.sitting()<jupyter_output><empty_output><jupyter_text>The `AttributeError` makes sense. The **Pet** object didn't have a `sitting` or `walking` method.   We defined the Pet and Dog classes so we know who is the parent and who is the child. But, what if we wanted to know **the subclasses** of either one?  Get all subclasses (or child classes): `ClassName.__subclasses__()`<jupyter_code>Pet.__subclasses__()<jupyter_output><empty_output><jupyter_text>**Pet** has the **Dog** subclass. That is what we expected. Lets see if **Dog** has any subclasses.<jupyter_code>Dog.__subclasses__()<jupyter_output><empty_output><jupyter_text>Nope, thats an empty list. So, **Dog**  has no subclasses.  Get all parent classes: `inspect.getmro(ClassName))`The mro in getmro stands for method resolution order. The getmro function returns all the parent classes of a class in order.If we had three classes A , B and C as follows, then inspect.getmro(C) would return classes C, B, A and object in a tuple. This is because C inherits from B who in turn inherits from A. On the execution of a method on an object of class C, first the definition of class C will be checked followed by B and then A.    Class A:        ...    Class B(A):        ...    Class C(B):        ...Lets test this function on the Dog class.<jupyter_code>import inspect

inspect.getmro(Dog)<jupyter_output><empty_output><jupyter_text>This is what we can expect since **Dog** inherits from **Pet** which doesn't have any parent classes besides `object`.The function `getmro` is pretty powerful. But, what if we were ok with just checking the immediate parent of a class? Whose my parent? `ClassName.__base__`<jupyter_code>Dog.__base__
Pet.__base__<jupyter_output><empty_output><jupyter_text>This works for us too. Now, lets do some inheritance testing. What if we wanted to explicitly check if **Dog** was a subclass of **Pet**? Inheritance Testing Option 1: `ParentClass.__subclasscheck__(ChildClass)`<jupyter_code>Pet.__subclasscheck__(Dog)<jupyter_output><empty_output><jupyter_text>`True` indicates that **Dog** is a subclass of **Pet**. Lets check the reverse.<jupyter_code>Dog.__subclasscheck__(Pet)<jupyter_output><empty_output><jupyter_text>`False` makes sense since **Pet** is not a subclass of **Dog**   Inheritance Testing Option 2: `instance(ChildClassObject,ParentClass)`In this one, we have to use an object as the first parameter and check whether the object is an instance of a class or of a subclass thereof.<jupyter_code>isinstance(d1,Pet)
isinstance(p1,Pet)
isinstance(p1,Dog)<jupyter_output><empty_output><jupyter_text>The first two in the above make sense since d1 is a dog instance which is a subclass of Pet and p1 is a Pet object.The last one is `False` because Pet object is not an instance of Dog but its parent. Lets take a look at another example.  Example Class: LabradorWe will define a new class **Labrador** that extends the **Dog** class.<jupyter_code>class Labrador(Dog):
    
    def __init__(self, name, age, color):
        super().__init__(name, age)
        self.color = color
    
    def __str__(self):
        return (self.name + " is a " + str(self.color) + "-colored " + str(self.age) + "-year(s) old labrador.")<jupyter_output><empty_output><jupyter_text>In the class definition of Labrador we decided to have slightly different `__init__` and `__str__` methods than the parent classes. When you declare a method in a child class with the same name as the one in the parent class, the child class method is said to be **overriding** the method in the parent class.Lets create a Labrador object and print it.<jupyter_code>lab1 = Labrador("Phoenicia",4,"brown")
print(lab1)<jupyter_output>Phoenicia is a brown-colored 4-year(s) old labrador.<jupyter_text>In the above, both the initialization and string conversion methods of the Labrador class are used and not of the parent classes.In the `__init__` function above, we used a function `super()`. What was that for?The super function can be used to gain access to inherited methods that have been overwritten in a class object. The line of code `super().__init__(name, age)` in the **Labrador** class executes the __init__ method of the Dog class. This was to reuse some of the earlier code for initializing the name and age of the Labrador. But, since Labrador has an additional instance variable color, we initialized it in the end. **Note: You don't need to supply `self` when you use `super()` in Python 3.x**Lets apply some of the earlier functions to see the parent-child relationships.<jupyter_code>print("The base class of Labrador is " + str(Labrador.__base__))

print("\nIs Labrador a subclass of Dog? "+ str(Dog.__subclasscheck__(Labrador)))

print("\nIs a Labrador instance an instance of Dog as well? " + str(isinstance(lab1,Dog)))

import inspect
print("\nAll the parent classes of Labrador are: " + str(inspect.getmro(Labrador)))<jupyter_output>The base class of Labrador is <class '__main__.Dog'>

Is Labrador a subclass of Dog? True

Is a Labrador instance an instance of Dog as well? True

All the parent classes of Labrador are: (<class '__main__.Labrador'>, <class '__main__.Dog'>, <class '__main__.Pet'>, <class 'object'>)<jupyter_text>Problem 1 (10 pts.): Complete the class definition below along with the 'Test cases' that follow.<jupyter_code>class Book:
    
    def __init__(self, isbn, title, author):
        self.isbn = isbn
        self.title = title
        self.author = author
        
    def __str__(self):
        return ("\nISBN: " + self.isbn + "\nTitle: " + self.title + "\nAuthor: " + self.author)
    
class KindleBook(Book):
    
    def __init__(self, isbn, title, author, price, file_size):
        super().__init__(isbn, title, author)
        # initializes the instance price attribute
        self.price = price
        
        # initializes the instance file_size attribute
        self.file_size = file_size
        
        
    def __str__(self):
        return (super().__str__() + "\nPrice: " + str(self.price) + "\nSize(MB): " + str(self.file_size))
        
class PaperBack(Book):
    
    def __init__(self, isbn, title, author, price, num_pages, shipping_weight):
        super().__init__(isbn, title, author)
        self.price = price
        self.num_pages = num_pages
        self.shipping_weight = shipping_weight

    def __str__(self):
        return (super().__str__() + "\nPrice: " + str(self.price) + "\nNumber of pages: " + str(self.num_pages) + 
               "\nShipping Weight: " + str(self.shipping_weight))

# Test Cases

# Create a book object 'book1' with ISBN=199957950X, Title = The Hundred-Page Machine Learning Book, Author = <NAME>
book1 = Book("199957950X","The Hundred-Page Machine Learning Book","<NAME>")

# prints the book1 object
print(book1)

# Creates a book object 'book2' with ISBN=B07F7LS2ZW, Title = Who Moved My Cheese?, Author = <NAME>
book2 = Book("B07F7LS2ZW", "Who Moved My Cheese?", "<NAME>")

# prints the book2 object
print(book2)

# Creates a KindleBook object 'kb2' with ISBN = B07F7LS2ZW, Title = Who Moved My Cheese? , 
# Author = <NAME>, price = $25, file size = 13MB
kb2 = KindleBook("B07F7LS2ZW", "Who Moved My Cheese?", "<NAME>", 25, 13)

# prints the kb2 object
print(kb2)

# Creates a PaperBack object 'pb2' with ISBN=B07F7LS2ZW, Title = Who Moved My Cheese?, 
# Author = <NAME>, price = $14, number of pages = 100, shipping weight = 3.5 lb
pb2 = PaperBack("B07F7LS2ZW", "Who Moved My Cheese?", "<NAME>", 14, 100, 3.5)

# prints the pb2 object
print(pb2)

# Prints all the parent classes of the class KindleBook
print("\nAll the parent classes of KindleBook are:", inspect.getmro(KindleBook))

# Displays only the immediate parent class of PaperBack
print(PaperBack.__base__)<jupyter_output>ISBN: 199957950X
Title: The Hundred-Page Machine Learning Book
Author: <NAME>

ISBN: B07F7LS2ZW
Title: Who Moved My Cheese?
Author: <NAME>

ISBN: B07F7LS2ZW
Title: Who Moved My Cheese?
Author: <NAME>
Price: 25
Size(MB): 13

ISBN: B07F7LS2ZW
Title: Who Moved My Cheese?
Author: <NAME>
Price: 14
Number of pages: 100
Shipping Weight: 3.5

All the parent classes of KindleBook are: (<class '__main__.KindleBook'>, <class '__main__.Book'>, <class 'object'>)
<class '__main__.Book'><jupyter_text>Problem 2 (8 pts.): Complete the class definition below along with the 'Test cases' that follow.<jupyter_code>class AudioBook(Book):
    
    def __init__(self, isbn, title, author, price, narrator, listening_length):
        # Using the super() initializes the isbn, title and author instance variables
        super().__init__(isbn, title, author)
        
        self.price = price
        self.narrator = narrator
        # initializes the listening_length instance variable
        self.listening_length = listening_length
        
        
    def __str__(self):
        return (super().__str__() + "\nPrice: $" + str(self.price) + "\nNarrator: " + str(self.narrator) +
               "\nDuration(min): " + str(self.listening_length))
    
    
class AudioMP3Book(AudioBook):
    def __init__(self, isbn, title, author, price, narrator, listening_length, file_size):
        super().__init__(isbn, title, author, price, narrator, listening_length)
        # initializes the file_size instance variable
        self.file_size = file_size
        

    def __str__(self):
        return (super().__str__() + "\nFilesize(MB): " + str(self.file_size))
        
class AudioCDBook(AudioBook):
    def __init__(self, isbn, title, author, price, narrator, listening_length, shipping_weight):
        super().__init__(isbn, title, author, price, narrator, listening_length)
        # initializes the shipping_weight instance variable
        self.shipping_weight = shipping_weight
        
    def __str__(self):
        # returns a string representation of class that includes all the instance attributes 
        return super().__str__()  + "\nShipping Weight(LB): " + str(self.shipping_weight)

# Test Cases
# Create an AudioBook object 'ab1' with ISBN=199957950X, Title = The Hundred-Page Machine Learning Book, Author = <NAME>,
# price = 29.99 , narrator = <NAME>, listening_length = 189
ab1 = AudioBook("199957950X","The Hundred-Page Machine Learning Book","<NAME>",
                29.99,"<NAME>",189)

# print the ab1 object
print(ab1)

# Creates an AudioMP3Book object 'amp3' with ISBN=199957950X, Title = The Hundred-Page Machine Learning Book, Author = <NAME>,
# price = 31.99 , narrator = <NAME>, listening_length = 189 mins, file_size = 126 MB
amp3 = AudioMP3Book("199957950X","The Hundred-Page Machine Learning Book","<NAME>",
                    31.99,"<NAME>",189,126)

# prints the amp3 object
print(amp3)

# Creates an AudioCDBook object 'acd1' with ISBN=199957950X, Title = The Hundred-Page Machine Learning Book, Author = <NAME>,
# price = 31.99 , narrator = <NAME>, listening_length = 189 mins, shipping_weight = 1.5 LB
acd1 = AudioCDBook("199957950X","The Hundred-Page Machine Learning Book","<NAME>",31.99,
                  "<NAME>", 189, 1.5)

# print the acd1 object
print(acd1)

# Checks whether acd1 is an instance of class Book
print("\nIs acd1 an instance of class Book?", isinstance(acd1, Book))

# print all the parent classes of AudioCDBook
print(inspect.getmro(AudioCDBook))<jupyter_output>ISBN: 199957950X
Title: The Hundred-Page Machine Learning Book
Author: <NAME>
Price: $29.99
Narrator: <NAME>
Duration(min): 189

ISBN: 199957950X
Title: The Hundred-Page Machine Learning Book
Author: <NAME>
Price: $31.99
Narrator: <NAME>
Duration(min): 189
Filesize(MB): 126

ISBN: 199957950X
Title: The Hundred-Page Machine Learning Book
Author: <NAME>
Price: $31.99
Narrator: <NAME>
Duration(min): 189
Shipping Weight(LB): 1.5

Is acd1 an instance of class Book? True
(<class '__main__.AudioCDBook'>, <class '__main__.AudioBook'>, <class '__main__.Book'>, <class 'object'>)
{'fraction_non_alphanumeric': 0.10289903365544818, 'fraction_numerical': 0.02165944685104965, 'mean_word_length': 4.219478260869566, 'pattern_counts': {'":': 0, '<': 106, '<?xml version=': 0, '>': 106, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '462315', 'license': 'MIT', 'n_tokens_mistral': 4847, 'n_tokens_neox': 4527, 'n_words': 1778, 'path': 'Fall2019/MSDS430/Mod9/KurczKerryModule9PythonAssignment.ipynb', 'repo_name': 'kbridge14/regression_things', 'size': '15224'}