Fundamentals (Nice)

Warning

The material in this ‘Nice’ chapter is optional. The discussion typically deals with content beyond what a novice should know. So, please finish all the ‘Need’ and ‘Good’ portions before you go through this chapter.

1 If if is not enough

Sometimes you have multiple conditions you want to check. For such situations, the if-elif-else statements can be cumbersome. Since Python 3.10, you can use a match-case statement that goes like this:

name = 'Batman'

match name:
    case 'Batman':
        print('Hello Hero | Batman!')
    case 'Robin':
        print('Hello Sidekick | Robin!')
    case _:
        print('Hello World!')

2 Ternary operators or Conditional Statements

Python offers ternary operators (containing three parts) that can be useful to make your code more readable and less verbose.

  1. So, instead of

    if nationality == 'French':
        greeting = "Bonjour!"
    else:
        greeting = "Hello!"

    we can write

    greeting = "Bonjour!" if nationality == 'French' else "Hello!"

    This works too!

    ("Hello!", "Bonjour!")[nationality == 'French']
  2. Here is another ternary operator.

    text = None
    message = text or "No message!"

3 Swapping values

If you want to swap the content of two variables, you can do:

a, b = 1, 2
a, b = b, a
print(a, b)

This only works with Python!

4 There are more types

We discussed earlier how we should never check the equality of two floats because it will be influenced by rounding off errors. What if you want to do some absurdly precise calculations? One way around this is to use higher precision types like np.float32 or np.float64. To demonstrate what these offer let me use np.finfo() to query the limits applicable to floating point operations. The following gives the difference between 1.0 and the next biggest nearest float for the various types:

my_types = [
    float,       # Default for core Python on my machine
    np.float16,
    np.float32,
    np.float64,
    np.float128
]

for my_type in my_types:
    print(f'{my_type.__name__:<15s}:', np.finfo(my_type).eps)
float          : 2.220446049250313e-16
float16        : 0.000977
float32        : 1.1920929e-07
float64        : 2.220446049250313e-16
longdouble     : 1.084202172485504434e-19

5 Operator precedance

There is a ‘pecking order’ among the various operators in Python. This idea is called operator precedence. Below is a quick summary of how this is set up in Python.

  1. Highest precedence at the top, lowest at the bottom.
  2. Operators in the same box evaluate left to right. (Reproduced from this website)
Description Operator
Parentheses(grouping) ()
Function call f(args...)
Slicing x[index:index]
Subscription x[index]
Attribute reference x.attribute
Exponentiation **
Bitwise not ~x
Positive, negative +x, -x
Multiplication, division, remainder *, / ,%
Addition, subtraction +, -
Bitwise shifts <<, >>
Bitwise AND &
Bitwise XOR ^
Bitwise OR \|
Comparisons, membership, identity in, not in, is, is not, <, <=, >, >=, <>, !=, ==
Boolean NOT not x
Boolean AND and
Boolean OR or
Lambda expression lambda

6 Variables in Python are just names

6.1 The Problem

Since we use variables all the time, it is good to understand how they work. This is particularly true for Python because certain Python variables can be sneaky!

What do you think will be printed if you run the following code? Try to predict the answers before running the code.

x = [1, 2]
y = x
y.append(3)

print(f"x: {x}, y: {y}")

6.2 An explanation

Let’s explore the root of this by first running the following code:

'''CODE 1'''
'CODE 1'
x = 1
y = 1

print(f"x: {id(x)}, y: {id(y)}, 1: {id(1)}")
x: 140280593755848, y: 140280593755848, 1: 140280593755848

The above code tells us that x, y both have the same id as 1! The following figure tries to explain what is happening.

  1. Before the code is run, Python has things or objects 1, 2 and a that have three properties type, value and id. For example, 1 can have the value 1, type int, and some id. a can have the value ‘a’, type str and some id.

  2. After the code is run, x and y are ‘looking at’ or bound to 1. So x and y are referred to as names that are bound to 11.

Now run this code:

'''CODE 2'''
'CODE 2'
x = 1
y = x + 1

print(f"x: {id(x)}, y: {id(y)}")
x: 140280593755848, y: 140280593755880
print(f"1: {id(1)}, 2: {id(2)}")
1: 140280593755848, 2: 140280593755880

Since the mathematical operation requires y to have the value 2, y now gets bound to object 2. This happens because the value of object 1 cannot be changed, so the binding is changed instead.

Objects such as 1 whose values cannot be changed are called immutable. Other such immutable types are str(i.e., letters), float, bool.

There are also objects whose values can be changed. These types are called mutable and include lists and dictionaries and instances of classes. These behave differently, as highlighted in the problem above.

Here is the code from the ‘problem’ with some explanations.

# x is bound to a list object with a value [1 ,2]
x = [1, 2]

# y is bound to the SAME list object with a value [1 ,2]
y = x

# y is used to change the value of the object from  [1, 2] to [1, 2, 3]
y.append(3)

6.3 A solution

If you really want y to have an independent copy of x, you should use:

y = x.copy()
Mutable vs. immutable types


Be very careful when you use mutable data types as variables.

7 == is not the same as is

Python has several ways of comparing ‘items’. == and is are two examples. Here is the difference between them:

x is y checks for identity. i. e., it asks if x and y are bound to the same object by comparing the ID.

x == y checks for equality by running a function that checks for equality (such as _eq_ of a class). You will understand more of this as we develop the idea of classes in later chapters.

Back to top

Footnotes

  1. This is the basis of the, sometimes contentious, statement that there are no variables in Python↩︎