2.8. Statements and Packaging#

A statement is an instruction that your program can execute. In Python, you can compute, bind, or save values in different ways.

Some of the most common ways are:

  1. Statements, for combining, printing or calculating values.

  2. Assignments, for binding values to variables (names).

  3. Lists and generators to store values.

  4. Compound statements, control structures for running code conditionally or repeatedly.

  5. Packaging, for reusing chunks of code.

2.8.1. 1. Simple statement#

# statement 1
print(4)

# statement 2
print((5 + 6) / 2)

# statement 3
print('glucose')

# statement 4
2 == 2
4
5.5
glucose
True

Fun fact: Python does not have a rule to add three numbers together, but complex problems through binary operations (operations involving two statements).

(5 + 6 - 3) / 2
4.0
# How Python solves : (5 + 6 - 3) / 2
x0 = 5 + 6
x1 = x0 - 3
x2 = x1 / 2
x2
4.0

2.8.2. 2. Assignment statement#

Assigning values to variables allows reusing a variable. We use = to bind a value to a variable. Assigning a value to a variable is useful to write more clear and concise code since we can now use our variable everywhere the value (or simple statement) would go.

Before defining variables, the environment (or namespace) is empty. You can use %who to check the status of your environment.

# assignment 1
au = 'Gold'

# assignment 2
total = (2 + 5 - 6) / 5

# assignment 3
elements = ['C', 'N', 'O']
elements[1]
'N'
total
0.2
%whos
Variable   Type     Data/Info
-----------------------------
au         str      Gold
elements   list     n=3
total      float    0.2
x0         int      11
x1         int      8
x2         float    4.0

But be careful, variables \(\neq\) values!

Data objects (values) exist in memory, while variables are a reference to the value, or best a reference to the place in memory where the object is stored. This also means that we can have more than one variable referencing the same value.

# Pay attention to variables
# In the following example, temperatures and grams have the same values at the beginning, 
# so we assume they will be the same in the future as well
temperatures = [20, 25]
grams = [20, 25]
temperatures = grams

# The setting of your experiment changes, and you have to use a higher quantity of grams
grams.append(100) # append is used to add items to a list

# Note that now we have a big problem in our temperatures list!
print(temperatures)
[20, 25, 100]

The built-in function id can be helpful to check if two variables reference exactly the same value

id(temperatures) == id(grams)
True
# Use ? to get documentation of any function 
id?

Be careful, variables can be overwritten!

# Define temperatures and grams
temperatures = [20, 25]
grams = [20, 25, 100]

# Print values in temperatures and grams
print(temperatures)
print(grams)
[20, 25]
[20, 25, 100]

As you can see, now temperatures contains different values than in cell 7.

2.8.3. 3. Lists and generators#

We will deep dive into lists and other types of data structures next week, for now, the only notion that you need to acquire is that items can be assigned to lists in multiple ways, e.g., manually (which we want to avoid since programming is all about automation), using a loop or through a list comprehension.

In the following examples, we will use different techniques to create list_gallons.

# To avoid: create a list manually

# create original list - we will reuse this in the next cells
list_liters = [6, 9, 15.7]

# create list_gallons manually
list_gallons = [1.585, 2.3775, 4.1475]

You can index items in a list in this way - and remember, you start counting from 0 in python, so index 0 is the first item!

print(f"The first item in the list is {list_gallons[0]}")
print(f"The second item in the list is {list_gallons[1]}")
print(f"...or {list_gallons[-2]}")
print(f"The last item in the list is {list_gallons[-1]}")
The first item in the list is 1.585
The second item in the list is 2.3775
...or 2.3775
The last item in the list is 4.1475
# Creating a list in a loop

# create empty list_gallons
list_gallons = []
# loop through the elements of the original list
for item in list_liters:
    # convert given value from liters to gallons
    gallons = round(item * 0.2641720524, 4)
    # assign resulting values in gallons to list_gallons
    list_gallons.append(gallons)
print("The list contains the following elements: ", list_gallons)
The list contains the following elements:  [1.585, 2.3775, 4.1475]

Another way to create a list is through list comprehension. This means that we will not use the traditional for loop syntax, but condense it into only one line.

# Creating the list through a list comprehension

# use a list comprehension to create list_gallons
list_gallons = [round(item * 0.2641720524, 4) for item in list_liters]
list_gallons
[1.585, 2.3775, 4.1475]

Generally, list comprehensions are faster than for loops, so keep this in mind if speed is a concern when writing your program. Read this tutorial if you want to know more about for loops and list comprehensions, and when to use them.

Extra: List comprehensions vs generator expressions

Note that you can also decide to create a generator instead of a list. The disadvantage of this approach is that you can only use a generator created in this way once.

You can read more about the differences between list comprehensions and generator expressions here.

# Create a generator

# create a generator instead of a list by substituting brackets with parenthesis
generator_gallons = (round(item * 0.2641720524, 4) for item in list_liters)
generator_gallons
<generator object <genexpr> at 0x0000024BA52934C0>
# The first time we run this loop the generator is going to return the values
for item in generator_gallons:
    print(item)
1.585
2.3775
4.1475
# the second time it won't
for item in generator_gallons:
    print(item)

You may be wondering, if the generator expression can’t be reused more than once, why would anyone use this instead of a normal list?

The answer is memory and time! Generators don’t construct a list object, so instead of storing the whole list in memory, they generate one item at a time and generate the next only when in demand. On the other hand, in a list comprehension, memory is reserved for the whole list. Additionally, generators are also more time efficient than list comprehensions.

2.8.4. 4. Compound statement#

Compound statements are more complex statements that allow code in Python to be run according to a logic or order. They allow code to be run conditionally, i.e., expressions are evaluated one by one until one is found to be true; or repeatedly, i.e., code runs until a condition is satisfied (as long as the expression is true or iteration over given data structure is over). The two most widely used compound statements are if statements and loops.

Here, we will go through the basics of compound statements, which will be explored in more detail in the next Notebook 5-Control Flow.

For more resources, check out the documentation here.

2.8.4.1. if statement#

if statement is used for conditional execution.

In plain language, an if-else clause tells Python to run the code in order.

  • if the first condition is met, then do something,

  • if the second (elif) condition is true, then do something else,

  • and so on

  • until reaching (if present) the last clause (else), if everything else before is false, then do this.

In Python syntax:

if <condition>:
 <suite>
elif <condition>:
 <suite>
elif <condition>:
 <suite>
...
else:
 <suite>

Let’s see an actual example of an if statement.

Imagine having a barrel of water and wanting to reach steady state. In this case, the inflow has to be equal to the outflow.

# define list containing the inflow (process[0]) value and the outflow (process[1]) value
process = [10, 10]
# first clause, if this is true...
if process[0] == process[1]:
    # ...then do this
    print('Steady state')
# if the clause before is not true...
else:
    # ...then do this
    print('No steady state')
Steady state

Now try to change one element in the process list so that the first condition is false

# Your code here

2.8.4.2. loops#

There are two types of loops used in Python: for and while loops.

Before looking at the cells below, can you guess the difference between a for and a while loop?

# Write your answer here 
# (either use an # for commenting the line out or transform the cell to Markdown)

2.8.4.2.1. for loop#

Python syntax for the for loop:

for <target_list> in <expression_list>:
 <suite>

Now let’s look at some examples.

# Example of for loop

# define list of experiments
experiments = [
    'Bubble column', 'Fluid bed', 'Crystalization',
    'Ion exchange', 'Solid-liquid extraction',
]

# loop through the list of experiments we want to perform
for experiment in experiments:
    # print the name of the experiments in lowercase
    print(experiment.lower())
bubble column
fluid bed
crystalization
ion exchange
solid-liquid extraction

2.8.4.2.2. while loop#

Python syntax for the while loop:

while <condition>:
 <suite>
# Example of while loop

# loop through the list of experiments as long as 
# the list contains more than 2 elements
while len(experiments) > 2:
    # print the list of experiments
    print('Experiments to perform: ', experiments)
    # remove the last element in the list
    experiments.pop()
# print the modified list
print('Only 2 experiments left: ', experiments)
Experiments to perform:  ['Bubble column', 'Fluid bed', 'Crystalization', 'Ion exchange', 'Solid-liquid extraction']
Experiments to perform:  ['Bubble column', 'Fluid bed', 'Crystalization', 'Ion exchange']
Experiments to perform:  ['Bubble column', 'Fluid bed', 'Crystalization']
Only 2 experiments left:  ['Bubble column', 'Fluid bed']

In summary:

  • for loops are used to iterate over elements of a sequence.

  • while loops are used for repeated execution as long as the condition is true

2.8.5. 5. Packaging#

“Packaging” your code into functions will enable you to organize and reuse code, and it makes it easy to maintain and update.

A function is defined using the syntax below. It needs to be preceded by ‘def’, it needs to be assigned a name (e.g., bar_to_pascal) and it allows you to provide some arguments (bar). “Calling the function” executes the wrapped code using the input arguments.

2.8.5.1. Functions#

# Define a function that can be reused in the future

# use the comand 'def' to define the function
# bar_to_pascal is the name that you assign to the function
# bar is the argument that you pass to your function
def bar_to_pascal(bar):
    one_bar_in_pascal = 100000
    return bar * one_bar_in_pascal

# call the function
bar_to_pascal(0.025)
2500.0

Note: variables defined inside a function cannot be used outside.

one_bar_in_pascal
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[25], line 1
----> 1 one_bar_in_pascal

NameError: name 'one_bar_in_pascal' is not defined

Why is it useful to create functions?

Functions allow you to reuse code. This means that your code will probably be:

  • Cleaner

  • Shorter (since you don’t have to specify long operations multiple times)

  • More robust (copying and pasting lines of code multiple times could introduce some errors, for example not changing one of the variables correctly.

Let’s see some examples.

The function that we defined above is quite simple and short, which means that we don’t save too many lines of code if we wanted to call it multiple times.

print(bar_to_pascal(0.25))
print(bar_to_pascal(0.05))
print(bar_to_pascal(0.02))
print(bar_to_pascal(1))

Compared to…

print(0.25 * 100000)
print(0.05 * 100000)
print(0.02 * 100000)
print(1 * 100000)

The result is the same, but you can maybe already notice that, without any context or comments, it would be very difficult to understand what operation you are trying to perform.

But what if your function was much longer?

Let’s compute the rate constant k. The initial concentration of A and B is: \(20.18 mol/m^3\). The gas can be considered ideal. The gas constant is \(R = 8.3144 Pa\cdot m^3/(mol\cdot K)\)

# define the function and arguments to be provided
def rate_constant(C0, C_ratio, t):
    # constant k in hours
    k_h = (1/(C0*t))*(1/C_ratio-1)
    # transform it in seconds
    k_s = k_h/(60*60)
    return k_s

t = 1 # time in h
CA_CA0 = 0.15 # ratio of CA/CA0
C0 = 20.18 # mol/m^3

# call the function and print the rate constant found
k = rate_constant(C0, CA_CA0, t)
print(f'The constant rate k is: {k} m^3/mol*s')

What if we now need to calculate the rate constant for an initial concentration of \(19.18 mol/m^3\), with a concentration of CA/CA0 of 0.17 in 2 hours? Than we would have to copy all the code and change the initial concentration variable!

It would look like this:

# Initial concentration of 20.18 mol/m^3

t = 1 # time in h
CA_CA0 = 0.15 # ratio of CA/CA0
C0 = 20.18 # mol/m^3
k_h = (1/(C0*t))*(1/CA_CA0-1) # constant k in hours
k_s = k_h/(60*60) # transform it in seconds
print(f'The constant rate k is: {k_s} m^3/mol*s')


# Initial concentration of 19.18 mol/m^3

t = 2 # time in h
CA_CA0 = 0.17 # ratio of CA/CA0
C0 = 19.18 # mol/m^3
k_h = (1/(C0*t))*(1/CA_CA0-1) # constant k in hours
k_s = k_h/(60*60) # transform it in seconds
print(f'The constant rate k is: {k_s} m^3/mol*s')

If we define a function, we can just do this:

k = rate_constant(19.18, 0.17, 2)
print(f'The constant rate k is: {k} m^3/mol*s')