<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
Step4:  Objects
An object is a combination of data and methods associated with the data. 
Here's a concrete example with docstrings explaining what goes on in each part.
Step5:  The above construct is a class, which is to say a model for creating objects.
To create an object we say we instantiate a class.
Step6:  Now we have an object called "jyry", which has the value
s listed above. We can call methods of the object and access the variables associated with the object.
Step7:  One can create multiple objects that all have their own identity, even though they share some variables.
Step8:  Typically object comparison is done using the same syntax as for basic types (which, by the way are objects too in Python).
If you want to implement special logic for comparisons in your own classes, look up magic methods either online or in another part of this introduction. It is a very common task and helps people who use your code (i.e. you).
Step9:  Python permits the programmer to edit objects without any access control mechanics. See for example.
Step10:  Figuring out an object
Opening a file using the open method returns an object.
Step11:  How can we find things out about this object? Below are a few examples
Step12:  Exceptions
In Python, exceptions are lightweight, i.e. handling them doesn't cause a notable decrease in performance as happens in some languages.
The purpose of exceptions is to communicate that something didn't go right. The name of the exception typically tells what kind of error ocurred and the exception can also contain a more explicit message.
Step13:  The container-class can exhibit at least two different exceptions.
Step14:  Who should worry about the various issues is a good philosophical question. We could either make the Container-class secure in that it doesn't raise any errors to whoever calls it or we could let the caller worry about such errors.
For now let's assume that the programmer is competent and knows what is a valid key and what isn't.
Step15:  A try-except may contain a finallyblock, which is always guaranteed to execute.
Also, it is permissible to catch multiple different errors.
Step16:  There is also syntax for catching multiple error types in the same catch clause.
The keyword raise is used to continue error handling. This is useful if you want to log errors but let them pass onward anyway.
A raise without arguments will re-raise the error that was being handled.
<ASSISTANT_TASK:>
Python Code:
class Student(object):
    
    The above states that the code-block (indented area) below will define a 
    class Student, that derives from a class called 'object'. Inheriting from 'object' is S
    
    
    
    def __init__(self, name, birthyear, interest=None):
        __init__ is special method that is called when instantiating the object. 
        Typically the methods can then be used to 
        self.name = name
        self.birthyear = birthyear
        self.interest = interest
        
    def say_hi(self):
         This is a classical example of a function that prints something. 
        The more complex your system, the less likely it is that it is a good idea to print anything other than 
        warnings from whithin your classes.
        if not self.interest:
            print("Hi, my name is " + self.name + "!")
        else:
            print("Hi, my name is " + self.name + " and I'm interested in " + self.interest + ".")
            
    def get_age(self):
         This is a much more style-pure example of classes. 
        Recording a birthyear instead of age is a good idea because next year we'll all be a year older.
        However requiring everyone who uses your class is impolite and would lead to duplicate code. 
        Doing it once and asking everyone to use that implementation reduces code complexity and improves 
        maintainability.
        
        import datetime
        return datetime.datetime.now().year-self.birthyear
Explanation: Objects
An object is a combination of data and methods associated with the data. 
Here's a concrete example with docstrings explaining what goes on in each part.
End of explanation
jyry = Student("Jyry", 1984, interest="Python")
Explanation: The above construct is a class, which is to say a model for creating objects.
To create an object we say we instantiate a class.
End of explanation
jyry.say_hi()
print(jyry.birthyear)
Explanation: Now we have an object called "jyry", which has the value
s listed above. We can call methods of the object and access the variables associated with the object.
End of explanation
tuomas = Student("Tuomas", 1984, interest="Java")
tuomas.say_hi()
Explanation: One can create multiple objects that all have their own identity, even though they share some variables.
End of explanation
tuomas == jyry
Explanation: Typically object comparison is done using the same syntax as for basic types (which, by the way are objects too in Python).
If you want to implement special logic for comparisons in your own classes, look up magic methods either online or in another part of this introduction. It is a very common task and helps people who use your code (i.e. you).
End of explanation
jyry.interest = "teaching"
jyry.say_hi()
Explanation: Python permits the programmer to edit objects without any access control mechanics. See for example.
End of explanation
fobj = open("../data/grep.txt")
Explanation: Figuring out an object
Opening a file using the open method returns an object.
End of explanation
print(fobj)
dir(fobj)
help(jyry.say_hi)
jyry.say_hi.__doc__
Explanation: How can we find things out about this object? Below are a few examples:
printing calls the __str__()-method of the object, which should return a (more or less) human-readable definition of the object
dir() lists the attributes of an object, that is to say functions and variables associated with it
the help-function attempts to find the docstring for your function
the __doc__ attribute of object members contains the docstring if available to the interpreter
This list is not comprehensive.
End of explanation
class Container(object):
    
    def __init__(self):
        self.bag = {}
        
    def put(self, key, item):
        self.bag[key] = item
        
    def get(self, key):
        return self.bag[key]
Explanation: Exceptions
In Python, exceptions are lightweight, i.e. handling them doesn't cause a notable decrease in performance as happens in some languages.
The purpose of exceptions is to communicate that something didn't go right. The name of the exception typically tells what kind of error ocurred and the exception can also contain a more explicit message.
End of explanation
container = Container()
container.put([1, 2, 3], "example")
container.get("not_in_it")
Explanation: The container-class can exhibit at least two different exceptions.
End of explanation
try:
    container = Container()
    container.put([1,2,3], "value")
except TypeError as err:
    print("Stupid programmer caused an error: " + str(err))
Explanation: Who should worry about the various issues is a good philosophical question. We could either make the Container-class secure in that it doesn't raise any errors to whoever calls it or we could let the caller worry about such errors.
For now let's assume that the programmer is competent and knows what is a valid key and what isn't.
End of explanation
try:
    container = Container()
    container.put(3, "value")
    container.get(3)
except TypeError as err:
    print("Stupid programmer caused an error: " + str(err))
except KeyError as err:
    print("Stupid programmer caused another error: " + str(err))
finally:
    print("all is well in the end")
    
# go ahead, make changes that cause one of the exceptions to be raised
Explanation: A try-except may contain a finallyblock, which is always guaranteed to execute.
Also, it is permissible to catch multiple different errors.
End of explanation
try:
    container = Container()
    container.put(3, "value")
    container.get(5)
except (TypeError, KeyError)  as err:
    print("please shoot me")
    if type(err) == TypeError:
        raise Exception("That's it I quit!")
    else:
        raise
Explanation: There is also syntax for catching multiple error types in the same catch clause.
The keyword raise is used to continue error handling. This is useful if you want to log errors but let them pass onward anyway.
A raise without arguments will re-raise the error that was being handled.
End of explanation
<END_TASK>