Python Crash Course

Python Crash Course

The Code

This series of blogs on Python was compiled as I was trying to learn the language. I present it here, for someone who might want a quick introduction to the language, without digging through all the manuals. This is not a 'Complete Reference' nor is it a 'Python for Dummies'. It is meant for someone who understands development and wants to peep into the world of Python. This is a series of blogs covers these topics. (It will continue to grow as I continue to learn)

As per the StackOverflow survey, Python was the fastest growing programming language of 2019. And we can expect this trend to grow as the developer community continues to move towards Python. That makes Python the language of choice for any developer.

Installation

There are many different sources and distributions of Python. Foremost is available on their own site Install Python. This page has everything you need. Download the version you like.

For legacy reasons, Python continues to host the Python 2.* installers. But, also warns us that it will be deprecated very soon. This "soon" has not arrived over the last 4 years that I have seen their page. But, from the trends we can say that, anyone serious about the future, should use Python 3. Specially if you are learning afresh, there is no reason to go for Python 2. (Unless you are forced to maintain a legacy Python 2 application).

We also have other distributions like Anaconda that package a good amount of useful stuff along with the base Python. This is what most enthusiasts use. And then there are other commercial distributions of Python like ActiveState Python that charge you for support and packaging. That is meant for people who prefer to spend money.

Python comes with its own editor (IDLE) that provides syntax highlighting. It is good for developing and running minor scripts and also for testing single commands. But, for doing anything more complex and useful, you will need a better IDE. Several open source IDE's are available on the net. The list keeps growing. Just lookup one you like from Google and you should be ready to go. I liked PyCharm, Spyder, Atom and Visual Studio Code. If you don't like these, just search for one on the net and let me know if you find something useful.

Hello World

We often put in a lot of effort on Traditions. We do a lot of things we don't really know why, but we do them because "that is the way"! The "Hello World" is another such tradition. None knows what is so magical about those two words. But there is something so magical that everyone wants to use just that phrase.

Anyway, let's do the same here - announcing to the world that we have started learning Python.Open your IDE and create a new Project - Learn Python or HelloWorld; Create a new file with the appropriate extension (.py).

If you are running on Windows, the extension should be enough. But if you are fond of Linux, the extension has no meaning. You need to explicitly indicate the interpreter using the shabang on the first line of the script. After that, add the following one line in the new script.

#!/bin/python
print("Hello World")

(The shabang would point to python or python3 depending upon the Python distribution that you use.)

That is all we need in order to start. The code prints the two words - Hello World - Nothing much for the world, but it does tell you that you have started well!

Congratulations! You have started off on your journey with the Python. Now follow along as we see how the Python has engulfed the world, and grown into an Anaconda!

Zen of Python

Before we jump into the code, it is important to understand the core values that are expected of a Python Programmer. Python language was created with some important principles in mind - that were lacking in other languages. These principles are the key to its success, and are called the Zen of Python.

  • Beautiful is better than ugly.
  • Explicit is better than implicit.
  • Simple is better than complex.
  • Complex is better than complicated.
  • Flat is better than nested.
  • Sparse is better than dense.
  • Readability counts.
  • Special cases aren't special enough to break the rules.
  • Although practicality beats purity.
  • Errors should never pass silently.
  • Unless explicitly silenced.
  • In the face of ambiguity, refuse the temptation to guess.
  • There should be one-- and preferably only one --obvious way to do it.
  • Although that way may not be obvious at first unless you're Dutch.
  • Now is better than never.
  • Although never is often better than right now.
  • If the implementation is hard to explain, it's a bad idea.
  • If the implementation is easy to explain, it may be a good idea.
  • Namespaces are one honking great idea -- let's do more of those!
  • For the uninitiated, these may seem stupid and silly. But someone who has seen and worked on an application with a million lines of code will certainly appreciate their value.

Syntax

Readability of the code is an important component of the Zen of Python. Python imposes this value in its core syntax. We have all seen code that could be more readable, only if the "author had the mercy to indent it". Indentation is the least you can do for the future developers who need to work on the code. But many programmers are sadists, who enjoy avoiding it.

Python will not let you do it. Here, indentation is a part of the syntax. In Python, there are no semi-colons or curly-braces. It is just newline and indentation. A new line is a new statement. Code with similar indentation is part of a block. So your code will not do what it should, if you do not take care of indenting it properly.

We will see more of this as we work with code flow, and other upcoming modules. For now, it is enough to understand that Python cares for you and enforces a level of readability in the code.

Comments are another important (and the most ignored) part of any programming language. Everyone knows they are required. Everyone knows why they are required. Everyone curses the developer when they see a code without comments. But, very few are gracious enough to comment their own code. For these generous minded developers, Python provides a simple syntax for adding comments to their code - #. Any text that follows a # - till the end of line, is ignored by the interpreter - as a comment. A # inside quotes is treated simply as a part of the string, and hence does not mark any comment.

Data Types

Python provides for most normal functionality like data types and normal code flow structures that any normal programming language can provide.

Numbers

Computing started with numbers. Today, it has covered several data types. But, numbers still form a major chunk of tasks. Python provides for different types of numbers. It also provides huge functionality for processing them. We have integers, floats, Try out the below code to check out the various numeric functions:

a = 10
b = 3
c = a + b       # 13
print(c)

c = a - b       # 7
print(c)

c = a * b       # 30
print(c)

c = a / b       # 3.3333333333333335
print(c)

c = a // b      # 3
print(c)

c = a % b       # 1
print(c)

c = a ** b      # 1000
print(c)

In addition to the integer and floating point numbers described above, Python also supports Decimal / Fraction / Complex numbers - that provide a lot more functionality. We will have a look at them later.

Strings

The other most commonly used data type is that of strings. Python provides for a huge functionality to work with and manipulate strings. Strings can be defined in single quotes as well as in double quotes. Special characters need to be escaped with a '\'. There is no particular difference between a string defined in single quotes and one defined in double quotes. Naturally, they have to be consistent and a string defined in single quotes should escape a single quote character within the string and a double quoted string should escape a double quote character in the string. Python defines several useful functions for Strings. Check out the code below

s = 'Single quoted String'
print(s)

s = "Double quoted String"
print(s)

s = "Adding a # inside a quoted string does not make it a comment."
print(s)

s = 'Single quoted string needs to escape \' character not "'
print(s)

s = "Double quoted string needs to escape \" character not '"
print(s)

s = r'use r if you \\ do not like the escape \ '
print(s)

s = """\
A
Multi
Line
String
"""
print(s)

A feature rich language like Python naturally has the basic functionality to split / join / append / substring, and a lot more that you can explore with the auto suggest in any sensible IDE, or by looking up the manuals. Try the below code to check out the basics.

s = "lEaRnInG"

# Append
s = s.__add__(" PyThOn")
#split
print(s.split())
print(s.split(sep="t"))

# Splicing
print(s[:])
print(s[1:])
print(s[1:-1])
print(s[5:-5])
print(s[16:0])

# Casing
print(s.lower())
print(s.upper())
print(s.title())

Booleans

Booleans are logical variables - used in decision making. Python defines two values True and False for Boolean variables.

Although these two values are predefined in the language, Python is a bit loose about Booleans. Internally, True is just the number 1 and False is the number 0. You can verify these by adding True + True, or if you are adventurous, by dividing True/False - Don't blame me for the exception!

Most other datatypes can be used in a 'Boolean context' - and they have a criteria for when they should be considered False and when True. Any non-zero number is True. Any non-empty string is True, and so on.

Compound Datatypes

Compound data types in Python are quite similar to collections in Java. We have lists, tuples, sets and dictionaries. Let's peep into each of them one by one.

Lists

Python provides for several compound data types - that can be used to process data in groups. A list is the simplest of these. A list can contain a collection of any type of data, including other lists. To define a list, the variables need to be placed sequentially in square brackets. The code below describes the definition of a list and how its elements can be accessed.

Note that unlike many other languages, Python does not insist on having a consistent data type among the elements of the list. We can have a number, string, boolean and any other data types in the same list.

We can access the elements of the list using indexes in the square brackets.

l = [1, 'Hello', "World", True, [2, 'Learn', "Python", False]]

print(l)          # [1, 'Hello', 'World', True, [2, 'Learn', 'Python', False]]
print(l[0])       # 1
print(l[4])       # [2, 'Learn', 'Python', False]
print(l[4][0])    # 2
print(l[4][3])    # False
print(l[-3])      # World

Few lines of code say a lot more than what could be said in words. We can have indexing, nested indexing as well as reverse indexing.

Python lists also support splicing, and provide several utility functions to add / remove / change data in the list. Check out the code below:

letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g']

print(letters)            # ['a', 'b', 'c', 'd', 'e', 'f', 'g']
print(letters[:])         # ['a', 'b', 'c', 'd', 'e', 'f', 'g']

letters[2:5] = ['C', 'D', 'E']
print(letters)            # ['a', 'b', 'C', 'D', 'E', 'f', 'g']

letters[2:5] = []
print(letters)            # ['a', 'b', 'f', 'g']

letters[2:2] = ['c', 'd', 'e']
print(letters)            # ['a', 'b', 'c', 'd', 'e', 'f', 'g']

letters.append('h')
print(letters)            # ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
print(len(letters))       # 8

letters.remove('a')
print(letters)            # ['b', 'c', 'd', 'e', 'f', 'g', 'h']

letters.pop()
print(letters)            # ['b', 'c', 'd', 'e', 'f', 'g']

del letters[2]
print(letters)            # ['b', 'c', 'e', 'f', 'g']

del letters[2:4]
print(letters)            # ['b', 'c', 'g']

letters.reverse()
print(letters)            # ['g', 'c', 'b']

Notice that the output of print(letters) is same as that of print(letters[:]).

But, there is a difference. The letters[:] is not the same object as letters. It is a copy of the original - a shallow copy.

Tuples

Tuples are similar to lists but, have one marked difference. Tuples are immutable. They cannot be changed once they are defined. Naturally, tuples provide most of the methods that lists provide - except any method that would modify the list. You might ask, what is the advantage of forcing such a restriction? It is speed! Due to its immutability, a tuple can be implemented differently from lists - with more focus on speed of execution. As mentioned before, Python being an interpreted language has to lag in speed. But, optimizations like these take it far ahead of others. Tuples are much faster than Lists and are used where we know that the data in the list is not going to change - this is a common scenario.

One major syntactical difference between List and Tuple is that a tuple is enclosed in circular brackets '(. . .)', while List is enclosed in square brackets '[. . .]'. You can convert a list to tuple and tuple to list by typecasting. Python also allows you to define a tuple without any brackets - because it is the most natural sequence for Python. A tuple can be converted to a list and a list can be converted to a tuple. Check out the code below for more.

t = (1, 2, 3)
print(t)         # (1, 2, 3)

t = 1, 2, 3
print(t)         # (1, 2, 3)

t = (1, 2, 'String', (3, 4, "String 2"), [1, 2, 3])
print(t)         # (1, 2, 'String', (3, 4, 'String 2'), [1, 2, 3])

print(t[4])      # [1, 2, 3]

t[4].extend([2, 3, 4])
print(t)         # (1, 2, 'String', (3, 4, 'String 2'), [1, 2, 3, 2, 3, 4])

l = list(t)
print(l)         # [1, 2, 'String', (3, 4, 'String 2'), [1, 2, 3, 2, 3, 4]]

t = tuple(l)
print(t)         # (1, 2, 'String', (3, 4, 'String 2'), [1, 2, 3, 2, 3, 4])

Note that although the tuple is immutable, a list contained in the tuple can be modified - because the tuple just contains the reference to the list object. The reference should not change. The list itself may be modified.

Sets

Sets are similar to their counterparts in other languages. As the name suggests, they ensure a distinct set of elements. Any duplicates are ignored. Sets do not have any order of elements. They are defined by data enclosed in curly braces - '{. . .}'. A set can be typecast to and from lists or tuples. Sets define various methods for manipulation.

s = {1, "String", ('1', 'Tuple'), 1, 2}
print(s)        # {1, 'String', 2, ('1', 'Tuple')}

s.add(1)
print(s)        # {1, 'String', 2, ('1', 'Tuple')}

s.add(3)
print(s)        # {1, 'String', 3, 2, ('1', 'Tuple')}

s.remove(1)
print(s)        # {'String', 3, 2, ('1', 'Tuple')}

# remove throws an exception and discard just ignores any attempt to remove non existent element
s.discard("Strings")
print(s)        # {'String', 3, 2, ('1', 'Tuple')}

s.pop()
print(s)        # {3, 2, ('1', 'Tuple')}

s.clear()
print(s)        # set()

Sets are choosy about the elements that they allow. For example, you cannot have a list inside a set. The elements have to be immutable and "hashable".

Dictionaries

Dictionaries are a special set of keys with a value associated with each key. You can work with a dictionary as below:

d = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
print(d)           # {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}

print(d['key1'])   # value1

d['key7'] = 'value7'
print(d)           # {'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key7': 'value7'}

del d['key7']
print(d)           # {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}

d['key1'] = 'New Value 1'
print(d)           # {'key1': 'New Value 1', 'key2': 'value2', 'key3': 'value3'}

The above code describes the most common functionalities of a dictionary.

Code Flow

Programming is all about data and decisions. In the above section, we saw a few ways data can be stored an processed in Python. Now, let us check out how decisions can be made, how code can be made to flow from one line to another. Then we will see a small code snippet to demonstrate basics of code flow.

Indentation

One major point where Python improves over most other languages is that it forces the developers to write readable code. It forces developers to indent the code; making it more readable. There are some developers who just have to write unreadable code - and they do find ways around Python as well.

Python does not use the curly braces {. . .} to define a block of code. A block of code is defined by its indentation. Consider the two blocks of code below:

a = 0
while a<10:
  a = a+1
  print(a)

a = 0
while a<10:
  a = a+1

print(a)

They show a typical while loop in Python. Note the ':' after the condition, followed by code that is indented. In the first block, both lines of code are indented, while the second block has print(a) outside the block. That made a big difference to the code flow. The print(a) when indented, was considered as a part of the while loop - hence executed 10 times. But, the same when not indented, is executed only once at the end of the loop.

This holds true for any block of code - if / elif / else / for / while / def (function) / class - anything that is appropriately indented is part of the block. Else, it is not.

Python does not insist on any specifics about space/tab indentation, number of spaces, etc. But the convention - that everyone follows - dictates that it should be 4 spaces.

I am not going to bore you (and myself) with the details of if / elif / else / while / for... We know them too well already. There are some subtle improvements in Python and we will see them as we go. Suffices here to say - Python provides for them. We will start with syntax of the basics and then move further.

I plan to just brush through the basics and jump to something more interesting. The code snippet below covers the basics of control flow. Check it out on your IDE!

Example

# Define an empty list
primes = []

# Define an empty set / map
divisors = {}

# Loop through all the numbers from 2 to 100.
for n in range(2, 100):
    divisors[n] = []
    # Loop through all values in list of primes
    for p in primes:
        # Break out of the loop if the number is
        # divisible by any of the primes
        if (not(n % p)):
            divisors[n].append(p)

    # This else block will be executed only if the
    # above for loop exits normally without a break
    # - implying that the number is prime.
    else:
        primes.append(n)

# Print the list of prime numbers and divisors
print(primes)
print(divisors)

Anyone who understands basic programming will surely understand what this code is doing. It just identifies the lists of divisors and prime numbers among the numbers 2-100.

The point to note here is the way code indentation defines the flow of the code. Most interesting is the else block. Because of its indentation, it is applied to the for loop rather than the if. Yes, Python also provides for an else block on a for loop - that is executed only if the loop reaches its natural end, without breaking off anywhere midway.

Iterators

In a previous sections, we saw loops on lists, tuples, etc.

for element in (1, 2, 3):
    print(element)

Python does not limit these iterations to its built-in collection types. Any class that implements the required methods can be used as an iterable. For example, you want to iterate over the first n elements of Fibonacci series, you can create a simple iterator for that:

class Fib:
    def __init__(self, n):
        self.max = n
        self.last = 0
        self.secondlast = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.last:
            self.secondlast, self.last = self.last, self.last + self.secondlast
        else:
            self.last = 1
        if self.last > self.max:
            raise StopIteration
        return self.last

for x in Fib(100):
    print(x)

In fact, you need not implement the next method in the object that you are working with. All you need is the iter() method. This method should return an object which implements the next()

def fib(n):
    i, j = 0, 1
    while j <= n:
        yield(j)
        i, j = j, i+j

    for x in Fib(100):
        print(x)

This is all you need! Note that keyword 'yield'. It is not return. The method does not return anything. It yields one value at a time. Of course, internally this translates to an iterator of some sort. But, the code certainly looks much better and much easier to manage. Since we have lesser script, it means there is lesser effort on interpretation and more of the C code doing the job. Naturally the performance is better.

Modularity

Modularity is one of the basic components of any language. Any decent language - be it a low level assembly language or a 4G language that generates code, has to provide some mechanism that allows us to reuse what we have done once. It provides for some way of extracting common functionality and hiding its complexity. Of course, Python adds its own flavor to this. Let us see how.

Python provides modularity in three forms - Functions (or Methods), Modules, and Classes.

Functions

'Demonstrate Python Functions'
def getFunction(full=True):
    'Outer Function'
    print(getFunction.__doc__)
    def p(frm=0, to=1, step=1):
        'Inner Function'
        print(p.__doc__)
        return (x ** 3 for x in range(frm, to, step))

    if (full):
        return p
    else:
        return lambda frm = 0, to = 1, step = 1: (x ** 3 \
              for x in range(frm, to, step))

print(__doc__)

t = getFunction()

print("Check the elaborate function")
for v in t(step=1, to=10):
    print(v)

t = getFunction(False)
print("Check the lambda function")

for v in t(1, 5):
    print(v)

As shown above, functions can be abbreviated using lambda functions. That saves the lines of code and can be used to improve performance - so long as it is readable.

Python provides for a concept similar to Java Docs. The first line in a function - if a single quoted string, is considered the function doc. But, Python takes this a step further. It is possible to use this value in code!

Modules

Modules provide a way to reuse code. A module is simply a file containing python code that we can 'import' into our code.

import re

def plural(noun):
    if re.search('[sxz]$', noun):
        return re.sub('$', 'es', noun)
    elif re.search('[^aeioudgkprt]h$', noun):
        return re.sub('$', 'es', noun)
    elif re.search('[^aeiou]y$', noun):
        return re.sub('y$', 'ies', noun)
    else:
        return noun + 's'

if __name__ == '__main__':
    print (plural('abc'))
    print (plural('def'))
    print (plural('des'))
    print (plural('xyz'))

Check out the code above. The first line imports a module re - that is built into Python. As the name suggests, it is meant for regular expressions. It has several methods related to regular expression search, replace, etc. All the methods / objects inside this module are invoked with the prefix of re.

You can also notice the line

if __name__ == '__main__':

before the main code starts. This is a useful construct in any code that your write. It helps anyone who might import this code as a module. When you run this code as a standalone code, it will execute the code under this if clause. But, if anyone imports this module, it is most likely that he just wants the methods in this module, he does not want to run the code outside these methods. This construct prevents such an accident.

The built in dir() function can be used to identify at runtime, the list of names defined within a given module. If you like, you can import only a part of the module by using the (from module import method) construct.

Packages

Packages are a common means of avoiding name clashes. You can import a module from a package using the (from package import module) construct. Python packages are similar to Java. Each package should have its own folder. You can have sub packages in subfolders.

One additional requirement that Python imposes for packages is that a package folder should have the init.py file. Python will consider a folder as a package only if it has this script. This file could be empty, or it can define the value of all. The all is the list of modules defined in the package - that would be imported if the user invokes (from package import *).

This is a helpful construct - something like the main inside a module file. It prevents unwanted execution of any additional code that is saved in the package folder - it allows you to save additional code in the package folder.

The init.py script can also execute any initialization code for the package.

Object Oriented Code

After reading the simple scripts we have seen so far, one might wonder why is it called an object oriented language. Yes, Python does not come in way of plain functional code, and allows you to write simple script to do small chunks of tasks. But Python is object oriented to its core! Everything in Python is an object - everything including the code itself!

Before we jump into the Python implementation of classes, and object orientation in general. Let's introspect to ask ourselves, what exactly do we mean by object oriented code? What is good or bad about object oriented code? Why is it more maintainable and when is it not efficient?

What is an object? In software, an object is defined as "something that has a defined behavior influenced by some information related to it". Any software, whether it is functional or object oriented, implements some functionality for objects. What matters is the point of focus - whether the functionality is in focus or the object itself? A functional code would have all the functionality separated from the information, whereas a good object oriented code should have the information clubbed with the behavior.

The language used to do this is not so important. You can have Java code that is functional in essence and also have C code that is object oriented in essence. What matters is the spirit of clubbing together the information with the behavior of the object. There is no good or bad about functional or object oriented code. Both are equally good in their own context. What is important is identifying which one is required in the current scenario and then applying it appropriately. Most often it is a mixture. For example, a properties file used to configure the system pulls out the information - thus adding a functional flavor to it. Or a function static variables in a C code clubs the behavior with the data - making it object oriented.

With that, let us get into the details of object oriented coding in Python. From the point of view of semantics, Python provides for class definition, inheritance, constructor, destructor and member variables. For some strange reason, they forgot to add private members. Python allows you all the freedom, but provides guidelines for discipline with conventions. Python does not let you enforce private members, but it is a universally accepted convention that any member variable with name starting with _ or __ has a special meaning and should not be touched by 'outsiders'. Developers use this for adding private members.

Python provides two special member methods - init and del. These are similar to the constructor and destructor. Needless to say that constructor is invoked when the object is created and can be used to initialize any members, while destructor is invoked in the cleanup process and is used to perform any cleanup activity.

A class can define several member methods and variables. One peculiar point about member functions is that they all must have one first parameter - self. Python compiler does not enforce the name "self". But, convention dictates it. Don't use any other word there if you feel that someone somewhere might ever peek into your code. This parameter is not passed to the method when calling it in the context of an object. But, the runtime takes care of passing a reference to the particular object in there.

Example

Let's check out this example code that gives basic details:

# A Sample Base Class to demostrate basic semantics
class Base:
    "A Sample Base Class"
    def __init__(self):
        print("Base Class Constructor")
        self._base_member_variable_ = 0

    def __del__(self):
        print("Base Class Constructor")

    def printBaseValue(self):
        print("Base Class: " + str(self._base_member_variable_))

# A Sample Derrived Class to demostrate basic semantics
class Derrived(Base):
    "A Sample Derrived Class"
    def __init__(self):
        super(Derrived, self).__init__()
        print("Derrived Class Constructor")
        self._derrived_member_variable_ = 1

    def __del__(self):
        print("Derrived Class Constructor")
        super(Derrived, self).__del__()

    def printDerrivedValue(self):
        print("Derrived Class: " + str(self._derrived_member_variable_))
        print("Derrived Class: " + str(self._base_member_variable_))

# A Python method to check out the classes defined above.
def checkout():
    o = Derrived()
    print(o.__class__)
    print(o.__doc__)
    o.printBaseValue()
    o.printDerrivedValue()

checkout();

The output of this code looks like this:

Base Class Constructor
Derrived Class Constructor

A Sample Derrived Class
Base Class: 0
Derrived Class: 1
Derrived Class: 0
Derrived Class Destructor
Base Class Destructor

Points to Note

Notice the following points in the code above:

A derived class id defined with the base class as a parameter in its definition. Python allows for Multiple Inheritance. In case of Multiple Inheritance, we can have clashes in method names. Python takes care of this by giving higher priority to the first parent in the list.

All the methods in the class are defined with one minimum parameter (self). This is not passed to the methods when they are invoked in the context of the object. The interpreter takes care of passing a reference of the object in this parameter.

A unique feature in Python is the support for code documentation. The line immediately after the class keyword is the class document. This can be a multiline or single line string. This is not just a code comment that is used for better readability, but Python allows you to use this documentation at runtime. What more, you can also modify this at runtime!

Note that the member variables in the class are not explicitly declared anywhere. They are just assigned values in the code. And they are available after that. Since the variables are not private, we can always create new variables in an object - at runtime. Thus, the variables are not really member variables - they are not tied to the class definition - they are just associated with the given instance. But, Python reflection code is powerful enough to show us this association and lets us manipulate them.

The object is created by invoking the constructor. We do not need a new keyword while creating objects in Python. The destructor is called when the object goes out of scope and needs to be destroyed. If the parent class constructor and destructor should be invoked, we need to call them explicitly.

The method 'super' is part of the reflection API, that lets you identify the superclass of the given child class.

This short structure of Python's Object Oriented code allows for infinite possibilities.

Miscellaneous Constructs

Having seen the major parts of the core language, let us now look into some of additional frills that make code a lot easier.

Iterators

In a previous sections, we saw loops on lists, tuples, etc.

for element in (1, 2, 3):
    print(element)

Python does not limit these iterations to its built-in collection types. Any class that implements the required methods can be used as an iterable. For example, you want to iterate over the first n elements of Fibonacci series, you can create a simple iterator for that:

class Fib:
    def __init__(self, n):
        self.max = n
        self.last = 0
        self.secondlast = 0 

    def __iter__(self):
        return self

    def __next__(self):
        if self.last:
            self.secondlast, self.last = self.last, self.last + self.secondlast
        else:
            self.last = 1
        if self.last > self.max:
            raise StopIteration
        return self.last

for x in Fib(100):
    print(x)

In fact, you need not implement the next method in the object that you are working with. All you need is the iter() method. This method should return an object which implements the next()

def fib(n):
    i, j = 0, 1
    while j <= n:
        yield(j)
        i, j = j, i+j

    for x in Fib(100): 
        print(x)

This is all you need! Note that keyword 'yield'. It is not return. The method does not return anything. It yields one value at a time. Of course, internally this translates to an iterator of some sort. But, the code certainly looks much better and much easier to manage. Since we have lesser script, it means there is lesser effort on interpretation and more of the C code doing the job. Naturally the performance is better.

Generators

The folks who made Python were not satisfied with simple Generators. They wanted to go one step further. Why do we have to create a new class or method for something that can be done by just one line of code? Generator expressions do just that! Naturally they are not as versatile as iterators and generators. But there are times when we really do not need all that.

For example, if you want the list of first 10 cubes, you just need a single line of code!

print([x**3 for x in range(10)])

Exceptions

Everything in Python is an object - so is an exception. Any exception is an instance of a class that extends the common base class - Exception. You can 'raise' an exception, using an object of the Exception class. Or else, you can just give the class name as a parameter to the 'raise' command. Python will take care of creating an object for it.

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")

Note that if the except clauses were reversed (with except B first), it would have printed B, B, B - the first matching except clause is triggered. The concept of Exceptions is not new in Python. Exceptions have been used in several other languages in the past, and most developers are very familiar with them. But the interesting twist that Python provides is because of the flexibility of Python classes and objects. Now you can pass in any damn information with any exception. All you need to do is to create the instance of the exception object, set the object attributes and then raise it!

try:
    e = Exception('Additional information')
    e.more_info = 'Some more information'
    raise e
except Exception as e:
    print(type(e))
    print(e.args)
    print(e.more_info)

This prints the type of the Exception (Exception), followed by a tuple containing the one argument that was passed in while creating the exception. You can have multiple arguments there. Next line prints 'Some more information' about the exception. This opens infinite possibilities for passing data from the exception to the catch block. You can send out not just strings, but any object that could be useful to the catch block.

Such minor flexibilities in Python open up infinite possibilities when you design and code!