# integers and floats
Python: syntax review
Basic Types
numbers
1
1.0
# conversion with int() and float()
float( 1 )
int(2.9) # floor
# no difference between various types of floats (16 bits, 32 bits, 64 bits, ...)
type( 2.2**50 ) # this doesn't fit in 32 bits
# usual operations + - *
print( 2 + 3 )
print( 9 -6 )
print( 3 / 2 )
print(2304310958*41324)
# divisions / and //
print(3/4)
print(13//4)
# exponentiation ** (not ^!)
# (1.04)^10
1.04)**10 (
# comparison operators: >, <, >=, <=, ==
print((1.0453432)*(0.96) > 1.001 )
print(1.001 >= 1.001)
# comparison operators can be chained:
print(0.2<0.4<0.5)
print(0.5<=0.4<=0.5) # equivalent to ((0.5<=0.4) and(0.4<=0.5))
special types: boolean and None
There are only two booleans: True
and False
(note uppercase). None
is a dummy type, which is used when no other type fits.
print( False )
True
True, False, None) (
Double equal sign tests for equality. Result should always be a boolean.
True==False
Logical operators are not
, and
and or
:
True or False) (
not (True or False)
1.3**1.04 > 1.9) | (1000**1143>1001**1142) (
Operators or
and and
can be replaced by |
and &
respectively. They are non-greedy, that is terms are not evaluated if the result of the comparison is already known.
False and (print("Hello"))
print( (print("Hello")) and False )
strings
definition
Strings are defined by enclosing characters either by '
(single quotes) or "
(double quote). Single quotes strings can contain double quotes strings and vice-versa.
"name"
'name'
'I say "hello"'
"You can 'quote' me"
Strings spanning over sever lines can be defined with triple quotes (single or double).
= """¿Qué es la vida? Un frenesí.
s ¿Qué es la vida? Una ilusión,
una sombra, una ficción,
y el mayor bien es pequeño;
que toda la vida es sueño,
y los sueños, sueños son.
"""
It is also possible to use the newline character \n
.
"La vida es sueño,\ny los sueños, sueños son."
print("La vida es sueño,\ny los sueños, sueños son.")
character sets
Strings can contain any unicode character:
= "🎻⽻༽" s
Refresher: ASCII vs unicode
ASCII (or ASCII-US) is an old standard which codes a character with 7 bits (or 8 bits for extended ASCII). This allows to code 128 different characters (256 for ex-ASCII).
Only a subset of these characters can be printed regularly.
chr(44)
# ASCII:
for i in range(32,127):
print( chr(i), end=' ')
The other characters include delete, newline and carriage return among others.
= 'This is\na\nmultiline string.' # note the newline character '\n' s
# print(s)
len(s)
Some antiquated platforms still use newline + carriage return at the end of each line. This is absolutely not required and causes incompatibilities.
= 'This is\n\ra\n\rmultiline string.' # note the newline character '\n' and carriager return '\r' s2
print(s2)
print(len(s2))
Unicode contains a repertoire of over 137,000 characters with all ASCII characters as subcases
To type: copy/paste, ctrl+shift+hexadecimal, latex + tab
Variable names aka identifiers can contain unicode characters with some restrictions: - they cannot start with a digit - they can’t contain special variables (‘!,#,@,%,$’ and other unicode specials ???) - they can contain underscore
operations
concatenation
'abc' + 'def'
'abc'*3
'abc' + 'abc' + 'abc'
substrings
# strings can be accessed as arrays (0 based indexing)
= "a b c"
s 0] s[
# slice notation ( [min,max[ )
= "a b c d"
s 2:5] # 0-based; 2 included, 5 excluded s[
# substrings are easy to check
"a" in s
"b c" in "a b c d"
It is impossible to modify a substring.
# but are immutable
= "a b c"
s #s[1] = 0 error
Instead, one can replace a substring:
s
' ', '🎻') s.replace(
Or use string interpolation
# string interpolation (old school)
"ny name is {name}".format(name="nobody")
"calculation took {time}s".format(time=10000)
# number format can be tweaked
"I am {age:.0f} years old".format(age=5.65)
# formatted strings
= 15914884.300292
elapsed
f"computations took {elapsed/3600:.2f} hours"
= "arnaldur" name
"dasnfnaksujhn {name}".format(name="whatever")
# basic string operations: str.split, str.join, etc...
# fast regular expressions
# more on it, with text processing lesson
str.split("me,you,others,them",',')
str.join( " | ",
str.split("me,you,others,them",','),
)
Escaping characters
The example above used several special characters: \n
which corresponds to only one ascii character and {
/}
which disappears after the string formatting. If one desires to print these characters precisely one needs to escape them using \
and {
}
.
print("This is a one \\nline string")
print("This string keeps some {{curly}} brackets{}".format('.'))
Other operations on strings
(check help(str) or help?)
- len() : length
- strip() : removes characters at the ends
- split() : split strings into several substrings separated by separator
- join() : opposite of split
'others,'
',me,others,'.strip(',')
',') s.count(
help(str)
Assignment
Any object can be reused by assigning an identifier to it. This is done with assignment operator =
.
= 3
a a
Note that assignment operator =
is different from comparison operator ==
. Comparison operator is always True or False, while assignment operator has no value.
2==2) == True (
Containers
Any object created in Python is identified by a unique id
. One can think of it approximately as its reference. Object collections, contain arbitrary other python objects, that is they contain references to them.
id(s)
tuples
construction
1,2,"a" ) (
Since tuples are immutable, two identical tuples, will always contain the same data.
= (2,23)
t1 = (2,23) t2
# can contain any data
= (1,2,3,4,5,6)
t = (t, "a", (1,2))
t1 = (0,) # note trailing coma for one element tuple
t2 = (t, "a", (1,2)) t3
0] = 78 t[
Since tuples never change, they can be compared by hash values (if the data they hold can be hashed). Two tuples are identical if they contain the same data.
Remark: hash function is any function that can be used to map data of arbitrary size to data of a fixed size. It is such that the probability of two data points of having the same hash is very small even if they are close to each other.
== t1 t3
print(hash(t3))
print(hash(t1))
id(t3), id(t1)
access elements
# elements are accessed with brackets (0-based)
0] t[
# slice notation works too ( [min,max[ )
1:3] t[
# repeat with *
3,2)*5 (
0)*5 (
0,)*5 (
*5 t2
# concatenate with +
+t1+t2 t
# test for membership
1 in t) (
lists
lists are enclosed by brackets are mutable ordered collections of elements
= [1,"a",4,5] l
1] l[
1:] # if we omit the upper-bound it goes until the last element l[
2] l[:
# lists are concatenated with +
2] + l[2:] == l l[:
# test for membership
5 in l) (
# lists can be extended inplace
= [1,2,3]
ll 4,5]) # several elements
ll.extend([6)
ll.append( ll
Since lists are mutable, it makes no sense to compute them by hash value (or the hash needs to be recomputed every time the values change).
hash(ll)
Sorted lists can be created with sorted (if elements can be ranked)
= [4,3,5] ll
sorted(ll)
ll
It is also possible to sort in place.
ll.sort() ll
sorted(ll) # creates a new list
# does it in place ll.sort()
# in python internals: ll.sort() equivalent sort(ll)
set
Sets are unordered collections of unique elements.
= set([1,2,3,3,4,3,4])
s1 = set([3,4,4,6,8])
s2 print(s1, s2)
print(s1.intersection(s2))
3,4} == {4,3} {
dictionaries
Dictionaries are ordered associative collections of elements. They store values associated to keys.
# construction with curly brackets
= {'a':0, 'b':1} d
d
# values can be recovered by indexing the dict with a key
'b'] d[
= dict()
d # d['a'] = 42
# d['b'] = 78
d
'a'] = 42 d[
'b'] d[
Keys can be any hashable value:
'a','b')] = 100 d[(
'a','b'] ] = 100 # that won't work d[ [
Note: until python 3.5 dictionaries were not ordered. Now the are guaranteed to keep the insertion order
Control flows
Conditional blocks
Conditional blocks are preceeded by if
and followed by an indented block. Note that it is advised to indent a block by a fixed set of space (usually 4) rather than use tabs.
if 'sun'>'moon':
print('warm')
They can also be followed by elif and else statements:
= 0.5
x if (x<0):
= 0.0
y elif (x<1.0):
= x
y else:
= 1+(x-1)*0.5 y
Remark that in the conditions, any variable can be used. The following evaluate to False: - 0 - empty collection
if 0: print("I won't print this.")
if 1: print("Maybe I will.")
if {}: print("Sir, your dictionary is empty")
if "": print("Sir, there is no string to speak of.")
While
The content of the while loop is repeated as long as a certain condition is met. Don’t forget to change that condition or the loop might run forever.
= False
point_made = 0
i while not point_made:
print("A fanatic is one who can't change his mind and won't change the subject.")
+= 1 # this is a quasi-synonym of i = i + 1
i if i>=20:
= True point_made
Loops
# while loops
= 0
i while i<=10:
print(str(i)+" ", end='')
+=1 i
# for loop
for i in [0,1,2,3,4,5,6,7,8,9,10]:
print(str(i)+" ", end='')
# this works for any kind of iterable
# for loop
for i in (0,1,2,3,4,5,6,7,8,9,10):
print(str(i)+" ", end='')
# including range generator (note last value)
for i in range(11):
print(str(i)+" ", end='')
range(11)
# one can also enumerate elements
= ("france", "uk", "germany")
countries for i,c in enumerate(countries):
print(f"{i}: {c}")
= set(c) s
# conditional blocks are constructed with if, elif, else
for i,c in enumerate(countries):
if len(set(c).intersection(set("brexit"))):
print(c)
else:
print(c + " 😢")
It is possible to iterate over any iterable. This is true for a list or a generator:
for i in range(10): # range(10) is a generator
print(i)
for i in [0,1,2,3,4,5,6,7,8,9]:
print(i)
We can iterate of dictionary keys or values
= {1:2, 3:'i'}
d for k in d.keys():
print(k, d[k])
for k in d.values():
print(k)
or both at the same time:
for t in d.items():
print(t)
# look at automatic unpacking
for (k,v) in d.items():
print(f"key: {k}, value: {v}")
Comprehension and generators
There is an easy syntax to construct lists/tuples/dicts: comprehension. Syntax is remminiscent of a for loop.
**2 for i in range(10)] [i
set(i-(i//2*2) for i in range(10))
**2 for i in range(10)} {i: i
Comprehension can be combined with conditions:
**2 for i in range(10) if i//3>2] [i
Behind the comprehension syntax, there is a special object called generator. Its role is to supply objects one by one like any other iterable.
# note the bracket
= (i**2 for i in range(10))
gen # does nothing gen
= (i**2 for i in range(10))
gen for e in gen:
print(e)
= (i**2 for i in range(10))
gen print([e for e in gen])
There is a shortcut to converte a generator into a list: it’s called unpacking:
= (i**2 for i in range(10))
gen *gen] [
Functions
Wrong approach
= 34
a1 = (1+a1*a1)
b1 = (a1+b1*b1)
c1
= 36
a2 = (1+a2*a2)
b2 = (a2+b2*b2)
c2
print(c1,c2)
Better approach
def calc(a):
= 1+a*a
b = a+b*b
c return c
34), calc(36)) (calc(
it is equivalent to replace the content of the function by:
= 32
a = a # def calc(a):
_a = 1+_a*_a # b = 1+a*a
_b = _a+_b*_b # c = a+b*b
_c = _c # return c res
Note that variable names within the function have different names. This is to avoid name conflicts as in:
= 1
y def f(x):
= x**2
y return y+1
def g(x):
= x**2+0.1
y return y+1
= f(1.4)
r1 = g(1.4)
r2 = y
r3 (r1,r2,r3)
= ['france', 'germany']
l def fun(i):
print(f"Country: {l[i]}")
0) fun(
= ['france', 'germany']
l def fun(i):
= ['usa', 'japan']
l 'spain')
l.append(print(f"Country: {l[i]}")
0) fun(
l
In the preceding code block, value of y
has not been changed by calling the two functions. Check pythontutor.
Calling conventions
Function definitions start with def
and a colon indentation. Value are returned by return
keyword. Otherwise the return value is None
. Functions can have several arguments: def f(x,y)
but always one return argument. It is however to return a tuple, and “unpack” it.
def f(x,y):
= x+y
z1 = x-y
z2 return (z1,z2) # here brackets are optional: `return z1,z2` works too
= f(0.1, 0.2)
res = f(0.2, 0.2) # t1,t2=res works too t1, t2
res
Named arguments can be passed in any order and receive default values.
def problem(why="The moon shines.", what="Curiosity killed the cat.", where="Paris"):
print(f"Is it because {why.lower().strip('.')} that {what.lower().strip('.')}, in {where.strip('.')}?")
='Paris') problem(where
="ESCP", why="Square root of two is irrational", what="Some regressions never work.") problem(where
Positional arguments and keyword arguments can be combined
def f(x, y, β=0.9, γ=4.0, δ=0.1):
return x*β+y**γ*δ
0.1, 0.2) f(
Docstrings
Functions are documented with a special string. Documentation It must follow the function signature immediately and explain what arguments are expected and what the function does
def f(x, y, β=0.9, γ=4.0, δ=0.1): # kjhkugku
"""Compute the model residuals
Parameters
----------
x: (float) marginal propensity to do complicated stuff
y: (float) inverse of the elasticity of bifractional risk-neutral substitution
β: (float) time discount (default 0.9)
γ: (float) time discount (default 4.0)
δ: (float) time discount (default 0.1)
Result
------
res: beta-Hadamard measure of cohesiveness
"""
= x*β+y**γ*δ
res return res
Remark: Python 3.6 has introduced type indication for functions. They are useful as an element of indication and potentially for type checking. We do not cover them in this tutorial but this is what they look like:
def f(a: int, b:int)->int:
if a<=1:
return 1
else:
return f(a-1,b) + f(a-2,b)*b
Packing and unpacking
A common case is when one wants to pass the elements of an iterable as positional argument and/or the elements of a dictionary as keyword arguments. This is espacially the case, when one wants to determine functions that act on a given calibration. Without unpacking all arguments would need to be passed separately.
= (0.1, 0.2)
v = dict(β=0.9, γ=4.0, δ=0.1)
p
0], v[1], β=p['β'], γ=p['γ'], δ=p['δ']) f(v[
There is a special syntax for that: *
unpacks positional arguments and **
unpacks keyword arguments. Here is an example:
*v, **p) f(
The same characters *
and **
can actually be used for the reverse operation, that is packing. This is useful to determine functions of a variable number of arguments.
def fun(**p):
= p['β']
β return β+1
=1.0)
fun(β=1.0, γ=2.0) # γ is just ignored fun(β
Inside the function, unpacked objects are lists and dictionaries respectively.
def fun(*args, **kwargs):
print(f"Positional arguments: {len(args)}")
for a in args:
print(f"- {a}")
print(f"Keyword arguments: {len(args)}")
for key,value in kwargs.items():
print(f"- {key}: {value}")
0.1, 0.2, a=2, b=3, c=4) fun(
Functions are first class objects
This means they can be assigned and passed around.
def f(x): return 2*x*(1-x)
= f # now `g` and `f` point to the same function
g 0.4) g(
def sumsfun(l, f):
return [f(e) for e in l]
0.0, 0.1, 0.2], f) sumsfun([
def compute_recursive_series(x0, fun, T=50):
= [x0]
a for t in range(T):
= a[-1]
x0 = fun(x0)
x
a.append(x)return a
0.3, f, T=5) compute_recursive_series(
There is another syntax to define a function, without giving it a name first: lambda functions. It is useful when passing a function as argument.
sorted(range(6), key=lambda x: (-2)**x)
Lambda functions are also useful to reduce quickly the number of arguments of a function (aka curryfication)
def logistic(μ,x): return μ*x*(1-x)
# def chaotic(x): return logistic(3.7, x)
# def convergent(x): return logistic(2.5, x)
= lambda x: logistic(3.7, x)
chaotic = lambda x: logistic(2.5, x) convergent
= [compute_recursive_series(0.3,fun, T=20) for fun in [convergent, chaotic]]
l *zip(*l)] [
from matplotlib import pyplot as plt
import numpy as np
= np.array(l)
tab 0,:-1],tab[0,1:])
plt.plot(tab[= np.array(l)
tab 1,:-1],tab[1,1:])
plt.plot(tab[0,1),np.linspace(0,1))
plt.plot(np.linspace("$x_n$")
plt.xlabel("$x_{n+1}$")
plt.ylabel( plt.grid()
Functions pass arguments by reference
Most of the time, variable affectation just create a reference.
= [1,2,3]
a = a
b 1] = 0
a[ (a, b)
To get a copy instead, one needs to specify it explicitly.
import copy
= [1,2,3]
a = copy.copy(a)
b 1] = 0
a[ (a, b)
Not that copy follows only one level of references. Use deepcopy for more safety.
= ['a','b']
a0 = [a0, 1, 2]
a = copy.copy(a)
b 0][0] = 'ξ'
a[ a, b
= ['a','b']
a0 = [a0, 1, 2]
a = copy.deepcopy(a)
b 0][0] = 'ξ'
a[ a, b
Arguments in a function are references towards the original object. No data is copied. It is then easy to construct functions with side-effects.
def append_inplace(l1, obs):
l1.append(obs)return l1
= ([1,2,3], 1.5)
l1, obs = append_inplace(l1,obs)
l2 print(l2, l1)
# note that l1 and l2 point to the same object
0] = 'hey'
l1[print(l2, l1)
This behaviour might feel unnatural but is very sensible. For instance if the argument is a database of several gigabytes and one wants to write a function which will modify a few of its elements, it is not reasonable to copy the db in full.
Objects
Objects ?
- can be passed around / referred too
- have properties (data) and methods (functions) attached to them
- inherit properties/methods from other objects
Objects are defined by a class definition. By convention, classes names start with uppercase . To create an object, one calls the class name, possibly with additional arguments.
class Dog:
= "May" # class property
name
= Dog()
d1 = Dog()
d2
print(f"Class: d1->{type(d1)}, d2->{type(d2)}")
print(f"Instance address: d2->{d1},{d2}")
Now, d1
and d2
are two different instances of the same class Dog
. Since properties are mutable, instances can have different data attached to it.
= "Boris"
d1.name print([e.name for e in [d1,d2]])
Methods are functions attached to a class / an instance. Their first argument is always an instance. The first argument can be used to acess data held by the instance.
class Dog:
= None # default value
name def bark(self):
print("Wouf")
def converse(self):
= self.name
n print(f"Hi, my name is {n}. I'm committed to a strong and stable government.")
= Dog()
d # bark(d)
d.bark() d.converse()
Constructor
There is also a special method __init__
called the constructor. When an object is created, it is called on the instance. This is useful in order to initialize parameters of the instance.
class Calibration:
def __init__(self, x=0.1, y=0.1, β=0.0):
if not (β>0) and (β<1):
raise Exception("Incorrect calibration"})
self.x = x
self.y = y
self.β = β
= Calibration()
c1 = Calibration(x=3, y=4) c2
Two instances of the same class have the same method, but can hold different data. This can change the behaviour of these methods.
# class Dog:
# state = 'ok'
# def bark(self):
# if self.state == 'ok':
# print("Wouf!")
# else:
# print("Ahouuu!")
# d = Dog()
# d1 = Dog()
# d1.state = 'hungry'
# d.bark()
# d1.bark()
To write a function which will manipulate properties and methods of an object, it is not required to know its type in advance. The function will succeed as long as the required method exist, fail other wise. This is called “Duck Typing”: if it walks like a duck, it must be a duck…
class Duck:
def walk(self): print("/-\_/-\_/")
class Dog:
def walk(self): print("/-\_/*\_/")
def bark(self): print("Wouf")
= [C() for C in (Duck,Dog)]
animals def go_in_the_park(animal):
for i in range(3): animal.walk()
for a in animals:
go_in_the_park(a)
Inheritance
The whole point of classes, is that one can construct hierarchies of classes to avoid redefining the same methods many times. This is done by using inheritance.
class Animal:
def run(self): print("👣"*4)
class Dog(Animal):
def bark(self): print("Wouf")
class Rabbit(Animal):
def run(self):
super().run() ; print( "🐇" )
Animal().run()= Dog()
dog
dog.run()
dog.bark() Rabbit().run()
In the above example, the Dog class inherits from inherits the method run
from the Animal class: it doesn’t need to be redefined again. Essentially, when run(dog)
is called, since the method is not defined for a dog, python looks for the first ancestor of dog
and applies the method of the ancestor.
Special methods
By conventions methods starting with double lowercase __
are hidden. They don’t appear in tab completion. Several special methods can be reimplemented that way.
class Calibration:
def __init__(self, x=0.1, y=0.1, β=0.1):
if not (β>0) and (β<1):
raise Exception("Incorrect calibration")
self.x = x
self.y = y
self.β = β
def __str__(self):
return f"Calibration(x={self.x},y={self.y}, β={self.β})"
str(Calibration() )
complement
Python is not 100% object oriented. - some objects cannot be subclassed - basic types behave sometimes funny (interning strings)
mindfuck: Something that destabilizes, confuses, or manipulates a person’s mind.
= 'a'*4192
a = 'a'*4192
b is b a
= 'a'*512
a = 'a'*512
b is b a