December 4: Python Numeric and Mathematical Modules

Today is a bit less bad than yesterday: As part of the Python Standard Library traversal, after yesterday's extensive post about data types, today we're going to look at Python modules that take care of numbers and maths and make us very happy because we don't need to implement them ourselves. Or wait for NumPy to install.

Highlights

  • The numbers module contains abstract base classes so you can check for features of given numbers using isinstance.
  • The Python documentation takes a stand for tau and against pi. Good documentation.
  • cmath provides mathematical operations and functions for complex numbers – it exists separately from math so that people aren't surprised when they run math.sqrt(-1). 🤣
  • fractions.limit_denominator() allows you to take a float and turn it into the number it was meant to be. Go team fractions.
  • The fact that decimal is singular and fractions is plural will never cease to confuse me.
  • Apparently there's both random.gauss() and random.normalvariate() ???
  • statistics.NormalDist creates a normal distribution either from a set of samples or the mean and a standard deviation.

numbers

The numbers module contains abstract base classes, so you can check for features of given numbers using isinstance. These classes are Number (the root class), Complex (guarantees real, imag and conjugate()), Real, Rational (guarantees numerator and denominator) and Integral.

math

Here you'll find mathematical functions defined by the C standard. Those include basic functions like ceil() and floor(), trunc(), remainder() and prod(), isclose(), factorial() and comb() (binomial coefficient), gcd() (greatest common divisor) and lcm() (least common multiple, new in 3.9). Square root, logarithmic functions and exponential/power functions, trig functions and conversion between degree and radians, plus some functions I've never heard of. Additionally, you get pi, e, tau plus advertisement for tau and against pi, inf and nan.

There's also a bit of floating point handling, as of 3.9: ulp() returns the value of the least siginificant bit of a given number ("unit in the last place"), and nextafter() returns the next float value from a in direction b.

cmath

cmath contains helper functions for dealing with complex numbers – exactly like math, only that all return values are always complex numbers, even when they could be expressed as real numbers. Use polar() and rect() to convert between the two different coordinate representations. There are a ton of calculation functions: exp, log, log10, sqrt, all the trigonometric functions, and isclose() to see if two complex numbers are close to each other.

This module exists to avoid confusing the average math module user, who would not expect math.sqrt(-1) to actually return a result. 🤣

decimal

Who wants floating point arithmetic that doesn't constantly try to screw you over? I do! decimal.Decimal is here for all of us.

Decimals

Decimals are immutable objects with a sign, a coefficient and an exponent. They generally follow float norms in also understanding about positive and negative zero and infinity. They are instantiated with a number, a string or a tuple. You usually do not want to pass float values, and instead go with their string representation. You can get the most significant digit with .adjusted(), get the value of e**x with exp(), run log10() and ln(), and run all sorts of arithmetic operations – you'd choose to use the decimal object methods to make sure you're dealing with the precision you want. Run normalize() if you want to make sure the number is as readable as possible. Use helper methods like is_finite(), is_nan(), is_signed(), is_zero() and the like to learn more about a number.

Context

All decimal operations take place in a context, which you can change. The context specifies precision, rounding rules, limits on exponents, flags indicating the results of operations, and trap enablers which determine whether signals are treated as exceptions. You can use getcontext() and setcontext() to interact with it, or use the localcontext context manager to change it just for a while. Read the docs for a list of rounding flags, trap/signal flags and other attributes (like prec) that you can change with the context.

Signals

Signals can be ignored, treated as information or as errors. They arise when "exceptional conditions arise" during computation, such as inexact operations, division by zero or overflows.

Working with threads

Every thread retains its own context object, so you can change your decimal context without influencing other threads.

fractions

You know how floats are evil and bad? Fractions are not evil and not bad! You can instantiate them from two numbers, or a single number, or even a string. You can do all sorts of math on them and run as_integer_ratio() (much more useful than on floats!). Even more useful: limit_denominator() returns the closest fraction with a denominator smaller than n=100000, which is great for converting a float to the number it was meant to be!

random

All random generators share the same base generator, random.Random, which you can instantiate if you need a separate generator. Almost all functions rely on random.random() (which returns values between 0 and 1), and then runs some transformations on it. The generators are pseudo-random and should not be used for security purposes.

Bookkeeping functions

random.seed() starts the generator with a seed or with system time (use for reproducibility). With getstate() and setstate(), you can manipulate the generator status.

The main generator functions are randrange() for integers, choice(), choices(), shuffle() and sample() for sequences. For real-value distributions, there's the base random() for the 0, 1 interval, randbytes() and randbits() for n random bytes or bytes (new in 3.9), uniform() for any interval, triangular() for a weighted interval, gauss() and normalvariate() and a lot of other distributions. Wish I knew enough math to appreciate it.

Alternative Generator

Normal random methods use random.Random as generator, but you can also use random.SystemRandom, which uses os.urandom() if it is available.

statistics

This is basically our scientific calculator – not a competitor with NumPy, but making sure we can draw graphs and conclusions. Functions support int, float, Decimal and Fraction.

The statistics package includes different sorts of mean() and median() calculations, plus mode, multimode and quantile functions. It also has measures for spread, such as stdev() and variance(). Additionally, there is a NormalDist class that creates a standard distribution from either a mean and a standard deviation (boring) or a set of samples (woo-hoo!)