Lecture 02: Primitives

You will be given an in-depth introduction to the fundamentals of Python (objects, variables, operators, classes, methods, functions, conditionals, loops). You learn to discriminate between different types such as integers, floats, strings, lists, tuples and dictionaries, and determine whether they are subscriptable (slicable) and/or mutable. You will learn about referencing and scope. You will learn a tiny bit about floating point arithmetics.

Take-away: This lecture is rather abstract compared to the rest of the course. The central take-away is a language to speak about programming in. An overview of the map, later we will study the terrain in detail. It is not about memorizing. Almost no code projects begin from scratch, you start by copying in similar code you have written for another project.

Hopefully, this notebook can later be used as a reference sheet. When you are done with the DataCamp courses, read through this notebook, play around with the code, and ask questions if there is stuff you do not understand.

Links:

  • Tutorial: A more detailed tutorial is provided here.
  • Markdown: All text cells are written in Markdown. A guide is provided here.

1. Your first notebook session

Optimally: You have this notebook open as well on your own computer.

1.# 1.# 1. Download course material

  1. Follow the installation guide
  2. If you have not yet restarted JupyterLab after installations, do it now.
  3. Go to the Extensions tab (lowest icon in the far left side). Press Warning. Agree to enable extensions.
  4. Make sure you are in the folder, where the course content should be located.
  5. Press the tab Git.
  6. Press Clone a repository
  7. Paste in https://github.com/NumEconCopenhagen/lectures-2022 and enter.
  8. You now have all course material in the folder lectures-2022
  9. Do the same thing for exercise class material using the url:
    • https://github.com/NumEconCopenhagen/exercises-2022
  10. Create a copy of the cloned folder, where you work with the code (otherwise you may get conflicting copies when updating)

Updating your local version of a notebook.

  • 1: Close down all tabs.
  • 2: Press the tab Git.
  • 3: Press Open Git Repository in Terminal
  • 4: Make sure that you are in the repository folder you want to update (lectures-2022 or exercises-2022, or your own repo).
    • On Windows write cd.
    • On Mac write pwd.
    • This will display your current location.
  • 5: See if YOU have any changes
    • Write git status.
    • Note if it says modified: 02/Primitives.ipynb, or show modifications to other files.
  • 6: View incoming changes
    • Write git fetch
    • Write git diff --name-status main..origin/main
  • 7: Remove conflicting notebooks
    • Were any of the files listed in Step 6 also found on the list produced in Step 5? Eg. 02/Primitives.ipynb in both places?
    • If there are any overlaps (conflicts), you need to discard your own changes (you'll learn to stash later).
    • Of course, if you made notes or experiments that you want to keep, you can always make a copy of your conflicting file and keep that. Just use a good old copy-paste and give your own file a new name.
    • Then write git checkout -- 02/Primitives.ipynb only if there was a conflict for that file. Do so with all overlapping files.
  • 8: Accept incoming changes
    • Write git merge

As you may have guessed, the command git pull is identical to a git fetch combined with a git merge.

Note: This guide is only a rough start, meant to avoid all conflicting updates. You will soon learn to do better and not having to discard all you local changes in case of overlaps.

PROBLEMS? Ask your teaching asssistant ASAP.

1.1 Execution of code in cells

  • Movements: Arrows and scrolling
  • Run cell and advance: Shift+Enter
  • Run cell: Ctrl+Enter
  • Edit: Enter
  • Toggle sidebar: Ctrl+B
  • Change to markdown cell: M
  • Change to code cell: Y

2. Fundamentals

Computers

Before we start looking at code, let's check out the structure of a computer:

computer

2.1 The work flow of your computer

  • You give it some command through an input device (eg. keyboard)
  • The control unit figures out if any new data from the hard disk (external storage) is needed
  • If it is needed, the control unit loads that data and puts in an address in memory
  • From memory, the data can be accessed by the arithmetic unit in the cpu to do the prompted calculations
  • Resulting data is stored in memory
  • Control unit can then pass resulting data in memory to output devices (eg. screen) and to hard disk

Figuratively speaking:

  • Memory is like a well organized file cabinet, where all data is neatly stored and quickly accessible.
  • Each drawer of this "file cabinet" is an address where data can be stored.
  • We can see the address of any variable in memory by applying the function id().
  • In turn, the hard disk is like a cellar with reports in boxes. It contains much more data but is also slower to retrieve from.

2.2 What is a variable?

  • A variable in python is thus a reference (or pointer) to a place in memory where data resides.
  • There are many types of variables.
  • Some store data directly, some are containers for data.
  • There are 4 types of data:
    • Booleans (true/false)
    • Integers
    • Floats (decimal numbers)
    • Strings
  • The 4 kinds of data use different amounts of memory pr unit. Important not to waste memory!
  • A variable that references one of these data types directly is an Atomic type.
    The data of an atomic type is unchangeable at the address.
  • Variable types that are containers are eg. lists, dictionaries, data frames, etc.
    Their data is allowed to change.
  • All variables are objects: bundles of data and functions.

2.3 Atomic types

The most simple types are called atomic, because they cannot be changed - only overwritten.

Integers (int): -3, -2, -1, 0, 1, 2, 3, ,\ldots, \infty
There is no cap on the size of ints in python!

[53]
# variable x references an integer type object with a value of 1
x = 1 

print('x is a', type(x)) # prints the type of x
print('x =', x)
print('Address of x is',id(x)) 
print('x uses',sys.getsizeof(x),'bytes')

x = x*2
print('\nNote that the address is new, as x gets a new value!')
print('x =', x)
print('Address of x is',id(x)) 
x is a <class 'int'> x = 1 Address of x is 140496634779952 x uses 28 bytes Note that the address is new, as x gets a new value! x = 2 Address of x is 140496634779984

Decimal numbers (float): 3.14, 2.72, 1.0, etc.

[52]
x = 1.2
# variable x references an floating point (decimal number) type object 
# with a value of 1.2 

print('x is a',type(x))
print('x =',x)
print('x uses',sys.getsizeof(x),'bytes')
x is a <class 'float'> x = 1.2 x uses 24 bytes

Strings (str): 'abc', '123', 'this is a full sentence', etc.

[54]
x = 'abc' 
# variable x references a string type opbject 
# with a value of 'abc'

print('x is a',type(x))
print('x =',x)
print('x uses',sys.getsizeof(x),'bytes')
x is a <class 'str'> x = abc x uses 52 bytes

Note: Alternatively, use double quotes instead of single quotes.

[39]
x = "abc" 
# variable x reference a string type opbject 
# with a value of 'abc'

print('x is a',type(x))
print('x =',x)
sys.getsizeof("abc")
x is a <class 'str'> x = abc
52

Booleans (bool): True and False

[55]
x = True 
# variable x reference a boolean type opbject 
# with a value of False

print('x is a',type(x))
print('x =',x)
print('x uses',sys.getsizeof(x),'bytes')
x is a <class 'bool'> x = True x uses 28 bytes

Atomic types:

  1. Integers, int
  2. Floating point numbers, float
  3. Strings, str
  4. Booleans, bool

2.4 Type conversion

Objects of one type can (sometimes) be converted into another type.
This obviously changes the address of an atomic type.
As an example, from float to string:

[13]
x = 1.2
# variable x references an floating point (decimal number) type object 
# with a value of 1.2 

y = str(x) 
# variable y now references a string type object 
# with a value created based on x 

print('x =', x)
print('x is a',type(x))
print('y =', y)
print('y is a',type(y))
x = 1.2 x is a <class 'float'> y = 1.2 y is a <class 'str'>

From float to integer: always rounds down!

[15]
x = 2.9

y = int(x) # variable y now references an integer type object  
print('x =', x)
print('y =', y)
print('y is a',type(y))
x = 2.9 y = 2 y is a <class 'int'>

Limitation: You can, however, e.g. not convert a string with letters to an integer.

[ ]
try: # try to run this block
    x = int('222a')
    print('can be done')
    print(x)
except: # if any error found run this block instead
    print('canNOT be done')

Note: The identation is required (typically 4 spaces).

Question: Can you convert a boolean variable x = False to an integer?

  • A: No
  • B: Yes, and the result is 0
  • C: Yes, and the result is 1
  • D: Yes, and the result is -1
  • E: Don't know

2.5 Operators

Variables can be combined using arithmetic operators (e.g. +, -, /, **).
For numbers we have:

[ ]
x = 3
y = 2
print(x+y)
print(x-y)
print(x/y)
print(x*y)
print(x**2)

For strings we can use an overloaded '+' for concatenation:

[ ]
x = 'abc'
y = 'def'
print(x+y)

A string can also be multiplied by an integer:

[ ]
x = 'abc'
y = 2
print(x*y)

Question: What is the result of x = 3**2?

  • A: x = 3
  • B: x = 6
  • C: x = 9
  • D: x = 12
  • E: Don't know

Note: Standard division converts integers to floating point numbers.

[ ]
x = 8
y = x/2 # standard division
z = x//3 # integer division
print(y,type(y))
print(z,type(z))

2.6 Augmentation

Variables can be changed using augmentation operators (e.g. +=, -=, *=, /=)

[23]
x = 3 
print('x =',x)

x += 1 # same result as x = x+1
print('x =',x)
x *= 2 # same result as x = x*2
print('x =',x)
x /= 2 # same result as x = x/2
print('x =',x)
x = 3 x = 4 x = 8 x = 4.0

2.7 Logical operators

Variables can be compared using boolean operators (e.g. ==, !=, <, <=, >, >=).

[ ]
x = 3
y = 2
z = 10
print(x < y) # less than
print(x <= y) # less than or equal
print(x != y) # not equal
print(x == y) # equal

The comparison returns a boolean variable:

[24]
z = x < y # z is now a boolean variable
print(z)
False

2.8 Summary

The new central concepts are:

  1. Variable
  2. Reference
  3. Object
  4. Type (int, float, str, bool)
  5. Value
  6. Operator (+, -, *, **, /, //, % etc.)
  7. Augmentation (+=, -=, *=, /= etc.)
  8. Comparison (==, !=, <, <= etc.)

3. Containers

  • A more complicated type of variable is a container.
  • This is an object, which consists of several objects, for instance atomic types.
  • Therefore, containers are also called collection types.
  • Types of containers
    • Lists
    • Dictionaries
    • Tuples
    • Pandas data frames
    • ...

3.1 Lists

A first example is a list. A list contains elements each referencing some data in memory.

[31]
x = [1,'abc'] 
# variable x references a list type object with elements
# referencing 1 and 'abc'

print(x,'is a', type(x))
[1, 'abc'] is a <class 'list'>

The length (size) of a list can be found with the len function.

[35]
print(f'the number of elements in x is {len(x)}')
the number of elements in x is 2

A list is subscriptable and starts, like everything in Python, from index 0. Beware!

[ ]
print(x[0]) # 1st element 
print(x[1]) # 2nd element

A list is mutable, i.e. you can change its elements on the fly.
That is, you can change its references to objects.

[34]
x[0] = 'def'
x[1] = 2
print('x =', x, 'has id =',id(x))

# Change x[1]
x[1] = 5
print('x =', x, 'has id =',id(x))
x = ['def', 2] has id = 140496784122944 x = ['def', 5] has id = 140496784122944

and add more elements

[ ]
x.append('new_element') # add new element to end of list
print(x)

Slicing a list

A list is slicable, i.e. you can extract a list from a list.

[ ]
x = [0,1,2,3,4,5]
print(x[0:3]) # x[0] included, x[3] not included
print(x[1:3])
print(x[:3])
print(x[1:])
print(x[:99]) # This is very particular to Python. Normally you'd get an error.  
print(x[:-1]) # x[-1] is the last element

print(type(x[:-1])) # Slicing yields a list
print(type(x[-1])) # Unless only 1 element

Explantion:

  • Slices are half-open intervals.
  • x[i:i+n] means starting from element x[i] and create a list of (up to) n elements.
  • Sort of nice if you have calculated i and know you need n elements.
[ ]
# splitting a list at x[3] and x[5] is: 
print(x[0:3])
print(x[3:5])
print(x[5:])

Question: Consider the following code:

[58]
x = [0,1,2,3,4,5]

What is the result of print(x[-4:-2])?

Referencing

Container types, incl. lists, are non-atomic

  • Several variables can refer to the same list.
  • If you change the data of a list that one variable refers to, you change them all.
  • Variables refering to the same object has the same id.
[61]
x = [1,2,3]
print('initial x =',x)
print('id of x is',id(x))
y = x # y now references the same list as x
print('id of y is',id(y))
y[0] = 2 # change the first element in the list y
print('x =',x) # x is also changed because it references the same list as y
initial x = [1, 2, 3] id of x is 140496827424896 id of y is 140496827424896 x = [2, 2, 3]

If you want to know if two variables contain the same reference, use the is operator.

[65]
print(y is x) 
z = [1,2]
w = [1,2] 
print(z is w) # z and w have the same numerical content, but do not reference the same object. 
True False

Conclusion: The = sign copy the reference, not the content!

Atomic types cannot be changed and keep their identity.

[62]
z = 10
w = z
print(z is w) # w is now the same reference as z
z += 5
print(z, w)
print(z is w) # z was overwritten in the augmentation statement. 
True 15 10 False

If one variable is deleted, the other one still references the list.

[ ]
del x # delete the variable x
print(y)

Containers should be copied by using the copy-module:

[ ]
from copy import copy

x = [1,2,3]
y = copy(x) # y now a copy of x
y[0] = 2
print(y)
print(x) # x is not changed when y is changed
print(x is y) # as they are not the same reference

or by slicing:

[ ]
x = [1,2,3]
y = x[:] # y now a copy of x
y[0] = 2
print(y)
print(x) # x is not changed when y is changed

Advanced: A deepcopy is necessary, when the list contains mutable objects.

[ ]
from copy import deepcopy

a = [1,2,3]
x = [a,2,3] # x is a list of a list and two integers
y1 = copy(x) # y1 now a copy x
y2 = deepcopy(x) # y2 is a deep copy

a[0] = 10 # change1
x[-1] = 1 # change2
print(x) # Both changes happened
print(y1) # y1[0] reference the same list as x[0]. Only change1 happened 
print(y2) # y2[0] is a copy of the original list referenced by x[0]

Question: Consider the following code:

[ ]
x = [1,2,3]
y = [x,x]
z = x
z[0] = 3
z[2] = 1

What is the result of print(y[0])?

3.2 Tuples

  • A tuple is an immutable list.
  • Tuples are created with soft parenthesis, t = (1,3,9).
  • As with lists, elements are accessed by brackets, t[0].
  • Immutable: t[0]=10 will produce an error.
  • We use tuples to pass variables around that should not change by accident.
  • Functions will output tuples if you specify multiple output variables.
  • Tuples can also be used as arguments to function.
[ ]
x = (1,2,3) # note: parentheses instead of square backets
print('x =',x,'is a',type(x))
print('x[2] =', x[2], 'is a', type(x[2]))
print('x[:2] =', x[:2], 'is a', type(x[:2]))

But it cannot be changed (it is immutable):

[ ]
try: # try to run this block
    x[0] = 2
    print('did succeed in setting x[0]=2')
except: # if any error found run this block instead
    print('did NOT succeed in setting x[0]=2')
print(x)

3.3 Dictionaries

  • A dictionary is a key-based container.
  • Lists and tuples use numerical indices.
  • Initialized with curly brackets. d={} is an empty dictionary.
  • Should be used when you need to look up data quickly by a name.
  • Arch example: a phone book.
    You know the name of a person (key), and want the phone number (data).
  • Frequent use: want to make a collection of variables or parameters used in a model.
  • Keys: All immutable objects are valid keys (eg. str or int).
  • Values: Fully unrestricted.
[82]
x = {'abc': 1.2, 'D': 1, 'ef': 2.74, 'G': 30} # Create a dictionary
print("x['ef'] =", x['ef']) # Extracting content
x['abc'] = 100 # Changing content
x['ef'] = 2.74

Elements of a dictionary are extracted using their keyword. Can be a variable

[ ]
key = 'abc'
value = x[key]
print(value)

Content is deleted using its key:

[ ]
print(y)
del y['abc']
print(y)

Task: Create a dictionary called capitals with the capital names of Denmark, Sweden and Norway as values and country names as keys.

Answer:

[ ]
capitals = {}
capitals['denmark'] = 'copenhagen'
capitals['sweden'] = 'stockholm'
capitals['norway'] = 'oslo'

capital_of_sweden = capitals['sweden']
print(capital_of_sweden)

3.4 Summary

The new central concepts are:

  1. Containers (lists, tuples, dictionaries)
  2. Mutable/immutable
  3. Slicing of lists and tuples
  4. Referencing (copy and deepcopy)
  5. Key-value pairs for dictionaries

Note: All atomic types as immutable, and only strings are subscriptable.

[ ]
x = 'abcdef'
print(x[:3])
print(x[3:5])
print(x[5:])
try:
    x[0] = 'f'
except:
    print('strings are immutable')

Advanced: Other interesting containers are e.g. namedtuple and OrderDict (see collections), and sets.

4. Conditionals and loops

4.1 Conditionals

You typically want your program to do one thing if some condition is met, and another thing if another condition is met.

In Python this is done with conditional statments:

[ ]
x = 3
if x < 2: 
    # happens if x is smaller than 2
    print('first possibility')
elif x > 4: # elif = else if
    # happens if x is not smaller than 2 and x is larger than 4
    print('second possibility')
elif x < 0:
    # happens if x is not smaller than 2, x is not larger than 4
    #  and x is smaller than 0
    print('third posibility') # note: this can never happen
else:
    # happens if x is not smaller than 2, x is not larger than 4
    #  and x is not smaller than 0
    print('fourth possiblity')  

Note:

  1. "elif" is short for "else if"
  2. the indentation after if, elif and else is required (typically 4 spaces)

An equivalent formulation of the above if-elif-else statement is:

[ ]
x = -1
cond_1 = x < 2 # a boolean (True or False)
cond_2 = x > 4 # a boolean (True or False)
cond_3 = x < 0 # a boolean (True or False)
if cond_1: 
    print('first possibility')
elif cond_2:
    print('second possibility')
elif cond_3:
    print('third posibility')
else:
    print('fourth possiblity')

y = [1, 2]
if y:
    print('y is not empty')

The above can also be written purely in terms of if-statements:

[ ]
if cond_1: 
    print('first possibility')
if not cond_1 and cond_2:
    print('second possibility')
if not (cond_1 or cond_2) and cond_3:
    print('third posibility')
if not (cond_1 or cond_2 or cond_3):
    print('fourth possiblity')

4.2 Basics of looping

  • We often need to do the same task many times over.
  • We use loops to do that.
  • Code repetition gives you horrible errors and takes time.
  • 2 kinds of loop
    • for loop: when you know how many iterations to do
    • while loop: when the stopping point is unknown
  • Use list comprehension instead of simple for loops.

Never do this:

[93]
xs = [0,1,2,3,4]
x_sqr = []

x_sqr.append(xs[0]**2)
x_sqr.append(xs[1]**2)
x_sqr.append(xs[2]**2)
x_sqr.append(xs[4]**2)
print(x_sqr)
[0, 1, 4, 16]

Use a for loop instead:

[ ]
x_sqr = [] # empty list
for x in xs:
    x_sqr.append(x**2)

Even better: a list comprehension
List comprehension is the shortest syntax and runs faster. Use that if you can.

[ ]
x_sqr = [x**2 for x in xs]

Use a while loop:

[100]
i_sqr = [] # empty list
i = 0
while i < 10:
    i_sqr.append(i**2)
    i += 1
print(i_sqr)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Use a for loop with range instead:

[ ]
y_list = [] # empty list
for x in range(5):
    print(x)
    y_list.append(x**2)
print(y_list)

4.3 More complex loops

Nice to know when looping in python

  • Need a count variable? For loops can be enumerated.
  • Need to loop over 2 lists, element-by-element? Use zipping.
  • You can nest loops.
[103]
y_list = []
x_list = [10,25,31,40]

for i,x in enumerate(x_list):
    print('i =', i)
    y_list.append(x*i)
print('y_list =',y_list)
i = 0 i = 1 i = 2 i = 3 y_list = [0, 25, 62, 120]

Loops can be fine-tuned with continue and break.

[ ]
y_list = []
x_list = [*range(10)]

for i,x in enumerate(x_list):
    if i == 1:
        continue # go to next iteration 
    elif i == 4:
        break # stop loop prematurely
    y_list.append(x**2)
print(y_list)

Task: Create a list with the 10 first positive uneven numbers.

[ ]
# write your code here

Answer:

[ ]
my_list = []
for i in range(10):
    my_list.append((i+1)*2-1)
print(my_list)

Zip: We can loop over 2 lists at the same time:

[108]
x = ['I', 'II', 'III']
y = ['a', 'b', 'c']

for i,j in zip(x,y):
    print(i+j)
Ia IIb IIIc

Zipping is not the same as nesting

[109]
# A nested loop
for i in x:
    for j in y:
        print(i+j)
Ia Ib Ic IIa IIb IIc IIIa IIIb IIIc

Iter(ation)tools enable us do complicated loops in a smart way. We can e.g. loop through all combinations of elements in 2 lists:

[ ]
import itertools as it
for i,j in it.product(x,y):
    print(i,j)

4.4 Dictionaries

We can loop throug keys, values or key-value pairs of a dictionary.

[ ]
my_dict = {'a': '-', 'b': '--', 'c': '---'}
for key in my_dict.keys():
    print(key)
[ ]
for val in my_dict.values():
    print(val)
[ ]
for key,val in my_dict.items():
    print(key,val)

We can also check whether a key exists:

[ ]
if 'a' in my_dict:
    print('a is in my_dict with the value ' + my_dict['a'])
else:
    print('a is not in my_dict')
[ ]
if 'd' in my_dict:
    print('d is in my_dict with the value ' + my_dict['d'])
else:
    print('d is not in my_dict')

Note: dictionaries can do this operation very quickly without looping through all elements. So use a dictionary when lookups are relevant.

4.5 Summary

The new central concepts are:

  1. Conditionals (if, elif, else)
  2. Loops (for, while, range, enumerate, continue, break, zip)
  3. List comprehensions
  4. Itertools (product)

5. Functions

The most simple function takes one argument and returns one output:

[ ]
def f(x):
    return x**2

print(f(2))

Note: The identation after def is again required (typically 4 spaces).

Alternatively, you can use a single-line lambda formulation:

[ ]
g = lambda x: x**2 
print(g(2))

Introducing multiple arguments are straigtforward:

[113]
def f(x,y):
    return x**2 + y**2

print(f(2,2))
8

Multiple outputs gives tuples

[115]
def f(x,y):
    z = x**2
    q = y**2
    return z,q

full_output = f(2,2) # returns a tuple
print('full_output =', full_output)
print('full_output is a',type(full_output))
full_output = (4, 4) full_output is a <class 'tuple'>

The output tuple can be unpacked:

[117]
z,q = full_output # unpacking
print('z =',z,'q =',q)
z = 4 q = 4

5.1 No outputs...

Functions without any output can be useful when arguments are mutable:

[ ]
def f(x): # assume x is a list
    new_element = x[-1]+1
    x.append(new_element) 
    
x = [1,2,3] # original list
f(x) # update list (appending the element 4)
f(x) # update list (appending the element 5)
f(x)
print(x)

Note: this is called a side-effect, which is often best avoided.

5.2 Keyword arguments

We can also have keyword arguments with default values (instead of positionel arguments):

[122]
def f(x,y,a=2,b=2):
    return x**a + y*b

print(f(2,4)) 
print(f(2,4,b=6))
print(f(2,4,a=6,b=3))
12 28 76

Note: Keyword arguments must come after positional arguments.

Advanced: We can also use undefined keyword arguments:

[ ]
def f(**kwargs):
    # kwargs (= "keyword arguments") is a dictionary
    for key,value in kwargs.items():
        print(key,value)
f(a='abc',b='2',c=[1,2,3])

and these keywords can come from unpacking a dictionary:

[ ]
my_dict = {'a': 'abc', 'b': '2', 'c': [1,2,3]}
f(**my_dict)

5.3 A function is an object

A function is an object and can be given to another functions as an argument.

[ ]
def f(x):
    return x**2

def g(x,h):
    temp = h(x) # call function h with argument x
    return temp+1

print(g(2,f))

5.4 Scope

Important: Variables in functions can be either local or global in scope.

  • Global is the main body of your python code.
  • Local is inside the belly of a function.
  • Never use global variables inside your function's belly.
[131]
a = 2 # a global variable

def f(x):
    return x**a # a is global. This is BAD

def g(x,a=4):
    # a's default value is fixed when the function is defined
    return x**a 

def h(x):
    a = 4 # a is local
    return x**a

print(f(2), g(2), h(2))
print('incrementing the global variable:')
a += 1 
print(f(2), g(2), h(2)) # output is only changed for f
4 16 16 incrementing the global variable: 8 16 16

5.5 Summary

Functions:

  1. are objects
  2. can have multiple (or no) arguments and outputs
  3. can have positional and keyword arguments
  4. can use local or global variables (scope)

Task: Create a function returning a person's full name from her first name and family name with middle name as an optional keyword argument with empty as a default.

[ ]
# write your code here

Answer:

[ ]
def full_name(first_name,family_name,middle_name=''):
    name = first_name
    if middle_name != '':
        name += ' '
        name += middle_name
    name += ' '
    name += family_name
    return name
    
print(full_name('Jeppe','Druedahl','"Economist"'))

Alternative answer (more advanced, using a built-in list function):

[ ]
def full_name(first_name,family_name,middle_name=''):
    name = [first_name]
    
    if middle_name != '':
        name.append(middle_name)
        
    name.append(family_name)
    return ' '.join(name)

print(full_name('Jeppe','Druedahl','"Economist"'))

6. Floating point numbers

On a computer the real line is approximated with numbers on the form:

number=significand×baseexponent\text{number} = \text{significand} \times \text{base}^{exponent}

  • significand: 1 bit, positive or negative
  • base: 52 bits
  • exponent: 11 bits

Obviously, this is a finite approximation.
All numbers can therefore not be represented exactly. A close neighboring number is thus used.

[123]
x = 0.1
print(f'{x:.100f}') # printing x with 100 decimals
x = 17.2
print(f'{x:.100f}') # printing x with 100 decimals
0.1000000000000000055511151231257827021181583404541015625000000000000000000000000000000000000000000000 17.1999999999999992894572642398998141288757324218750000000000000000000000000000000000000000000000000000

Simple sums might, consequently, not be exactly what you expect.

[28]
d = 0.0
for i in range(10):
    d += 0.1
print(d)
0.9999999999999999

And just as surprising:

[ ]
print(0.1 == 0.10000000000000001)

Comparisions of floating point numbers is therefore always problematic.
We know that

acbc=ab\frac{a \cdot c}{b \cdot c} = \frac{a}{b}

but:

[125]
a = 100
b = 0.3
c = 10
equality = ((a*c)/(b*c) == a/b)
print('Does equality hold?', equality)
Does equality hold? False

However, rounding off the numbers to a close neighbor may help:

[37]
test = round((a*c)/(b*c), 10) == round(a/b, 10)
print(test)
True

You may also use the np.isclose function to test if 2 floats are numerically very close, i.e. practically the same:

[126]
import numpy as np
print(np.isclose((a*c)/(b*c), a/b))
True

Underflow: Multiplying many small numbers can result in an exact zero:

[128]
x = 1e-60
y = 1
for _ in range(6):
    y *= x
    print(y)
1e-60 1e-120 1e-180 1e-240 9.999999999999999e-301 0.0

Overflow: If intermediate results are too large to be represented, the final result may be wrong or not possible to calculate:

[129]
x = 1.0
y = 2.7
for i in range(200):    
    x *= (i+1)
    y *= (i+1) 
print(y/x) # should be 2.7
print(x,y)
nan inf inf

Note: nan is not-a-number. inf is infinite.

Note: Order of additions matter, but not by that much:

[ ]
sum1 = 10001234.0 + 0.12012 + 0.12312 + 1e-5
sum2 = 1e-5 + 0.12312 + 0.12012 + 10001234.0
print(sum1-sum2)

6.1 Summary

The take-aways are:

  1. Decimal numbers are approximate on a computer!
  2. Never compare floats with equality (only use strict inequalities)
  3. Underflow and overflow can create problem (not very important in practice)

For further details see here.

Videos:

7. Classes (user-defined types)

Advanced: New types of objects can be defined using classes.

[ ]
class human():
    
    def __init__(self,name,height,weight): # called when created
        
        # save the inputs as attributes
        self.name = name # an attribute
        self.height = height # an attribute
        self.weight = weight # an attribute
    
    def bmi(self): # a method
        
        bmi = self.weight/(self.height/100)**2 # calculate bmi
        return bmi # output bmi
    
    def print_bmi(self):
        print(self.bmi())

A class is used as follows:

[ ]
# a. create an instance of the human object called "jeppe"        
jeppe = human('jeppe',182,80) # height=182, weight=80
print(type(jeppe))

# b. print an attribute
print(jeppe.height)

# c. print the result of calling a method
print(jeppe.bmi())

Methods are like functions, but can automatically use all the attributes of the class (saved in self.) without getting them as arguments.

Attributes can be changed and extracted with .-notation

[ ]
jeppe.height = 160
print(jeppe.height)
print(jeppe.bmi())

Or with setattr- and getatrr-notation

[ ]
setattr(jeppe,'height',182) # jeppe.height = 182
height = getattr(jeppe,'height') # height = jeppe.height
print(height)
print(jeppe.bmi())

7.1 Operator methods

If the appropriate methods are defined, standard operators, e.g. +, and general functions such as print can be used.

Define a new type of object called a fraction:

[ ]
class fraction:
    
    def __init__(self,numerator,denominator): # called when created
        self.num = numerator
        self.denom = denominator
    
    def __str__(self): # called when using print
        
        return f'{self.num}/{self.denom}' # string = self.nom/self.denom
    
    def __add__(self,other): # called when using +
        
        new_num = self.num*other.denom + other.num*self.denom
        new_denom = self.denom*other.denom
        
        return fraction(new_num,new_denom)

Note: We use that

ab+cd=ad+cbbd\frac{a}{b}+\frac{c}{d}=\frac{a \cdot d+c \cdot b}{b \cdot d}

We can now add fractions:

[ ]
x = fraction(1,3)
print(x)
[ ]
x = fraction(1,3) # 1/3 = 5/15
y = fraction(2,5) # 2/5 = 6/15
z = x+y # 5/15 + 6/15 = 11/15
print(z,type(z))

Equivalent to:

[ ]
z_alt = x.__add__(y)
print(z,type(z))

But we cannot multiply fractions (yet):

[ ]
try:
    z = x*y
    print(z)
except:
    print('multiplication is not defined for the fraction type')

Extra task: Implement multiplication for fractions.

7.2 Summary

The take-aways are:

  1. A class is a user-defined type
  2. Attributes are like variables encapsulated in the class
  3. Methods are like functions encapsulated in the class
  4. Operators are fundamentally defined in terms of methods

8. Summary

This lecture: We have talked about: 1. Types (int, str, float, bool, list, tuple, dict) 2. Operators (+, , /, +=, =, /=, ==, !=, <) 3. Referencing (=) vs. copying (copy, deepcopy) 4. Conditionals (if-elif-else) and loops (for, while, range, enumerate, zip, product) 5. Functions (positional and keyword arguments) and scope 6. Floating points 7. Classes (attributes, methods)

You work: When you are done with the DataCamp courses read through this notebook, play around with the code and ask questions if there is stuff you don't understand.

Next lecture: We will solve the consumer problem from microeconomics numerically.

Your to-do list: You should be running JupyterLab on your own computer.

9. Extra: Iterators

Consider the following loop, where my_list is said to be iterable.

[ ]
my_list = [0,2,4,6,8]
for i in my_list:
    print(i)

Consider the same loop generated with an iterator.

[ ]
for i in range(0,10,2):
    print(i)

This can also be written as:

[ ]
x = iter(range(0,10,2))
print(x)
print(next(x))
print(next(x))
print(next(x))

The main benefit here is that the, potentially long, my_list, is never created.

We can also write our own iterator class:

[ ]
class range_two_step:
    
    def __init__(self, N):
        self.i = 0
        self.N = N
        
    def __iter__(self):
        return self
    
    def __next__(self):
        
        if self.i >= self.N:
            raise StopIteration
        
        temp = self.i
        self.i = self.i + 2
        return temp 

Can then be used as follows:

[ ]
x = iter(range_two_step(10))
print(next(x))
print(next(x))
print(next(x))

Or in a loop:

[ ]
for i in range_two_step(10):
    print(i)

10. Extra: More on functions

We can have an undefined number of input arguments:

[ ]
def f(*args):
    out = 0
    for x in args:
        out += x**2
    return out
print(f(2,2))
print(f(2,2,2,2))

We can have recursive functions to calculate the Fibonacci sequence:

F0=0F1=1Fn=Fn1+Fn2\begin{aligned} F_0 &= 0 \\ F_1 &= 1 \\ F_n &= F_{n-1} + F_{n-2} \\ \end{aligned}
[ ]
def fibonacci(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)
    
y = fibonacci(7)
print(y)