4. Basic operations and types¶
4.1. Python as a calculator¶
At its most basic, Python can be used as a calculator to perform standard mathematical operations:
25 * 4.7
Most of these translate directly from pen+paper ("math expression") into a program ("code expression"), as in each of the following examples:
Math expression |
Code expression |
|
---|---|---|
|
||
|
||
|
||
|
The spacing between the numbers and the operators within a line does
not matter; thus, 3+2
and 3+ 2
and 3 + 2
all evaluate
equivalently. However, it is important to make the code readable to
humans, so it is useful to be consistent in form: 3+2
or 3 + 2
might be preferable. (Later, we will see that spacing at the
beginning of a line does have important meaning in Python.)
Since programming's roots are in performing mathematical expressions
on a machine, Python contains many of the basic operators that we
might expect: addition +
, subtraction -
, division /
and
multiplication *
. These are in a family called binary
arithmetic operators, because they perform basic arithmetic on two
values (called operands): one to their left and one to their
right, e.g., 10 + 31
. Additional binary arithmetic operators
include:
- The integer division operator:
//
.Here, the value of the quotient is returned with the remainder discarded, so that9 // 2
evaluates to4
, and45 // 7
evaluates to6
. - The remainder operator (also called the modulo operator):
%
.Essentially,%
is the complement of//
, because it produces only the remainder from division. Thus,9 % 2
evaluates to1
, and45 % 7
evaluates to3
. - The power operator (or exponentiation):
**
.To square five, one would write5**2
. Fractional and negative powers may be used, so that the square root of seven could be written as7**0.5
, and the cube of one half could be written2**-3
.
The order in which Python performs operations in expressions is called
operator precedence: operations with "higher precedence" are
evaluated first. Much of this mirrors the standard mathematical
hierarchy of the order of operations. For example, parenthetical
expressions (...)
have precedence over exponentiation, which is
evaluated before multiplication and division, which in turn are
evaluated before addition and subtraction. So, 3 + 4 * 5
evaluates to 23, not to 35 (and spacing within the expression would
not affect this). Both %
and //
are grouped with division and
multiplication. We will meet further operators below, such as logic
operators and comparisons, and will discuss their relative precedence
in evaluation, too.
An example of a Python operator that has only one operand is what we
would consider "negation" in mathematics: -3
. In this case, the
-
is a unary arithmetic operator, operating on one object to
its right ("unary" means one, as opposed to "binary", which means
two):
-
as unary operator, when it only has a value (operand) to its right (and either no value or an operator to its left):- 20 7.5 ** -2
-
as binary operator, when it has an operand on both the left and the right (parentheses are not an operator; their interior resolves to a value, which forms the operand on the left):3-4 (3 + 1) - 10
(And +
can also be either a unary or binary operator in the exact
same way as -
. We just tend to use the former less, since +2
is the same as the simpler 2
.)
Python internally distinguishes when -
is a binary arithmetic
operator (performing subtraction) and when it is a unary arithmetic
operator (signing a value) by context; the spacing doesn't matter, but
instead the Python interpreter checks what is to the left and right of
the operator. The thing for us to know is that the binary and unary
forms of -
have different operational precedence, so we have to be
careful to not mistake them.
Q: Think about what the results of each of the following expressions should be, and then check by evaluating each line in Python (which will have to be done individually---as written, Python won't output each line's evaluation, just the final one; we will see how to output multiple values simultaneously soon):
13**2
2-3**2
33 ** -2
43 ** 2 - 2
53 * 2-2
610+3 * 2
7-+-++-5
810 // 3 * 2
911 // -4
109**0.5 + 1
114**(-0.5)
1223 % 5 % 2
1325 % 5
145 % 25
Note
From the above examples, note that the order of
precedence with -
and **
depends on whether the
minus appears occurs in the base or in the exponent.
If we are ever uncertain about operator precedence in this or other cases, we can check a simple test expression or two. This quick testability is one great convenience of having interactive Python environments like IPython or Jupyter-Notebooks available for quickly running small pieces of code. Additionally, we can always use parentheses to simplify and clarify interpretation visually. We want the code to be both correct and readable.
4.2. Errors and debugging¶
Errors, mistakes, typos and bugs (the last, charmingly named when computational machines were much larger and actual bugs could cause mishaps) will occur. So what happens if we make a mistake with syntax when typing an expression---how will Python cope with this? The following line is missing a second operand for the binary power operator:
4 + 3**
This causes Python to give us an error message:
File "<ipython-input-8-46aaa8163a8b>", line 1
4 + 3**
^
SyntaxError: invalid syntax
Note how Python helpfully tries to tell us where the problem is: it
specifies that the error occurred in "line 1" (later we will have
multiline commands) and then uses the ^
symbol like an arrow to
point to an even more specific part of that line where it got confused
(recall, this error is missing an operand after **
). Then it also
describes the nature of its complaint: it classifies it as a
SyntaxError
(and that indeed was kind of error we intentionally
made here). A syntax error might occur if an expected item is missing
(like a value) or added (like extra symbol), and we will see examples
of other kinds of errors as we proceed.
Here and in the future: Don't be afraid to see error messages. The Python interpreter is trying to help us to debug the code (that is, to solve the problem). An error message provides useful information (hence, the "message" part), and with more experience it becomes easier to understand it and to see what is happening in the code. There can be subtleties with debugging. Sometimes the actual error is something that occurred earlier in the code than where it points, causing Python to get confused when it reaches that spot. And sometimes there are multiple errors---when this happens, we should go to the first error message given, and start solving progressively from there. We can help ourselves greatly if we test pieces of our code, and run our code often, so that we are more likely to know immediately when new errors pop up and narrow the range of where they might be happening. This is a good habit to start developing early on.
Even experienced programmers get error messages when they write code. But being experienced means that they understand more of the terminology and have likely seen similar messages before, so that they can typically resolve their coding issues more quickly. Debugging will become much easier as we get more coding experience. For this reason we will frequently point out possible errors and common error messages in given situations, and it is worth making mental note of them, as well as other ones you happen to encounter. The sooner you become comfortable with reading them and debugging, the more efficient and happier you will be while programming.
We just have to make sure we fix our code in an appropriate way. Going back to the above case, we could do so perhaps as follows:
4 + 3**2
Note that there are more subtle kinds of errors, those that the Python
interpreter won't flag. Consider if we had accidentally left our
finger on the 2
key too long above, and typed:
4 + 3**222
In the context of the calculation, we entered the incorrect number---but this is not a mistake that the Python interpreter will detect. These are bugs that we, the humans, will have to squash on our own, with testing, testing and more testing of the code; even if it results in a failure later in the code, we will have to track down the original badness. We wish that Python could detect these kinds of mistakes, and we can even put "sanity checks" in our code to help catch things like this. But this is also part of the art of programming, and why it is important to have an expectation of outcomes of the code.
4.3. Exactness (or not) of calculations¶
What should we expect from our computed results? Are they always, 100% correct? Well, what do we even mean by "correct"?
Firstly, as with all machines, computers are not perfect. But the
odds of some random, internal error producing a result that evaluates
3+5
as 7
is negligible. While it is possible that random
errors can occur to affect calculations, the odds of this on a healthy
computer are extremely small, and there are even underlying corrective
checks in place. In practice we shouldn't worry about these kinds of
"machine" errors occurring.
Secondly, programmers are certainly not perfect, and unfortunately
the odds of coding mistakes (bugs, introduced above) are not
negligible. Python can detect certain kinds of errors (such as
syntax-based mistakes) and alert us to these with error messages, but
there are still many user errors that will not be caught by the
internal debugging tools (e.g., typing 4
instead of -4
). We
as programmers need to test and re-test our code to reduce the number
and likelihood of these. This effort of validating and checking our
code is one of the main arts of programming.
Thirdly, computers are finite machines. In mathematics, a number can be arbitrarily large, even to the point of using infinity. However, a computer cannot calculate with a number so big that it won't fit into the memory or disk space. There is always some finite maximum value above which we cannot calculate: infinity won't fit gracefully into computer memory. In Python version 3, this limit tends to be quite large, and it will often be above any number that would be realistically calculated, which is convenient. (But note that different programming languages might have very different maxima; and if you work with very large numbers, you want to check the maximum to make sure you don't reach it!)
Relatedly, the finitude of computers means that we also cannot
represent a number with so many decimal places that it exceeds
computer memory. In fact, we will have to account for this decimal
finiteness quite often, and perhaps at a lower number of decimal
places than one might expect. Consider this simple division:
2.0/3.0
. What is the result when Python evaluates it? In
mathematics the result would be with
an infinite number of decimal places. However, the computer has
finite disk space and memory, and here outputs only
0.6666666666666666
, even though the computer memory is larger than
16 demical places. In general, we cannot expect the computer to have
an exact representation of such numbers, nor of irrational
(, ) nor of transcendental numbers
(, Euler's e, etc.; though mathematicians also have
difficulty exactly representing this last kind, at times...).
The finitude of computers will always be something to keep in mind when dealing with values that contain decimal points (called floating point numbers or just floats, discussed more below). There is a limit to the degree of precision (or just precision) with which a computer can evaluate expressions, which determines the maximal number of decimal places are used in the output. If this limit is reached, one of two outcomes can occur, depending on the evaluation: truncation, whereby the remaining digits get chopped off; or rounding, which can occur by different rules. Thus, decimal (or "floating point") expressions have a finite level of approximation, which can be loosely thought of as "being correct to a certain number of decimal places" (decimal precision).
A consequence of having finite precision is that we must view all float evaluations as approximations. The digit at that last place of precision might not be consistent over repeated calculations or on different machines (e.g., due to varied rounding rules). A degree of difference from analytic calculation will exist, called truncation error or round-off error, depending on the underlying mechanism. While we might be tempted to ignore such a tiny error that might be, say, of order , it still means that we can have no expectation of exactness of computer-based calculations with floating point numbers. Also, if we have millions of calculations within a program, those random errors can accummulate into a meaningful difference. And while a particular floating point calculation might lead to equality on your computer, it might evaluate differently after a software update, or on another computer, or due to some other uncontrollable change. We will see later that some floats without many decimal places even have issues. Even within a single calculation, the round-off error and its instability might have meaningful consequences, so we must always treat floating point calculations as approximations.
Note
Python has a syntax for scientific notation (as do many
programming languages). To express, say,
or , one
would use 1.23e4
and 5.678e-9
, respectively. The
"e" is for "exponentiation." Equivalently, one could use a
capital "E" and write 1.23E4
, etc.
We will see this notation often when discussing precision
and roundoff error, such as when an expression evaluates to
a tiny number of order (or 1e-16
, in
Python), instead of to 0.
In summary, both programmers and computers have some fundamental limitations that affect the way we will go about programming. There are many calculations that we can treat as exact, but we must be aware that a very large, important class of them cannot be. We can still use programming to usefully translate mathematics into operations the machine can do for us, but we will have to be careful in our own programming practice and in the kinds of calculations we ask a computer to perform. This section should convince you of the following:
Floating point results may have small precision errors, making exact results (from analytic calculations) difficult if not impossible to obtain with floats;
Differences involving the results of floating point arithmetic may lead to tiny rounding errors instead of to an exact zero, which may lead to problems in further calculations.
At all times when programming, we should have some knowledge of expected output, so that we can test and verify both pieces of code and the overall results.
Exactness is a tricky concept in many computer calculations. When working with floats (decimal points), don't rely on it.
Computers are useful, but they must be used with care!
4.4. Type, with some mathematical examples¶
Above, we have started noting some special considerations of numbers that have decimal points in them. One question that might be asked is: what is the difference (if any) in Python between the following calculations:
3 * 4
3.0 * 4.0
? The first evaluates to 12
, and the second evaluates to
12.0
. While these numbers have the same value, they do have
an important difference: mathematically speaking, the first appears to
be an integer, and the second appears to belong to the (rational) real
numbers.
In many applications we take for granted the mathematical sets that
numbers belong to, like integers () or reals
(). But there is an analogous feature in
programming which we will not be able to ignore: every quantity
and object---even the non-mathematical ones---belongs to some
well-defined category, and this fundamental property is called its
type. The type property relates to how much space is allocated
for an object on the computer, what operations can be performed with
it, and how the utilized operators behave. There is a built-in
function in Python called type()
to tell us the type of any
object, and applying it here we can see that: type(12)
is int,
and type(12.0)
is float.
When performing calculations, we must consider both the value and type of the quantities involved. There are several types corresponding to familiar number sets from mathematics. In many cases the names are similar, but when programming we should use the computational name for clarity. We have noted some specific considerations that computational types have as opposed to their mathematical set analogues, mostly due to the finite nature of computers. Below are some examples of commonly used numerical types:
Comp type |
Related math set |
Examples |
Comment |
---|---|---|---|
bool |
Booleans |
|
There are only two values in this set. |
int |
Integers |
|
The math set goes to , but the computational int has finite boundaries. |
float |
Real numbers , more specifically rationals |
|
The precision of floats is limited, and they have finite magnitude. |
complex |
Complex numbers |
|
The real and imaginary components are each floats, and therefore bound to finite precision and finite magnitude. In Python, is the imaginary number. |
It is worth stressing again that the mapping between most computational types and math sets is close but not perfect. This is particularly true for floats, because they have a finite precision, and therefore they can only represent a subset of the rational number set. The same is true of complex numbers, since each of their imaginary and real components are floats. The size and precision of all numerical values is eventually limited by computer size or other practical constraints.
When performing operations, what determines the type of the result?
In Python, it is a combination of the input(s) and the operator(s) in
the expression. Adding ints produces an int, as does multiplying
ints. Adding or multiplying floats produces a float. However, some
operators may lead to the result having a different type. For
example, an int divided by an int yields a float, even if the result
contains no remainder: 6/3
evaluates to 2.0
and 3/10
evaluates to 0.3
; if the results stayed ints, then they would
evaluate to 2
and 0
. This is an example of Python performing
implicit type conversion, whereby the interpreter is producing a
type different than that of the elements of the expression, in order
to reduce possible truncation.
Note
Not all programming languages behave the same way. For
example, in C and even in the earlier Python 2.7, standard
division of two integers produced another integer, so that
3/10
would evaluate to 0
(basically, performing
integer division, which would require //
in Python 3).
What happens when types are mixed? Evaluating 3.0 * 4
yields
12.0
, a float. Here again Python is performing implicit type
conversion: when mixing types in an expression, the result will be of
the "most general" type in the expression. In this example, the more
general type is float (in the sense that int values are a subset
within floats).
The programmer can also have a final say on the type of a quantity, by
performing explicit type conversion: using Python functions to
specify the desired type. For example, float(3)
yields 3.0
,
and int(3.4)
yields 3
. The output of bool()
used on any
numerical type that is nonzero will be True
, and anyone that is
exactly zero will be False
: thus, bool(3)
, bool(-3)
and
bool(0.0)
evaluate to True
, True
and False
. Bool
conversion is a useful "zero detector" because of this.
The complex type can be a bit tricky to deal with: complex(-1.0)
yields (-1+0j)
, but trying to invert the process with
float(-1+0j)
yields an error. The latter is not a well-defined
mathematical operation, namely because the complex type essentially
has two components, while the float is scalar. However,
bool(-1+0j)
is still defined---it is True
here because the
complex number is nonzero. Complex numbers are discussed more in
a later section.
Q: What type does the following output?
type(4.5 + 3.5)
Q: Think about what the type of each of the following is, and then
put each expression within the type(..)
function to check:
14
2-4
3float(4)
40.0
5int(float(True))
4.5. Practice¶
First come up with what you think the answer should be, and then check the result using an ipython terminal:
What are the types of each of the following?
1-1 220.00001 310**19 410**19 - 10**19 51 + 0j 61 7-2. 8False
What are the values (and types-- difficult properties to separate in some cases!) of the following?:
12 + True 24.5 - 3 34.5 / 3 45 + 3 55 / 3 65 / 3. 75 // 3. 84.7 % 3 9float( 1 //4 ) 103. * 15 // 4 11False * True 12False and True
Is 18 a factor of 12345678? Use two different operators to check.
What are the category(ies) of each of the following operators:
5 - -1
In math, we could write and evaluate it. Copy this exact this exact expression directly to Python (without changing/adding any symbols); what error do you get, and why? How can you write a correct expression for this in Python?
Is this the correct way to calculate cube root of 27? If not, why not, and how could one adjust it to provide that information?:
27**1/3
Let's take the number 246897531. Write a single expression using only the modulo and integer division operators to output its hundreds digit (and which will keep working for any number we replace as a starter).
Does Python produce the correct answer for the following:
0.1**2
? Why or why not? (Hint: "correct" mathematically might differ from "correct" computationally.)