Skip to article frontmatterSkip to article content

Abstract

This notebook explains how to read and write basic Python expressions for arithmetic operations. Much like mathematical expressions, the precise meanings of Python expressions must consider the precedence and associativity of the operators.

from __init__ import install_dependencies

await install_dependencies()
import ast  # for AST
import sys
from dis import dis
from ipywidgets import interact, fixed

%load_ext divewidgets
%load_ext jupyter_ai
%ai update chatgpt dive:chat

Floating Point Numbers

Not all numbers are integers. In Enginneering, we often need to use fractions.

How to enter fractions in a program?

x = -0.1  # decimal number
y = -1.0e-1  # scientific notation
z = -1 / 10  # fraction
x, y, z, type(x), type(y), type(z)

What is the type float?

Numbers of type float are stored in memory using the floating point representation, which is similar to scientific notation:

sign1.0mantissae1exponent=1×101\overbrace{-}^{\text{sign}} \underbrace{1.0}_{\text{mantissa}} e \overbrace{-1}^{\text{exponent}} = -1 \times 10^{-1}

Integers in mathematics may be regarded as a float instead of int:

type(1.0), type(1e2)

You can also convert an int or a str to a float.

float(1), float("1")

Is it better to store an integer as float?

Python stores a floating point with finite precision, usually 64-bit/double precision:

sys.float_info

It cannot accurately represent a number larger than the max:

sys.float_info.max * 2

The precision also affects the check for equality.

(
    1.0 == 1.0 + sys.float_info.epsilon * 0.5,  # returns true if equal
    1.0 == 1.0 + sys.float_info.epsilon * 0.6,
    sys.float_info.max + 1 == sys.float_info.max,
)

Another issue with float is that it may show more decimal places than desired.

1 / 3

How to round a floating point number to the desired number of decimal places?

round(2.665, 2), round(2.675, 2)
Solution to Exercise 1 #

The value stored is 2.6749999523162841796875, which explains why it is rounded to 2.67 instead of 2.68.

The round function can also be applied to an integer.

round(150, -2), round(250, -2)

Why 250 rounds to 200 instead of 300?

  • Python 3 implements the default rounding method in IEEE 754.
%%ai chatgpt -f text
Explain in one paragraph how arbitrary precision arithmetic works in a computer 
despite the memory being finite. If we can have arbitrary precision arithmetic, 
why do we need `float`?

Operators

The followings are common operators you can use to form an expression in Python:

OperatorOperationExample
unary -Negation-y
+Additionx + y
-Subtractionx - y
*Multiplicationx*y
/Divisionx/y
  • x and y in the examples are called the left and right operands respectively.
  • The first operator is a unary operator, which operates on just one operand.[1]
  • All other operators are binary operators, which operate on two operands.

Python also supports other operations such as

OperatorOperationExample
//Integer divisionx//y
%Modulox%y
**Exponentiationx**y

The following demonstrates the operations of binary operators:

binary_operators = {
    "+": " + ",
    "-": " - ",
    "*": "*",
    "/": "/",
    "//": "//",
    "%": "%",
    "**": "**",
}


@interact(operand1=r"10", operator=binary_operators, operand2=r"3")
def binary_operation(operand1, operator, operand2):
    expression = f"{operand1}{operator}{operand2}"
    value = eval(expression)
    print(
        f"""{'Expression:':>11} {expression}\n{'Value:':>11} {value}\n{'Type:':>11} {type(value)}"""
    )
Solution to Exercise 2
  • / is the usual division, and so 10/3 returns the floating-point number 3.3˙3.\dot{3}.
  • // is integer division, and so 10//3 gives the integer quotient 3.
Solution to Exercise 3 #
  • The first expression means concatenating 'abc' three times.
  • The second means concatenating 'a' ten times.

Operator Precedence and Associativity

An expression can consist of a sequence of operations performed in a row. For instance, does x + y * z means

  1. (x + y) * z or
  2. x + (y * z)?

In other words, should the operand y be associated with + or *. In Mathematics, multiplication has higher priority, and so y should be associated with *. This rule also applies to the Python expression. The association can be seen explicitly from the Abstract Syntax Tree (AST):

print(ast.dump(ast.parse("x + y*z", mode='eval'), indent=4))

How to determine the order of operations?

Like arithmetic, an operand is associated with an operator according to the following list of rules in order, and the operator precedence and associativity in Table 1:

  1. Grouping by parentheses: Operator in the inner grouping first.
  2. Operator precedence/priority: Operator of higher precedence first.
  3. Operator associativity:
    • Left associativity: Operators are grouped from left to right.
    • Right associativity: Operator are grouped from right to left.

Table 1:Operator precedence and associativity

OperatorsAssociativity
**right
- (unary)right
*,/,//,%left
+,-left

For instance, x + y - z is equivalent to ((x + y) - z) because both + and - are left associative, and so the operands are grouped with the operators from left to right.

print(ast.dump(ast.parse("x + y - z", mode='eval'), indent=4))

As another example, x ** y ** z is equivalent to (x ** (y ** z)) because ** is right associative, and so the operands are grouped with the operators from right to left.

print(ast.dump(ast.parse("x ** y ** z", mode='eval'), indent=4))
Solution to Exercise 5

The expression evaluates to ((102))×3=0.03(-(10^{-2}))\times 3=-0.03 because the exponentiation operator ** has higher precedence than both the multiplication * and the unary operators -. The reason -2 is evaluated as the exponent before 10 ** ... is because the exponentiation operator ** requires a right operand. This forces the evaluation of -2 before applying the ** operation. Our understanding of how the machine works is often over-simplified for the ease of understanding!

%%ai chatgpt -f text
Explain the value of the expression -10 ** -2*3. 
In particular, why is the second unary operator - evaluated first before ** 
even though ** has a higher precedence?
### BEGIN SOLUTION
-10 ** 2*3  # can use code-prettify or formatter to fix incorrect styles
### END SOLUTION

Augmented Assignment Operators

For convenience, Python defines the augmented assignment operators such as +=, where

x += 1 means x = x + 1.

The following widgets demonstrate other augmented assignment operators.

@interact(
    initial_value=fixed(r"10"),
    operator=["+=", "-=", "*=", "/=", "//=", "%=", "**="],
    operand=fixed(r"2"),
)
def binary_operation(initial_value, operator, operand):
    assignment = f"x = {initial_value}\nx {operator} {operand}"
    _locals = {}
    exec(assignment, None, _locals)
    print(f"Assignments:\n{assignment:>10}\nx: {_locals['x']} ({type(_locals['x'])})")
y = 3*(x := 15)
x, y
%%optlite -l -h 400
3*(x = 15)
Solution to Exercise 7

Assignment operators are used in assignment statements, which are not expressions because they cannot be evaluated.

String Formatting

Can we round a float or int for printing but not calculation?

This is possible with format strings:

x = 10000 / 3
print("x ≈ {:.2f} (rounded to 2 decimal places)".format(x))
x
  • {:.2f} is a replacement field or place holder
  • that gets replaced by a string
  • that represents the argument x of format
  • according to the format specification .2f, i.e.,
    a decimal floating point number rounded to 2 decimal places.
@interact(
    x="10000/3",
    align={"None": "", "<": "<", ">": ">", "=": "=", "^": "^"},
    sign={"None": "", "+": "+", "-": "-", "SPACE": " "},
    width=(0, 20),
    grouping={"None": "", "_": "_", ",": ","},
    precision=(0, 20),
)
def print_float(x, sign, align, grouping, width=0, precision=2):
    format_spec = (
        f"{{:{align}{sign}{'' if width==0 else width}{grouping}.{precision}f}}"
    )
    print("Format spec:", format_spec)
    print("x ≈", format_spec.format(eval(x)))
print("{:,.2f}".format(10000 / 3))

String formatting is useful for different data types other than float.
E.g., consider the following program that prints a time specified by some variables.

# Some specified time
hour = 12
minute = 34
second = 56

print("The time is " + str(hour) + ":" + str(minute) + ":" + str(second) + ".")

Imagine you have to show also the date in different formats.
The code can become very hard to read/write because

  • the message is a concatenation of multiple strings and
  • the integer variables need to be converted to strings.

Omitting + leads to syntax error. Removing str as follows also does not give the desired format.

print("The time is ", hour, ":", minute, ":", second, ".")  # note the extra spaces

To make the code more readable, we can use the format function as follows.

message = "The time is {}:{}:{}."
print(message.format(hour, minute, second))

The format function replaces the placeholders {} with its arguments, in order.

According to the string formatting syntax, we can also change the order of substitution using:

  • Indices (0 is the first item), or
  • Names inside the placeholders {}.
print("You should {0} {1} what I say instead of what I {0}.".format("do", "only"))
print("The surname of {first} {last} is {last}.".format(first="John", last="Doe"))

We can also evaluate variables inside the replacement field:

yyyy, mm = "2024", "09"
f"""{yyyy}{mm}CS1302
Intro to Comp Progm'g"""
@interact(
    expression=r"'ABC'",
    fill="*",
    align={"None": "", "<": "<", ">": ">", "=": "=", "^": "^"},
    width=(0, 20),
)
def print_object(expression, fill, align="^", width=10):
    format_spec = f"{{:{fill}{align}{'' if width==0 else width}}}"
    print("Format spec:", format_spec)
    print("Print:", format_spec.format(eval(expression)))
Solution to Exercise 9
  1. It returns a ValueError because align must be specified when fill is.
  2. The newline character is simply regarded a character. The formatting is not applied line-by-line. E.g., try ‘ABC\nDEF’.
%%ai chatgpt -f text
Explain in a paragraph or two the idea of a templating engine such as jinja2 
and why jinja2 is called jinja2.
Footnotes
  1. + can also be used as a unary operator, but it is not very useful.