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:
1. Your first notebook session
Optimally: You have this notebook open as well on your own computer.
1.# 1.# 1. Download course material
- Follow the installation guide
- If you have not yet restarted JupyterLab after installations, do it now.
- Go to the Extensions tab (lowest icon in the far left side). Press Warning. Agree to enable extensions.
- Make sure you are in the folder, where the course content should be located.
- Press the tab Git.
- Press
Clone a repository
- Paste in
https://github.com/NumEconCopenhagen/lectures-2022
and enter. - You now have all course material in the folder
lectures-2022
- Do the same thing for exercise class material using the url:
https://github.com/NumEconCopenhagen/exercises-2022
- 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
orexercises-2022
, or your own repo).- On Windows write
cd
. - On Mac write
pwd
. - This will display your current location.
- On Windows write
- 5: See if YOU have any changes
- Write
git status
. - Note if it says
modified: 02/Primitives.ipynb
, or show modifications to other files.
- Write
- 6: View incoming changes
- Write
git fetch
- Write
git diff --name-status main..origin/main
- Write
- 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.
- Were any of the files listed in Step 6 also found on the list produced in Step 5? Eg.
- 8: Accept incoming changes
- Write
git merge
- Write
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.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,
There is no cap on the size of ints in python!
# 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.
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.
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.
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
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:
- Integers, int
- Floating point numbers, float
- Strings, str
- 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:
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!
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. +=, -=, *=, /=)
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:
z = x < y # z is now a boolean variable
print(z)
False
2.8 Summary
The new central concepts are:
- Variable
- Reference
- Object
- Type (int, float, str, bool)
- Value
- Operator (+, -, *, **, /, //, % etc.)
- Augmentation (+=, -=, *=, /= etc.)
- Comparison (==, !=, <, <= etc.)
3.1 Lists
A first example is a list. A list contains elements each referencing some data in memory.
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.
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.
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 elementx[i]
and create a list of (up to)n
elements.- Sort of nice if you have calculated
i
and know you needn
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:
x = [0,1,2,3,4,5]
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.
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.
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.
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
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
orint
). - Values: Fully unrestricted.
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:
- Containers (lists, tuples, dictionaries)
- Mutable/immutable
- Slicing of lists and tuples
- Referencing (copy and deepcopy)
- 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.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:
- "elif" is short for "else if"
- 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 dowhile
loop: when the stopping point is unknown
- Use list comprehension instead of simple for loops.
Never do this:
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:
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.
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:
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
# 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:
- Conditionals (if, elif, else)
- Loops (for, while, range, enumerate, continue, break, zip)
- List comprehensions
- Itertools (product)
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:
def f(x,y):
return x**2 + y**2
print(f(2,2))
8
Multiple outputs gives tuples
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:
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):
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.
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:
- are objects
- can have multiple (or no) arguments and outputs
- can have positional and keyword arguments
- 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:
- 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.
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.
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
but:
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:
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:
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:
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:
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:
- Decimal numbers are approximate on a computer!
- Never compare floats with equality (only use strict inequalities)
- Underflow and overflow can create problem (not very important in practice)
For further details see here.
Videos:
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
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:
- A class is a user-defined type
- Attributes are like variables encapsulated in the class
- Methods are like functions encapsulated in the class
- Operators are fundamentally defined in terms of methods
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.
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)
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:
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)