<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Introduction to objects and classes in Python
We will touch upon some basic aspects, including
 - code reuse
 - abstraction
 - Encapsulation
 - subclasses and hierarchies
Step1:  In the above examples, it becomes clear that there is much repetition, and we can make the code more compact. Let us abstract common functionality into an abstract class.
Step3:  Iterators
How do I add an iteration facility to my own objects so that they can be used with for loops?
Step4:  Generators
Use functions to create iterators, instead of classes
Step5:  Summing series
We want to efficiently find the sum
$$
\sum_{n = 0}^N \frac{1}{n^2}
$$
As $N$ becomes larger, this becomes $\frac{\pi^2}{6}$.
Step6:  The key thing to note is that this is much more efficient than generating a list of terms in memory and summing it. That is, more efficient than
Step7:  Decorators
Alter the behaviour of functions (somewhat)
Example
<ASSISTANT_TASK:>
Python Code:
class Dog:
    def __init__(self, name):
        self.age = 0
        self.name = name
        self.noise = "Woof!"
        self.food = "dog biscuits"
    
    def make_sound(self):
        print(self.noise)
        
    def eat_food(self):
        print("Eating " + self.food + ".")
        
    def increase_age(self, n = 1):
        self.age = self.age + n
d1 = Dog('Buster')
d1.make_sound()
d2 = Dog('Tiger')
d2.noise = 'Bark'
d2.make_sound()
d1.make_sound()
d1.eat_food()
d1.increase_age(3)
print(d1.age)
class Cat:
    def __init__(self, name):
        self.age = 0
        self.name = name
        self.noise = "Meow!"
        self.food = "cat food"
    
    def make_sound(self):
        print(self.noise)
        
    def eat_food(self):
        print("Eating " + self.food + ".")
        
    def increase_age(self, n = 1):
        self.age = self.age + n
c1 = Cat('Harvey')
c1.make_sound()
c1.eat_food()
Explanation: Introduction to objects and classes in Python
We will touch upon some basic aspects, including
 - code reuse
 - abstraction
 - Encapsulation
 - subclasses and hierarchies
End of explanation
from abc import ABCMeta, abstractmethod
class Mammal(metaclass=ABCMeta):
    @abstractmethod
    def __init__(self, name):
        self.age = 0
        self.name = name
        self.noise = "None!"
        self.food = "none"
    
    def make_sound(self):
        print(self.name + " says " + self.noise)
        
    def eat_food(self):
        print(self.name + " is eating " + self.food + ".")
        
    def increase_age(self, n = 1):
        self.age = self.age + n
class Dog(Mammal):
    def __init__(self, name):
        super(Dog, self).__init__(name)
        self.noise = "Bark!"
        self.food = "dog biscuits"
class Cat(Mammal):
    def __init__(self, name):
        super(Cat, self).__init__(name)
        self.noise = "Meow!"
        self.food = "cat food"
d = Dog("Buster")
c = Cat("Harvey")
d.make_sound()
c.make_sound()
c.eat_food()
m = Mammal("Name")
m.make_sound()
m.eat_food()
import sys
print(sys.version)
animal_house = [Dog("MyDog" + str(i))
                for i in range(1, 5)]
animal_house.extend([Cat("MyCat" + str(i))
                     for i in range(1, 5)])
for i in animal_house:
    i.make_sound()
Explanation: In the above examples, it becomes clear that there is much repetition, and we can make the code more compact. Let us abstract common functionality into an abstract class.
End of explanation
class Reverse:
    Iterator for looping over a sequence backwards.
    def __init__(self, data):
        self.data = data
        self.index = len(data)
    def __iter__(self):
        return self
    def next(self): # def next(self): in Python 2!
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]
rev = iter(Reverse([10, 30, 200, 0.0, 'ABC']))
for i in rev:
    print(i)
Explanation: Iterators
How do I add an iteration facility to my own objects so that they can be used with for loops?
End of explanation
def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
for char in reverse("Madam, I'm Adam"):
    print(char)
Explanation: Generators
Use functions to create iterators, instead of classes
End of explanation
import math
def series_sum(max_terms=1000):
    n = 0
    while n < max_terms:
        n = n + 1
        yield 1.0 / n**2
print(sum(series_sum(100000)) - math.pi**2 / 6)
Explanation: Summing series
We want to efficiently find the sum
$$
\sum_{n = 0}^N \frac{1}{n^2}
$$
As $N$ becomes larger, this becomes $\frac{\pi^2}{6}$.
End of explanation
print(sum([1.0 / i**2 for i in range(1, 10000)]))
Explanation: The key thing to note is that this is much more efficient than generating a list of terms in memory and summing it. That is, more efficient than
End of explanation
def add_numbers(a, b):
    return a + b
def arg_wrapper(f, *args, **kwargs):
    print("The function arguments are:")
    print(args)
    print(kwargs)
    print("Now running the function!")
    return f(*args, **kwargs)
#print(add_numbers(1, 2))
#print(arg_wrapper(add_numbers, 1, 2))
def myfunction(name='Test', age=30):
    print("Name: %s, Age: %d" % (name, age))
arg_wrapper(myfunction, name='Harvey', age=3)
import time
def timing_function(some_function):
    def wrapper():
        t1 = time.time()
        some_function()
        t2 = time.time()
        return "Time it took to run the function: " + str((t2 - t1)) + "\n"
    return wrapper
@timing_function
def my_function():
    num_list = []
    for num in (range(0, 10000)):
        num_list.append(num)
    print("\nSum of all the numbers: " + str((sum(num_list))))
my_function()
Explanation: Decorators
Alter the behaviour of functions (somewhat)
Example:
I wish to print the arguments to a function before I call it.
One way: Edit the function. But it's clumsy!
Better way, wrap it in another function:
End of explanation
<END_TASK>