Python for programmers (part 3)

This is the last part of a 3 part exploration trip into Python. I wrote this for any  programmer coming from a C-style language to highlight all the basic features and syntax of the language. Hopefully, someone reading this will save some time from reading the excellent but a bit lengthy official language tour.

This part is covering (8) Errors and Exceptions and (9) Classes.

Exceptions

# Simple exception handling
try:
    x = int(input("Please enter a number: "))
    break
except ValueError:
    print("Not a number")
    

# Catch multiple exceptions at once
try:
    x = int(input("Please enter a number: "))
    break
except (RuntimeError, TypeError, NameError):
    print("Something went wrong")

# `Else` contains code to be executed if `try` does not raise an exception
try:
    f = open(arg, 'r')
except OSError:
    print('Cannot open file')
else:
    print(arg, 'has', len(f.readlines()), 'lines')
    f.close()

Except clause may specify a variable after the exception name. The variable is bound to an exception instance with the arguments stored in instance.args.

try:
    raise Exception('spam', 'eggs') # Use `raise` to throw an exception
except Exception as inst:
    print(type(inst))    # <class 'Exception'>
    print(inst.args)     # ('spam', 'eggs')
    print(inst)          # ('spam', 'eggs')
    x, y = inst.args
    print('x =', x)      # x = spam
    print('y =', y)      # y = eggs

finally clause is intended to define clean-up actions that must be executed under all circumstances.

try:
    result = x / y
except ZeroDivisionError:
    print("Division by zero!")
else:
    print("Result is", result)
finally:
    print("This will be executed at all cases.")

Classes

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

The constructor of a class is defined with an __init__() method. Class instantiation automatically invokes __init__() for the newly-created class instance.

class MyClass:
    def __init__(self):    
        print('Hello world')

x = MyClass()

Instance variables are for data unique to each instance and class variables are for attributes and methods shared by all instances of the class.

class Dog:

    kind = 'canine' # class variable shared by all instances

    def __init__(self, name):
        self.name = name # instance variable unique to each instance

d = Dog('Fido')
e = Dog('Buddy')
d.kind # 'canine'
e.kind # 'canine'
d.name # 'Fido' (unique to d)
e.name # 'Buddy' (unique to e)

Shared data can have possibly surprising effects with involving mutable objects such as lists and dictionaries. For example, if want to save the tricks list for each Dog, we should not use a class variable, but declare an instance variable:

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

d = Dog('Fido')
e = Dog('Buddy')
d.add_trick('roll over')
e.add_trick('play dead')
d.tricks # ['roll over']
e.tricks # ['play dead']

Inheritance

Used for resolving attribute references: if a requested attribute is not found in the class, the search proceeds to look in the base class. This rule is applied recursively if the base class itself is derived from some other class.

class DerivedClassName(modname.BaseClassName):
   [...]

Multiple Inheritance

Python supports a form of multiple inheritance. The search for attributes inherited from a parent class as depth-first, left-to-right, not searching twice in the same class where there is an overlap in the hierarchy.

# If an attribute is not found in DerivedClassName, it is searched for in Base1, 
# then (recursively) in the base classes of Base1, and if it was not found 
# there, it was searched for in Base2, and so on.
class DerivedClassName(Base1, Base2, Base3):
    [...]

Private variables

“Private” instance variables don’t exist in Python. However, there is a convention:  a name prefixed with an underscore (e.g. _spam) should be treated as a non-public part of the API (whether it is a function, a method, or a data member).

class MyClass:
	
    def __init__(self, name):
        self._name = name

Iterators

Defines the method __next__() which accesses elements in the container one at a time. When no more elements, __next__() raises a StopIteration exception. You can call the __next__() method using the next() built-in function (or use a for loop).

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):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]

rev = Reverse('spam')
for char in rev:
    print(char)

Generators are a tool for creating iterators. They look like regular functions but use the yield statement whenever they want to return data. Each time next() is called on it, the generator resumes where it left off (it remembers all the data values and which statement was last executed).

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]

Hopefully, this was a gently intro to the language. Happy coding (and learning) in Python!