Sunday, February 12, 2017

Objects and Class

It’s time to learn about core functionality of Python programming language and that is Python objects and classes. In this lesson you’ll learn how to create it and use it in your Python program. As mentioned many times before Python is an object oriented programming language just like C++ and many others. Just to clarify the difference between procedural and object oriented programming the emphasis in procedural programming is on functions while in object oriented programming the main focus are objects.
Collection of data (variables) and methods (functions) that act on those data are what make objects. We can think of a class as a sketch or prototype. Class is like a sketch of a car because it contains all the details about tires, engine, lights, seats … Based on this description a car is built and the car is an object.

So if the class is a blueprint for building a car we can conclude that we can create many objects from this class. You will find in literature that object is an instance of a class and the process of creating this object is called instantiation.

How to Create a Class? 

Now let’s start with something simple. Let’s create a simple class in Python. To define a class use the keyword class. After the class, give a name of the class and the first line after class declaration is usually used for a brief description about the class. This line (string) is called docstring and while it’s not mandatory it is usually recommended (just to know why you created this class).
class myFirstclass:
      "This line is a docstring and it usualy say something about a class"
      Pass
A class creates a new local namespace where all its attributes are defined. Attributes may be data or functions. There are special attributes that can be used and they begin with double underscore ‘__’. An example of that attribute is __doc__ and it gives us the docstring of that class.
>>> class myFirstclass:
...       "This line is a docstring and it usualy say something about a class"
...       pass
...
>>> myFirstclass.__doc__
'This line is a docstring and it usualy say something about a class'


When we create a class, a new class object is created with the same name. This class object allows us to access the different attributes as well as to instantiate new objects of that class.
class mySecondclass:
    "This is my second class"
    a = 50
    def function(self):
        print('Hello')

print 'a = ' + str (mySecondclass.a)
a = 50
print mySecondclass.function
print "DOCSTRING = " + str(mySecondclass.__doc__)
DOCSTRING = This is my second class

 How to Create an Object? 

We saw that the class object could be used to access different attributes. It can also be used to create new object instances (instantation) of that class. The procedure of creating an object is similar to a function call.
obj = mySecondclass()
This command will create a new instance of an object named obj. We can access attributes of objects using the object name as prefix.
class mySecondclass:
    "This is my second class"
    a = 50
    def function(self):
        print('Hello')
obj = mySecondclass()
"
"
Self - parameter is parameter that will always be used as reference back to the object itself. The object itself is passed as first argument. So obj.function() translates to mySecondclass.func(obj). In general calling a method with a list of n arguments is equivalent to calling the corresponding function with an argument list that is created by inserting methods object before the first argument. For that reason the first argument of the function in the class must be object itself. 



Namespaces and Scope

Variable name also called identifier is simply a name given to objects. Everything in Python is an object. So name is a way to access specific object.  Let’s define a variable a and assign value of 5 (a = 5) . Here 5 is an object stored in primary memory or RAM (RAM is short for Random Access Memory) and a is the name we associate it with. We can get the address of some object through the built-in function id().
>>> a = 5
>>> print 'id(a) = ' + str(id(a))
id(a) = 8420656

Id value may be different on your computer. Besides variable a we can also check out the memory address of variable 5 .
>>> a = 5
>>> print 'id(a) = ' + str(id(a))
id(a) = 8420656
>>> print 'id(5) = ' + str(id(5))
id(5) = 8420656

As you can see they both refer to the same object. Now we will add a with 1 and then check the id() of variable a, and it’s value, and finally we’ll define new variable b and assign value of 5.
>>> a = 5
>>> print 'id(a) = ' + str(id(a))
id(a) = 8420656
>>> print 'id(5) = ' + str(id(5))
id(5) = 8420656
>>> a = a + 1
>>> print 'id(a) = ' + str(id(a))
id(a) = 8420644
>>> print 'id(6) = ' + str(id(6))
id(6) = 8420644
>>> b = 5
>>> print 'id(b) = ' + str(id(b))
id(b) = 8420656
First we’ve created object 5 and the name a is associated with it, when we add one to variable a, then a new object 6 is created and now a associates with this object. Id(a) and id(6) have the same value. When we created object 5 and assign value of b to it and then check it’s id. As you can see it has the same value as the number 5.
This is efficient as Python doesn’t have to create a new duplicate object. This property makes Python very powerful so name could refer to any type of object.
>>> a = 5
>>> print 'id(a) = ' + str(id(a))
id(a) = 8420656
>>> a = 'Hello World!'
>>> print 'id(a) = ' + str(id(a))
id(a) = 38299392
>>> a = [1, 2, 3, 4, 5, 6]
>>> print 'id(a) = ' + str(id(a))
id(a) = 38366864
Three different types of objects have been assign to variable a and each time the id value was different, because the data is temporarily stored on different memory address.
Functions are also objects, so the name of the function can refer to them as well. Now we’ll define a function call printHello(). From the name of the function you can guess what the function does.
>>> def printHello():
...     print 'Hello'
...
>>> a = printHello()
Hello

Our name a can refer to a function ad we can call the function through it, pretty neat. 

What is a namespace?

Now that you’ve understood what names are, we’re moving on to the concept of namespaces. Strictly speaking a namespace is a collection of names. In Python you can imagine a namespaces as a mapping of every name, you have defined, to corresponding objects. Different namespaces can co-exist at a given time but are completely isolated. When we start Python a namespace containing all the built-in names is created and exists as long as we don’t exit the Python.
Built-in function such as id(), print(), type(), list(), dict()  and others are always at disposal to us from any part of the program. Each module creates its own global namespace. These different namespace are isolated so the same name that may exist in different modules do not collide.
Modules can have various functions and classes. A local namespace is created when a function is called, which has all the names defined in it. Similar, is the case with class

What is scope?
Although there are various unique namespaces defined, we may not be able to access all of them from every part of the program. It’s time to introduce variable scope.
Scope is a portion of the program from where a namespace can be accessed directly without any prefix. At any given moment there are at least three nested scopes.
  1. Scope of the current function which has local names
  2. Scope of the module which has global names
  3. Outermost scope which has built-in names.

When a reference is made inside the function, the name is searched in the local namespace, then in the global namespace and finally in the built-in namespace. If we have function inside the function which is called a nested function then a new scope is nested inside the local scope. The example is shown below.
>>> def outerfunction():
...     b = 20
...     def innerfunction():
...         c = 30
...
>>> a = 10
In previous example the variable a is in the global namespace, variable b is in the local namespace of outerfunction() and c is in the nested local namespace of innerfunction(). When we’re inside the innerfunction(), variable c is local variable, while variable b is nonlocal and a is global variable. We can read as well as assign new values to c but can only read b and c from innerfunction(). If we try to assign as a value to b, a new variable b is created in the local namespace which is different than the nonlocal b. Same thing happen when we assign a value to a. However, if we declare a as global, all the reference and assignment go to global a. Similarly, if we want to rebind the variable b, it must be declared as nonlocal. The following example will further clarify this.
>>> def outerfunction():
...     a = 20
...     def innerfunction():
...         a = 30
...     print 'a = ', a
...
>>> outerfunction()
a =  20









Assertions

An assertion is a sanity-check that you can turn on or turn off when you have finish testing the program. An expression is tested, and if the result comes up false, an exception is raised. Assertion are carried out through use of assert statement.
print 1
assert 2 + 2 == 4
print 2
assert 1 + 1 == 3
print 3
1
2
AssertionError

Good practice is to place assertions at the start of a function to check for valid input and after a function call to check for valid output.
Example: what is the highest number printed by this code ?
print 0
assert 'h' != 'w'
print 1
assert False
print 2
assert True
print 3
0
1
AssertionError

Assertion can take a second argument that is passed to the AssertionError raised if the assertion fails. AssertionError expectations can be caught and handled like another exception using the try-except statement, but if not handled, this type of exception will terminate the program.
a = - 10
assert(a >= 0), 'Colder than absolute zero'
AssertionError: Colder than absolute zero

Example: Fill in the blanks to define function that takes one argument. Assert the argument to be positive.
___ function1(x):
    _____ x > 0, 'Error!'
    _____ x

userinput = _________ ("Enter a value: ")
val = int(_________)
print function1(______)   
First run:
Enter a value: -10
AssertionError: Error!

Second run:
Enter a value: 10
10
None


Rasing Exception

You can use raise exception by using the raise statement.
print 1
raise ValueError
print 2
1
ValueError

Example: Which error occur in this code?
try:
    print 1/0
except ZeroDivisionError:
    raise ValueError
ZeroDivisionError and ValueError

Exceptions can be raised with arguments that give detail about them. For example:
name = '12345'
raise NameError('Invalid name!')
NameError: Invalid name!

In except blocks, the raise statement can be used without arguments to re-raise whatever exception occurred.
try:
    num = 5/0
except:
    print 'An error occurred'
    raise
An error occurred
ZeroDivisionError: integer division or modulo by zero


Exceptions Handling

To handle exceptions, and to call code when an exception occurs, you can use try/except statement. The try block contains code that might throw an exceptions. If that exception occurs, the code in the try block stops being executed, and the code in the except block is run. If no error occurs, the code in the except doesn’t run.
try:
    a = 7
    b = 0
    print(a/b)
    print('Done calculation')
except ZeroDivisionError:
    print('An error occured due to the zero division')
An error occured due to the zero division

In the code above, the except statement defines the type of exception to handle (in our case, the ZeroDivisonError). We can expand this example with the raw_input function in order to input numbers we want.
userinput1 = raw_input('Enter first value: ')
userinput2 = raw_input('Enter second value: ')
try:
    a = int(userinput1)
    b = int(userinput2)
    print(a/b)
    print('Done calculation')
except ZeroDivisionError:
    print('An error occured due to the zero division')
except ValueError:
    print("You've entered a non-integer value.")
First run:
Enter first value: kill

Enter second value: bill
You've entered a non-integer value.

Second run:
Enter first value: 5

Enter second value: 6
0
Done calculation


Example: What is the output of this code ?
try:
    variable = 10
    print(10/2)
except ZeroDivisionError:
    print('Error')
print('Finished')
5
Finished

A try statement can have multiple different except blocks to handle different exceptions. Multiple exceptions can also be put into a single except block using parenthesis, to have the except block handle all of them.
try:
    a = 10
    print a + 'Hello'
    print a/2
except ZeroDivisionError:
    print 'Divide by zero'
except (ValueError, TypeError):
    print 'Error occured'
Error occured

Example: What is the output of this code?
try:
    a = 30
    print a / 0
    print 'the meaning of life'
except(ValueError, TypeError):
    print 'ValueError or TypeError occured'
except ZeroDivisionError:
    print 'Divide by zero'
Divide by zero
An except statement without any exception specified will catch all errors. These should be used sparingly, as they can catch up unexpected errors and hid programming mistakes.
try:
    word = 'SPAM'
    print word/0
except:
    print 'An error occurred'
An error occurred

Exception handling is particularly useful when dealing with user input.
Example: fill in the blanks to handle all possible exceptions.
___:
    a = _____(':')
    b = input(':')
    print float(a)/_____(b)
_____:
    _____ 'Invalid input'
:5

:6
0.833333333333

To ensure that code runs no matter what errors occur, you can use a finally statement. The finally statement is placed is placed at the bottom of a try/except statement. Code within a finally statement always runs after execution of the code in the try, and possibility in the except blocks.
try:
    print 'Hello'
    print 1/0
except ZeroDivisionError:
    print 'Divided by zero'
finally:
    print 'This code will run no matter what'
Hello
Divided by zero
This code will run no matter what

Example: what is the output of this code?
try:
    print 1
except:
    print 2
finally:
    print 3
1
3

Code in a finally statement even runs if an uncaught exception occurs in one of the preceding blocks.
try:
    print 1
    print 10/0
except ZeroDivisionError:
    print unknown_var
finally:
    print 'This is executed last'
1
This is executed last
NameError: name 'unknown_var' is not defined
Example: Fill in the blanks to create a try/except/finally block.
__:
    print 1
______:
    print 2
________:
    print 42
1
42