Python

Writing style and conventions (PEP8 Guidelines)

  • Code blocks are defined by indentation (PEP8 indicates 4 white spaces) and are started by a ':'
  • Strings can be defined by " " and ' '
  • Inline comments are indicated with a #. Everything after the is ignored
  • Multiline comments are encoled in """ """. They are also called docstrings and can be parsed to create automatic documentation (PEP 257)
  • Modules and packages should be al lower case
  • Classes are indicated by CapWords
  • Functions and variable are lower case with _ separating words
  • Python does not have private variables or methods. leading '_' in a name indicate that the variable/method should be considered private (this can be mildly enforced using double leading underscores __private__variable)
  • Constants are ALL_CAPS

Duck typing

If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck.

              _      _      _
           __(.)< __(.)> __(.)=
           \___)  \___)  \___)

In python an object's usability is determined by the methods and properties is has not by the type

Ask forgivness not permission

One of the results of duck typing is that python encourages you to try something and catch the error rather than first checking if you are allowed to that that one thing

In [1]:
class Duck():
    def fly(self):
        print('Duck is flying')

class Airplane():
    def fly(self):
        print('Airplane is flying')

class Whale():
    def swim(self):
        print('Whale swimming')

def fly(entity):
    entity.fly() 
In [2]:
duck = Duck()
fly(duck)
Duck is flying
In [3]:
airplane = Airplane()
fly(airplane)
Airplane is flying
In [4]:
whale = Whale() 
fly(whale)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-f6886a7249bb> in <module>
      1 whale = Whale()
----> 2 fly(whale)

<ipython-input-1-1a5f72d543e4> in fly(entity)
     12 
     13 def fly(entity):
---> 14     entity.fly()

AttributeError: 'Whale' object has no attribute 'fly'

This allows us to use methods without having to worry what type is passed to the method. Observe how different variable types effected by the same operation (a+b)*c on different types

In [6]:
a = 3
b = 5
c = 2
print( (a + b) * c )
16
In [7]:
a = [1,2,3]
b = [4,5,6]
c = 3
print( (a + b) * c)
[1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
In [8]:
a = 'Cat '
b = 'is on the table. '
c = 3
print( (a + b) * c)
Cat is on the table. Cat is on the table. Cat is on the table. 

Python uses this to implement built-in functions. E.g. if a class defines a method called __len( )__ then calling len(object_instance) will return the value provided by the method.

def len(entity):
    entity.__len__()
In [9]:
print(len([10,4,'apple',5]))
print(len('strawberry'))
4
10

Other built-in functions inclued:

  • __str__: used when an object needs to be converted ot a string
  • __getitem__: used when iterating over the object (e.g. a custom list)
  • __eq__, __lt__, __gt__: used when comparing objects This can be used when creating your own classes and want to improve the usability of the class

Other built-in functions can be found at https://docs.python.org/3/library/functions.html#__import__

Variable types

Unlike other languages you do not have to declare variable types. A variable just indicates an object (duck typing)

In [10]:
a = 4 #a is an int
print(type(a))
<class 'int'>
In [11]:
a = 4.0 #a is now a float
print(type(a))
<class 'float'>
In [12]:
a = 2+3j #a is now a complex
print(type(a))
<class 'complex'>
In [13]:
a = True #a is now a boolean
print(type(a))
<class 'bool'>
In [14]:
a = [1,2,3] #a is now a list
print(type(a))
<class 'list'>
In [15]:
a = [[1,2,3],[4,5,6],[7,8,9,10]] #you can make lists of lists
print(a)
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]
In [16]:
a = [1,2,3,'a'] #or mixed lists
print(a)
[1, 2, 3, 'a']

An noteworthy result of Python 2 "interpreting" variable types is that if we aren't careful it might decide to use one type compared to another. In particular Python2 will tend to use the least possible precision for a number. So 1/3 does not give the same result of 1.0/3 or 1/3.0, even if we cast the result to float. Always keet thing truncation in mind if a result is not what you would expect.

This is not the case for Python 3 anymore, a division between ints will return a float

In [17]:
print('1/3 results in {0} of type {1}'.format(1/3,type(1/3)))
print('float(1/3) results in {0} of type {1}'.format(float(1/3), type(float(1/3))))
print('1.0/3 results in {0} of type {1}'.format(1.0/3,type(1.0/3)))
print('float(1)/3 results in {0} of type {1}'.format(float(1)/3,type(float(1)/3)))
1/3 results in 0.3333333333333333 of type <class 'float'>
float(1/3) results in 0.3333333333333333 of type <class 'float'>
1.0/3 results in 0.3333333333333333 of type <class 'float'>
float(1)/3 results in 0.3333333333333333 of type <class 'float'>

Adding functionality to python

Python offers many functionalities out-of-the-box, but more/better functions can be used by importing modules. this can be done in several ways. Choose the most efficient.

N.B. When importing a module python iterates though all already imported modules and does not perform double imports

  • import [module] (you can now use classes and methods from the module using dot notation):
      import random
          random.randint() #will call a method which returns a random integer

You can import all methods and classes from a module using the '*' (from random import *) however this is not advisable in most cases.

  • import [module] as [alias] (you can now use classes and methods from the module using the alias):
      import random as rnd
          rnd.randint() #will call a mehtod wich returns a random integer
  • from [module] import [method/class] (you can now use the imported method/class without dot notation)
      from random import randint
          randint() #will call a method which returns a random integer

Printing to console and concatenating strings

In [18]:
print('- Simply print a string')
- Simply print a string
In [19]:
print('- Insert variables instrings using format {first}, {second}'.format(first=1,second=2))
- Insert variables instrings using format 1, 2
In [20]:
print('- Format also accepts {0} arguments without having to provide a {1}'.format('positional',
                                                                                   'name'))
- Format also accepts positional arguments without having to provide a name
In [21]:
print('- Format will evaluate expressions: 5+3 = {result:.1f} and format the numerical output'
      .format(result=5+3))
print('- Format will evaluate expressions: 5+3 = {result:.3f} and format the numerical output'
      .format(result=5+3))
print('- Fomrat can also all leading zeros: {0:03d}'.format(3))
- Format will evaluate expressions: 5+3 = 8.0 and format the numerical output
- Format will evaluate expressions: 5+3 = 8.000 and format the numerical output
- Fomrat can also all leading zeros: 003
In [22]:
print('- If a string appears multiple times {dots} you can reuse it {dots}'.format(dots='...'))
- If a string appears multiple times ... you can reuse it ...
In [23]:
sentence = ['-','To','concatenate','a','list','of','strings','use','the','appropriate','method']
print(' '.join(sentence))
- To concatenate a list of strings use the appropriate method

Defining functions

A function is defined using the def keyword word. Remember to use a colon to show the start of the block and to indent correctly. Use the return keyword to return a value

In [24]:
def sum_operation(first_number, second_number):
    result = first_number+second_number
    return result

sum_operation(5,3)
Out[24]:
8

A function can have optional parameters, with a default value provided if none is passed

In [25]:
def increment_by(first_number, increment=1):
    result = first_number+increment
    return result
In [26]:
#If both parameters are passed then they are both used
print(increment_by(5,2))
7
In [27]:
#If the optional parameter is not given then the default value is used
print(increment_by(5))
6

A function can also return multiple values

In [28]:
def calculate_fall_parameters(fall_time,v0=0, g=9.8):
    final_speed = v0 + fall_time*g
    height = v0*fall_time + 0.5*g*fall_time**2
    return (height, final_speed)
In [29]:
h,vf = calculate_fall_parameters(10)
print('After 10s the object fell {height}m and reaced a speed of {v_final}m/s'.format(
    height=h,v_final=vf))
After 10s the object fell 490.00000000000006m and reaced a speed of 98.0m/s

Using lambda functions

lambda functions are used when you need an anonymous function for a quick operation (often inside another function or as a parameter to another function)

In [30]:
#A lambda function can be returned from a function and evaluated at a later moment
def create_multiplier(n):
    return lambda a: a*n
In [31]:
doubler = create_multiplier(2)
print(doubler(27.0))
54.0
In [32]:
tripler = create_multiplier(3)
print(tripler(27))
81
In [33]:
#A lambda function can also be passed as a parameter to a function
def operate_on_two(f):
    return f(2)
In [34]:
print(operate_on_two(lambda x : x**4))
16
In [35]:
print(operate_on_two(lambda x : x**2+15*x+2))
36

Conditional and loop blocks

As with other blocks conditional and for loops are encapsulated by indentation. Conditions are not enclosed by brackets.
If-else if-else blocks are done using if-elif-else.
For loops are done by iterating a variable over a list 'for x in [x1,x2,x3,x4]'

In [36]:
from math import floor,sqrt

def factorial(a):
    if a<0:
        print("Cannot calculate factorial for a negative number!")
        return
    elif a in [0,1]:
        return 1
    else:
        result = 1
        for n in (range(2,a+1)): #range creates a list of numbers [start_value,end_value)
            result *= n
        return result
In [37]:
print(factorial(-5))
Cannot calculate factorial for a negative number!
None
In [38]:
print(factorial(0))
1
In [39]:
print(factorial(3))
6

Lists

Unlike many other programming languages lists in python do not have a strict type, also unlike arrays they do not have a fixed length, and can be used to create multi-dimensional matricies. Also python is a zero-based index language, this means that the first element of a list is in position 0. Furthermore Python allows you to reference an item both from the beginning of the list (using positive positioning) or from the end (using negative positioning). In the latter case the last item is identified by -1, the second to last by -2 and so on

In [40]:
empty_list = []
print(empty_list)
[]
In [41]:
literal_list = ['one','two','three']
print(literal_list)
['one', 'two', 'three']
In [42]:
number_list = [1,2,3,4,5,6]
print(number_list)
[1, 2, 3, 4, 5, 6]
In [43]:
square_matrix = [[1,2,3],[4,5,6],[7,8,9]]
print(square_matrix)
type(square_matrix)
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Out[43]:
list
In [44]:
list_of_lists = [[1,2,3],['five','six','seven'],[8,'nine','ten',11]]
print(list_of_lists)
[[1, 2, 3], ['five', 'six', 'seven'], [8, 'nine', 'ten', 11]]
In [45]:
number_list = [1,2,3,4,5,6]
print('The second elements in number_list is {0}'.format(number_list[1]))
print('The last element in number_list is {}'.format(number_list[-1]))
The second elements in number_list is 2
The last element in number_list is 6

Range is a built in function used to create lists of consecutive integers. It can be called in different ways

  • range(end) #The start defaults to 0
  • range(start,end[, step]) #In documentation square brackets around a parameter indicate it is optional

the start parameter is included in the list while the end is excluded i.e. [start,end)

In [46]:
for x in range(7):
    print(x)
0
1
2
3
4
5
6
In [47]:
for x in range(3,11):
    print(x)
3
4
5
6
7
8
9
10
In [48]:
for x in range(5,13,3):
    print(x)
5
8
11
In [49]:
for x in range(20,10,-1):
    print(x)
20
19
18
17
16
15
14
13
12
11

List slicing alist[start:end:step]

Python allows you to copy part or all of a list by slicing it. If start or end are omitted then they default to 0 and -1 respectively, step can also be omitted and defaults to 1. Again the sliced extremes are inclusive of the start position and exclusive of the end position.

In [50]:
alist = list(range(13,30))
print(alist)
[13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
In [51]:
print(alist[2:9]) #take all the elements from the second (included) to the ninth (excluded)
[15, 16, 17, 18, 19, 20, 21]
In [52]:
print(alist[2:15:2])#take every other elements from the second (included) to the fifteenth (excluded)
[15, 17, 19, 21, 23, 25, 27]
In [53]:
print(alist[:10])#take every element from the first(included) to the tenth(excluded)
[13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
In [54]:
print(alist[5:])#take every element from the fifth(included) to the last (included)
[18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
In [55]:
print(alist[3:-3])#take every element from the third(included) to the third from the last (excluded)
[16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]
In [56]:
print(alist[-2:])#take the last two elements
[28, 29]
In [57]:
print(alist[:-2])#take everyting except the last two
[13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]
In [58]:
print(alist[::-1])#reverses the list
[29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13]
In [59]:
print(alist[:])#copy the whole list
[13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
In [60]:
#remember Python doesn't really care what type your variable is
astring="This is a string. It too can be sliced!!"
print(astring[18:-1])
It too can be sliced!

List comprehansion

A fast and compact way to initialize lists starting from another list or function. The basic structure is

A = [f(x) for x in [...] if ...]

the if condition is optional and can be any function returning a boolean

In [61]:
#To create a list of all the squares of the numbrs up to 10
A = [x**2 for x in range(11)]
print(A)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
In [62]:
#To take the even/odd numbers up to 50
even = [x for x in range(50) if x%2==0]
print(even)
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48]
In [63]:
#To take the even/odd numbers up to 50
odd = [x for x in range(50) if x%2!=0]
print(odd)
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49]

Dictionaries

Dictionaries are collections of key:value pairs. Inserted values can be obtained/set by using the corrisponding keys. If the key does not exist a new key/value pair will be created. Dictionaries are initialized using curly brackets

In [64]:
a = {}
a['first'] = 1
a['second'] = 2
print (a)

print (a['second'])
{'first': 1, 'second': 2}
2
In [65]:
a = {'Temperature':'C', 'Pressure':'atm'}
print(a)
{'Temperature': 'C', 'Pressure': 'atm'}

Creating custom classes

Python is an Object Oriented Programming (OOP) language, in order to fully use its potential it is important to understand how to create custom classes. We will see how to create a custom vector class which implements the most common operations on vectors. All methods in a class are automatically called with the first parameter being a reference to the instance of the object itself, traditionally the first parameter is therefore always self, and can be used to access the classes variables. In order to make the code more readable each method will be implement in a different cell and in order for this to work they will all be preceded by class CustomVector(). In a real case scenario all the methods would be incorporated in a single definition:

    class CustomVector():
        def method1(self,...):
            ....

        def method2(self,...):
            ....
In [66]:
class CustomVector(object):#object is not necessary for python 3.x
    '''
    The __init__ method is a base method, called whenever an instance of the class is created
    '''
    def __init__(self,vector_list=[0,0,0]):
        if(len(vector_list)!=3):
            raise Exception("Custom vector must be initialized by a list exactly three elements long."+ 
                            "The passed list had {0} elements".format(len(vector_list)))
        self._x = vector_list[0]
        self._y = vector_list[1]
        self._z = vector_list[2]
        
    def __str__(self):#This basic method is usd to rener the object as a string
        return 'Custom Vector:[{},{},{}]'.format(self._x, self._y, self._z)
In [67]:
vec = CustomVector([1,2,3])
print(vec)
print ("The vector's x component is {0}".format(vec._x))
print ("The vector's y component is {0}".format(vec._y))
print ("The vector's z component is {0}".format(vec._z))
Custom Vector:[1,2,3]
The vector's x component is 1
The vector's y component is 2
The vector's z component is 3

We were however forced to access "private" variables in order to print out the values of our vector. So let's make them accessable through what are called properties. The advantage being that we can, for example, have control over which values we accept, we might not want one of the components to be other than a number.

N.B. if you are accustomed with C/C++ and other languages properties resemble getters and setters, with the advantage of not having to use methods to get or set the value, but are instead accessible as public members of the class

In [68]:
from numbers import Number
class CustomVector(CustomVector):#This is only needed in order to split the definition over multiple cells
    def __init__(self,vector_list=[0,0,0]):
        if(len(vector_list)!=3):
            raise Exception("Custom vector must be initialized by a list exactly three elements long. The passed list had {0} elements".format(len(vector_list)))
        self.x = vector_list[0]
        self.y = vector_list[1]
        self.z = vector_list[2]
    
    @property
    def x(self):
        return self._x
    @x.setter
    def x(self, x):
        if(isinstance(x,Number)):
            self._x=x
        else:
            raise TypeError("Vector components can only be numbers")
        
    @property
    def y(self):
        return self._y
    @y.setter
    def y(self, y):
        if(isinstance(y,Number)):
            self._y=y
        else:
            raise TypeError("Vector components can only be numbers")
        
    @property
    def z(self):
        return self._z
    @z.setter
    def z(self, z):
        if(isinstance(z,Number)):
            self._z=z
        else:
            raise TypeError("Vector components can only be numbers")
In [69]:
vec = CustomVector([1,2,3])
print ("The vector's x component is {0}".format(vec.x))
print ("The vector's y component is {0}".format(vec.y))
print ("The vector's z component is {0}".format(vec.z))
vec.x='a' 
The vector's x component is 1
The vector's y component is 2
The vector's z component is 3
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-69-37ff9dd4e3b6> in <module>
      3 print ("The vector's y component is {0}".format(vec.y))
      4 print ("The vector's z component is {0}".format(vec.z))
----> 5 vec.x='a'

<ipython-input-68-0b63569c9b94> in x(self, x)
     16             self._x=x
     17         else:
---> 18             raise TypeError("Vector components can only be numbers")
     19 
     20     @property

TypeError: Vector components can only be numbers

As it is this class is not particularly useful, as we cannot do any operations on it. Let's start off by implementing the basic operations:

  • Addition
  • Subtraction
  • Multiplication by a scalar
  • Division by a scalar

For the time being we will not perform checks of the types passed, we will assume users are diligent enough to pass the right parameters

In [70]:
class CustomVector(CustomVector):
    def __add__(self, other):
        return CustomVector([self.x+other.x, self.y+other.y, self.z+other.z])
    def __sub__(self, other):
        return CustomVector([self.x-other. x, self.y-other.y, self.z-other.z])
    def __mul__(self, scalar): 
        
        return CustomVector([self.x*scalar, self.y*scalar, self.z*scalar])
    def __truediv__(self, scalar):
        return CustomVector([self.x/scalar, self.y/scalar, self.z/scalar])
In [71]:
#We can now do this:
vec = CustomVector([1,2,3])
vec2 = CustomVector([5,2,1])
print(vec+vec2)
print(vec-vec2)
print(vec*3)
print(vec/2)
Custom Vector:[6,4,4]
Custom Vector:[-4,0,2]
Custom Vector:[3,6,9]
Custom Vector:[0.5,1.0,1.5]

Let's also add some vector specific methods

In [72]:
from math import sqrt
class CustomVector(CustomVector):
    def dot(self, other):
        return self.x*other.x+self.y*other.y+self.z*other.z
    def cross(self, other):
        return CustomVector([self.y*other.z-self.z*other.y, self.z*other.x-self.x*other.z, self.x*other.y-self.y*other.x])
    def euclidean_norm(self):
        return sqrt(self.x**2+self.y**2+self.z**2)
In [73]:
vec = CustomVector([1,2,3])
vec2 = CustomVector([5,2,1])
print(vec.euclidean_norm())
print(vec.dot(vec2))
print(vec.cross(vec2))
3.7416573867739413
12
Custom Vector:[-4,14,-8]

Further expandion

At this point one can imagine to expand the class by implementing methods to enable iteration (this would allow us to use list comprehension for example) - __iter__ and __next__, other useful methods for using vectors, calculating directions etc.