<jupyter_start><jupyter_text>LSESU Applicable Maths Python Lesson 5 22/11/16 Today we will be learning about* Introductory Object Oriented Programming (OOP)    * What is a class?    * Creating a class and initialising the class    * Methods of a class    * Magic Methods    * Using a class in a program* A peek at Inheritance ** Recap from Week 4*** Lists* Tuples```def my_swap_function(a,b):    return (b,a)```* Dictionaries```for country in europe:    if country['speak_german']:        for key, value in country.items():            print('{}\t{}'.format(key,value))        print()```  OOP in Python - "Everything is an object" Object Oriented structure is one of the most popular way people who write software will develop a program. You might have also heard of Functional or Dynamic programming which have their own benefits but we will only focus on OOP. Before we start writing any code, we have to go through [some definitions](https://www.tutorialspoint.com/python/python_classes_objects.htm).<jupyter_code># We've interacted with objects and classes before when we used the type() function

my_list = [1,2,3,4,5]
type(my_list)
# We looked at class methods before when using the .items() method of a dictionary

my_dict = {'name':'<NAME>','age':154,'favourite_subject':'Maths'}

print(my_dict.items())
print()

for key,value in my_dict.items():
    print('{}:\t{}'.format(key,value))<jupyter_output><empty_output><jupyter_text>Creating a class Defining your own classes allows you to create a completely customisable data type for your own needs. This will be guided by the problem you are solving, you might need to create a class `Cell()` for a computational biology task, when you are modelling lots of kinds of cells. Or you might create a class `Location()` for a location extraction task, which stores the address, latitude and longtitude and many other features of a location (I've done something very similar to this working with production Python!).The formal definition we just looked at:    * Class: A user-defined prototype for an object that defines a set of attributes that characterize any object of the class. The attributes are data members (class variables and instance variables) and methods, accessed via dot notation.<jupyter_code># Let me create a class Dog

# This is the how you declare a class
class Dog(object):
    # You can then declare Data members of the class Dog()
    name = ''
    age = ''
    breed = ''
    animal = 'Dog'
# Creating object of class Dog is now as easy as creating any of the standard variables
my_dog = Dog()
my_dog.name = 'Lucky'
my_dog.age = '3'
my_dog.breed = 'Golden Retriever'

# This is not very useful at all, look at what happens when I print this...
print(my_dog)<jupyter_output><empty_output><jupyter_text>Initialising a class with a constructor We can initialise our class in a smarter way using a **constructor**, which means we can initialise the Dog() object on one line as below. The **self** keyword you see when making a class is the way that we refer to the specific instance of the class we want _inside_ the class itself.<jupyter_code>class Dog(object):
    # You can then declare Data members of the class Dog()
    animal = 'Dog'
    
    # This is similar to all the other functions you have written but the args start with self
    def __init__(self,name,age,breed):
            self.name = name
            self.age = age
            self.breed = breed
# We now create 2 instances of the Dog class which will have unique names, ages and breeds
my_dog = Dog('Lucky',3,'Golden Retriever')
my_other_dog = Dog('Hamilton',2,'<NAME>')

print(my_dog.name)
print(my_other_dog.breed)

# However the animal attribute is shared among ALL instances of the dog class
print(my_dog.animal)
print(my_other_dog.animal)
# We can also make the animal variable "pseudo private" so it is not easily accessible. 
# You won't ever really have to do this but it's a demonstration of how to get it done

class Dog(object):
    # The __ before the variable means someone cannot type my_dog.animal to get to it
    __animal = 'Dog'
    
    def __init__(self,name,age,breed):
            self.name = name
            self.age = age
            self.breed = breed
            
my_dog = Dog('Lucky',3,'Golden Retriever')

# This isn't going to work as the variable name is "mangled"
print(my_dog.__animal)

# You need a _ClassName before to get to the variable
print(my_dog._Dog__animal)<jupyter_output><empty_output><jupyter_text>Creating methods of a class First things first. When we tried to print the Dog() class we didn't get something that wasentirely useful. This is because when we want to print our own classes, we need to tell Pythonhow to print it, and we do that using the `__str__` method. One note: the `__str__` method and the `__repr__` method are very similar. Keep in mind that `__repr__` is meant to be **unambiguous** and the `__str__` method is just meant to be readable<jupyter_code>class Dog(object):
    __animal = 'Dog'
    
    def __init__(self,name,age,breed):
            self.name = name
            self.age = age
            self.breed = breed
            
    def __str__(self):
        return 'Hi my name is {} and I am a {} year old {}'.format(self.name,self.age,self.breed)
    
my_other_dog = Dog('Hamilton',2,'<NAME>')
print(my_other_dog)<jupyter_output><empty_output><jupyter_text>Any method between the `__example__` double underscores is a special method called a magic method which we will look at further below. We can also define other methods as function attributes of the class which can take arguments like an isolated function<jupyter_code>class Dog(object):
    __animal = 'Dog'
    
    def __init__(self,name,age,breed,favourite_toy):
            self.name = name
            self.age = age
            self.breed = breed
            self.favourite_toy = favourite_toy
            
    def __str__(self):
        return 'Hi my name is {} and I am a {} year old {}'.format(self.name,self.age,self.breed)
    
    def age_in_dog_years(self):
        return 7*self.age
    
    def is_favourite_toy(self,toy):
        if toy==self.favourite_toy:
            return 'That is my favourite toy!'
        else:
            return 'That is not my favourite toy :('
        
my_other_dog = Dog('Hamilton',2,'<NAME>','<NAME>')
print(my_other_dog)
print()

# We can access the method using dot notation
print(my_other_dog.age_in_dog_years())
print()

# Methods of classes can also take arguments
print(my_other_dog.is_favourite_toy('Stuffed Bear'))
print()
print(my_other_dog.is_favourite_toy('Tennis Ball'))<jupyter_output><empty_output><jupyter_text>Magic Methods Magic methods (like `__str__`) are what allows your own classes to behave similarly to the Python built in types. Instinctively it is hard to image how to do `dog_a > dog_b`, but if we use magic methods then this behaviour is definable.A detailed list of the definitions of the different kind of Magic Methods can be found [here](http://www.python-course.eu/python3_magic_methods.php)<jupyter_code>class Dog(object):
    __animal = 'Dog'
    
    def __init__(self,name,age,breed,favourite_toy):
            self.name = name
            self.age = age
            self.breed = breed
            self.favourite_toy = favourite_toy
           
    # THESE ARE ALL MAGIC METHODS
    def __str__(self):
        return 'Hi my name is {} and I am a {} year old {}'.format(self.name,self.age,self.breed)
    
    # I've chosen to compare on a Dog's age but this is up to the designer of the class.
    def __lt__(self,other):
        return self.age < other.age
    
    def __le__(self,other):
        return self.age <= other.age
    
    def __gt__(self,other):
        return self.age > other.age
    
    def __ge__(self,other):
        return self.age >= other.age    
    
    def __eq__(self,other):
        return (self.age == other.age)and(self.name==other.name)and(self.breed==other.breed)
    
    def __ne__(self,other):
        return not self==other
    
    # THESE ARE STANDARD ATTRIBUTE METHODS
    def age_in_dog_years(self):
        return 7*self.age
    
    def is_favourite_toy(self,toy):
        if toy==self.favourite_toy:
            return 'That is my favourite toy!'
        else:
            return 'That is not my favourite toy :('

my_dog = Dog('Lucky',1,'Golden Retriever','Stick')
my_other_dog = Dog('Hamilton',2,'Pembroke Corgi','Tennis Ball')
my_other_other_dog = Dog('Socks',4,'Pug','Giant Pillow')

# We can now compare objects of the Dog class using their base type attributes
print(my_dog > my_other_dog)
print(my_dog <= my_other_other_dog)
print(my_dog==my_other_dog)
print(my_dog!=my_other_dog)<jupyter_output><empty_output><jupyter_text>Challenge for today * **Create a class Human() which has attributes 'name', 'age' and 'height' (in centimetres). *** **Add a constructor to the class to be able to create Human objects in a program.*** **Add magic methods to the class to be able to compare Humans by age*** **Add a method to class Human() to return the age of the Human in Dog years (1 human year = 7 dog years)**The definition is given to you, the final block of code is the Test block. **Do not modify the test block**, use it to test your code, running it when you think your class is ready.<jupyter_code>class Human(object):
    #Everything goes here!<jupyter_output><empty_output><jupyter_text>This is the test block, run this to evaluate your code.<jupyter_code>joe = Human('Joe',19,174)
lisa = Human('Lisa',23,181)
chu = Human('Chu',23,160)

# Test the attributes set by the Constructor
try:
    print(joe.name)
    print(lisa.age)
    print(chu.height)
except AttributeError as e:
    print('Your attributes are not working correctly :(\n')
else:
    print('All your attributes are working correctly!\n')
    
# Test the Magic methods 
try:
    print(joe < lisa)
    print(lisa <= lisa)
    print(chu == lisa)
    print(joe > chu)
    print(lisa >= chu)
except TypeError as e:
    print('Your magic methods are not working correctly :(\n')
else:
    print('All your magic methods are working correctly!\n')
    
# Test the age_in_dog_years() function
try:
    print('{}\'s age in dog years is {}'.format(joe.name,joe.age_in_dog_years()))
    print('{}\'s age in dog years is {}'.format(lisa.name,lisa.age_in_dog_years()))
    print('{}\'s age in dog years is {}'.format(chu.name,chu.age_in_dog_years()))
except AttributeError as e:
    print('Your method is not working correctly :(\n')
else:
    print('Your method is working correctly!\n')
    print('Congratulations! Your class is all correct!')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09371181502020867, 'fraction_numerical': 0.004323714634834101, 'mean_word_length': 3.6523830345430697, 'pattern_counts': {'":': 0, '<': 43, '<?xml version=': 0, '>': 44, 'https://': 1, 'lorem ipsum': 0, 'www.': 2, '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': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '412050', 'license': 'MIT', 'n_tokens_mistral': 3291, 'n_tokens_neox': 3148, 'n_words': 1242, 'path': 'lesson5.ipynb', 'repo_name': 'trsherborne/learn-python', 'size': '10675'}