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.


In [ ]:
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

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.


In [ ]:
jyry = Student("Jyry", 1984, interest="Python")

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.


In [ ]:
jyry.say_hi()

In [ ]:
print(jyry.birthyear)

One can create multiple objects that all have their own identity, even though they share some variables.


In [ ]:
tuomas = Student("Tuomas", 1984, interest="Java")
tuomas.say_hi()

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).


In [ ]:
tuomas == jyry

Python permits the programmer to edit objects without any access control mechanics. See for example.


In [ ]:
jyry.interest = "teaching"
jyry.say_hi()

Figuring out an object

Opening a file using the open method returns an object.


In [ ]:
fobj = open("../data/grep.txt")

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.


In [ ]:
print(fobj)

In [ ]:
dir(fobj)

In [ ]:
help(jyry.say_hi)

In [ ]:
jyry.say_hi.__doc__

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.


In [ ]:
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]

The container-class can exhibit at least two different exceptions.


In [ ]:
container = Container()
container.put([1, 2, 3], "example")

In [ ]:
container.get("not_in_it")

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.


In [ ]:
try:
    container = Container()
    container.put([1,2,3], "value")
except TypeError as err:
    print("Stupid programmer caused an error: " + str(err))

A try-except may contain a finallyblock, which is always guaranteed to execute.

Also, it is permissible to catch multiple different errors.


In [ ]:
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

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.


In [ ]:
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