<jupyter_start><jupyter_text>**Note**: Click on "*Kernel*" > "*Restart Kernel and Clear All Outputs*" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/05_numbers/02_content.ipynb).  Chapter 5: Numbers & Bits (continued) In the third part of this chapter, we first look at the lesser known `complex` type. Then, we introduce a more abstract classification scheme for the numeric types.  The `complex` Type **What is the solution to $x^2 = -1$ ?** Some mathematical equations cannot be solved if the solution has to be in the set of the real numbers $\mathbb{R}$. For example, $x^2 = -1$ can be rearranged into $x = \sqrt{-1}$, but the square root is not defined for negative numbers. To mitigate this, mathematicians introduced the concept of an [imaginary number ](https://en.wikipedia.org/wiki/Imaginary_number) $\textbf{i}$ that is *defined* as $\textbf{i} = \sqrt{-1}$ or often as the solution to the equation $\textbf{i}^2 = -1$. So, the solution to $x = \sqrt{-1}$ then becomes $x = \textbf{i}$.If we generalize the example equation into $(mx-n)^2 = -1 \implies x = \frac{1}{m}(\sqrt{-1} + n)$ where $m$ and $n$ are constants chosen from the reals $\mathbb{R}$, then the solution to the equation comes in the form $x = a + b\textbf{i}$, the sum of a real number and an imaginary number, with $a=\frac{n}{m}$ and $b = \frac{1}{m}$.Such "compound" numbers are called **[complex numbers ](https://en.wikipedia.org/wiki/Complex_number)**, and the set of all such numbers is commonly denoted by $\mathbb{C}$. The reals $\mathbb{R}$ are a strict subset of $\mathbb{C}$ with $b=0$. Further, $a$ is referred to as the **real part** and $b$ as the **imaginary part** of the complex number.Complex numbers are often visualized in a plane like below, where the real part is depicted on the x-axis and the imaginary part on the y-axis.  `complex` numbers are part of core Python. The simplest way to create one is to write an arithmetic expression with the literal `j` notation for $\textbf{i}$. The `j` is commonly used in many engineering disciplines instead of the symbol $\textbf{i}$ from math as $I$ in engineering more often than not means [electric current ](https://en.wikipedia.org/wiki/Electric_current).For example, the answer to $x^2 = -1$ can be written in Python as `1j` like below. This creates a `complex` object with value `1j`. The same syntactic rules apply as with the above `e` notation: No spaces are allowed between the number and the `j`. The number may be any `int` or `float` literal; however, it is stored as a `float` internally. So, `complex` numbers suffer from the same imprecision as `float` numbers.<jupyter_code>x = 1j
id(x)
type(x)
x<jupyter_output><empty_output><jupyter_text>To verify that it solves the equation, let's raise it to the power of $2$.<jupyter_code>x ** 2 == -1<jupyter_output><empty_output><jupyter_text>Often, we write an expression of the form $a + b\textbf{i}$.<jupyter_code>2 + 0.5j<jupyter_output><empty_output><jupyter_text>Alternatively, we may use the [complex() ](https://docs.python.org/3/library/functions.htmlcomplex) built-in: This takes two parameters where the second is optional and defaults to `0`. We may either call it with one or two arguments of any numeric type or a `str` object in the format of the previous code cell without any spaces.<jupyter_code>complex(2, 0.5)<jupyter_output><empty_output><jupyter_text>By omitting the second argument, we set the imaginary part to $0$.<jupyter_code>complex(2)<jupyter_output><empty_output><jupyter_text>The arguments to [complex() ](https://docs.python.org/3/library/functions.htmlcomplex) may be any numeric type or properly formated `str` object.<jupyter_code>complex("2+0.5j")<jupyter_output><empty_output><jupyter_text>Arithmetic expressions work with `complex` numbers. They may be mixed with the other numeric types, and the result is always a `complex` number.<jupyter_code>c1 = 1 + 2j
c2 = 3 + 4j
c1 + c2
c1 - c2
c1 + 1
3.5 - c2
5 * c1
c2 / 6
c1 * c2
c1 / c2<jupyter_output><empty_output><jupyter_text>A `complex` number comes with two **attributes** `.real` and `.imag` that return the two parts as `float` objects on their own.<jupyter_code>x.real
x.imag<jupyter_output><empty_output><jupyter_text>Also, a `.conjugate()` method is bound to every `complex` object. The [complex conjugate ](https://en.wikipedia.org/wiki/Complex_conjugate) is defined to be the complex number with identical real part but an imaginary part reversed in sign.<jupyter_code>x.conjugate()<jupyter_output><empty_output><jupyter_text>The [cmath ](https://docs.python.org/3/library/cmath.html) module in the [standard library ](https://docs.python.org/3/library/index.html) implements many of the functions from the [math ](https://docs.python.org/3/library/math.html) module such that they work with complex numbers.  The Numerical Tower Analogous to the discussion of *containers* and *iterables* in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/04_iteration/02_content.ipynbContainers-vs.-Iterables), we contrast the *concrete* numeric data types in this chapter with the *abstract* ideas behind [numbers in mathematics ](https://en.wikipedia.org/wiki/Number).The figure below summarizes five *major* sets of [numbers in mathematics ](https://en.wikipedia.org/wiki/Number) as we know them from high school:- $\mathbb{N}$: [Natural numbers ](https://en.wikipedia.org/wiki/Natural_number) are all non-negative count numbers, e.g., $0, 1, 2, ...$- $\mathbb{Z}$: [Integers ](https://en.wikipedia.org/wiki/Integer) are all numbers *without* a fractional component, e.g., $-1, 0, 1, ...$- $\mathbb{Q}$: [Rational numbers ](https://en.wikipedia.org/wiki/Rational_number) are all numbers that can be expressed as a quotient of two integers, e.g., $-\frac{1}{2}, 0, \frac{1}{2}, ...$- $\mathbb{R}$: [Real numbers ](https://en.wikipedia.org/wiki/Real_number) are all numbers that can be represented as a distance along a line, and negative means "reversed," e.g., $\sqrt{2}, \pi, \text{e}, ...$- $\mathbb{C}$: [Complex numbers ](https://en.wikipedia.org/wiki/Complex_number) are all numbers of the form $a + b\textbf{i}$ where $a$ and $b$ are real numbers and $\textbf{i}$ is the [imaginary number ](https://en.wikipedia.org/wiki/Imaginary_number), e.g., $0, \textbf{i}, 1 + \textbf{i}, ...$In the listed order, the five sets are perfect subsets of the respective following sets, and $\mathbb{C}$ is the largest set. To be precise, all sets are infinite, but they still have a different number of elements.  The data types introduced in this chapter and its appendix are all *(im)perfect* models of *abstract* mathematical ideas.The `int` and `Fraction` types are the models "closest" to the idea they implement: Whereas $\mathbb{Z}$ and $\mathbb{Q}$ are, by definition, infinite, every computer runs out of bits when representing sufficiently large integers or fractions with a sufficiently large number of decimals. However, within a system-dependent range, we can model an integer or fraction without any loss in precision.For the other types, in particular, the `float` type, the implications of their imprecision are discussed in detail above.The abstract concepts behind the four outer-most mathematical sets are formalized in Python since [PEP 3141 ](https://www.python.org/dev/peps/pep-3141/) in 2007. The [numbers ](https://docs.python.org/3/library/numbers.html) module in the [standard library ](https://docs.python.org/3/library/index.html) defines what programmers call the **[numerical tower ](https://en.wikipedia.org/wiki/Numerical_tower)**, a collection of five **[abstract data types ](https://en.wikipedia.org/wiki/Abstract_data_type)**, or **abstract base classes** (ABCs) as they are called in Python jargon:- `Number`: "any number" (cf., [documentation ](https://docs.python.org/3/library/numbers.htmlnumbers.Number))- `Complex`: "all complex numbers" (cf., [documentation ](https://docs.python.org/3/library/numbers.htmlnumbers.Complex))- `Real`: "all real numbers" (cf., [documentation ](https://docs.python.org/3/library/numbers.htmlnumbers.Real))- `Rational`: "all rational numbers" (cf., [documentation ](https://docs.python.org/3/library/numbers.htmlnumbers.Rational))- `Integral`: "all integers" (cf., [documentation ](https://docs.python.org/3/library/numbers.htmlnumbers.Integral))<jupyter_code>import numbers
dir(numbers)<jupyter_output><empty_output><jupyter_text>Duck Typing (continued) The primary purpose of ABCs is to classify the *concrete* data types and standardize how they behave. This guides us as the programmers in what kind of behavior we should expect from objects of a given data type. In this context, ABCs are not reflected in code but only in our heads.For, example, as all numeric data types are `Complex` numbers in the abstract sense, they all work with the built-in [abs() ](https://docs.python.org/3/library/functions.htmlabs) function (cf., [documentation ](https://docs.python.org/3/library/numbers.htmlnumbers.Complex)). While it is intuitively clear what the [absolute value ](https://en.wikipedia.org/wiki/Absolute_value) (i.e., "distance" from $0$) of an integer, a fraction, or any real number is, [abs() ](https://docs.python.org/3/library/functions.htmlabs) calculates the equivalent of that for complex numbers. That concept is called the [magnitude ](https://en.wikipedia.org/wiki/Magnitude_%28mathematics%29) of a number, and is really a *generalization* of the absolute value.Relating back to the concept of **duck typing** mentioned in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/04_iteration/00_content.ipynbDuck-Typing), `int`, `float`, and `complex` objects "walk" and "quack" alike in context of the [abs() ](https://docs.python.org/3/library/functions.htmlabs) function.<jupyter_code>abs(-1)
abs(-42.87)<jupyter_output><empty_output><jupyter_text>The absolute value of a `complex` number $x$ is defined with the Pythagorean Theorem where $\lVert x \rVert = \sqrt{a^2 + b^2}$ and $a$ and $b$ are the real and imaginary parts.<jupyter_code>abs(3 + 4j)<jupyter_output><empty_output><jupyter_text>On the contrary, only `Real` numbers in the abstract sense may be rounded with the built-in [round() ](https://docs.python.org/3/library/functions.htmlround) function.<jupyter_code>round(123, -2)
round(42.1)<jupyter_output><empty_output><jupyter_text>`Complex` numbers are two-dimensional. So, rounding makes no sense here and leads to a `TypeError`. So, in the context of the [round() ](https://docs.python.org/3/library/functions.htmlround) function, `int` and `float` objects "walk" and "quack" alike whereas `complex` objects do not.<jupyter_code>round(1 + 2j)<jupyter_output><empty_output><jupyter_text>Goose Typing Another way to use ABCs is in place of a *concrete* data type.For example, we may pass them as arguments to the built-in [isinstance() ](https://docs.python.org/3/library/functions.htmlisinstance) function and check in which of the five mathematical sets the object `1 / 10` is.<jupyter_code>isinstance(1 / 10, float)<jupyter_output><empty_output><jupyter_text>A `float` object is a generic `Number` in the abstract sense but may also be seen as a `Complex` or `Real` number.<jupyter_code>isinstance(1 / 10, numbers.Number)
isinstance(1 / 10, numbers.Complex)
isinstance(1 / 10, numbers.Real)<jupyter_output><empty_output><jupyter_text>Due to the `float` type's inherent imprecision, `1 / 10` is *not* a `Rational` number.<jupyter_code>isinstance(1 / 10, numbers.Rational)<jupyter_output><empty_output><jupyter_text>However, if we model `1 / 10` as a `Fraction` object (cf., [Appendix ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/05_numbers/03_appendix.ipynbThe-Fraction-Type)), it is recognized as a `Rational` number.<jupyter_code>from fractions import Fraction
isinstance(Fraction("1/10"), numbers.Rational)<jupyter_output><empty_output><jupyter_text>Replacing *concrete* data types with ABCs is particularly valuable in the context of "type checking:" The revised version of the `factorial()` function below allows its user to take advantage of *duck typing*: If a real but non-integer argument `n` is passed in, `factorial()` tries to cast `n` as an `int` object with the [int() ](https://docs.python.org/3/library/functions.htmlint) built-in.Two popular and distinguished Pythonistas, [<NAME> ](https://github.com/ramalho) and [<NAME> ](https://en.wikipedia.org/wiki/Alex_Martelli), coin the term **goose typing** to specifically mean using the built-in [isinstance() ](https://docs.python.org/3/library/functions.htmlisinstance) function with an ABC (cf., Chapter 11 in this [book](https://www.amazon.com/Fluent-Python-Concise-Effective-Programming/dp/1491946008) or this [summary](https://dgkim5360.github.io/blog/python/2017/07/duck-typing-vs-goose-typing-pythonic-interfaces/) thereof).  Example: [Factorial ](https://en.wikipedia.org/wiki/Factorial) (revisited)<jupyter_code>def factorial(n, *, strict=True):
    """Calculate the factorial of a number.

    Args:
        n (int): number to calculate the factorial for; must be positive
        strict (bool): if n must not contain decimals; defaults to True;
            if set to False, the decimals in n are ignored

    Returns:
        factorial (int)

    Raises:
        TypeError: if n is not an integer or cannot be cast as such
        ValueError: if n is negative
    """
    if not isinstance(n, numbers.Integral):
        if isinstance(n, numbers.Real):
            if n != int(n) and strict:
                raise TypeError("n is not integer-like; it has non-zero decimals")
            n = int(n)
        else:
            raise TypeError("Factorial is only defined for integers")

    if n < 0:
        raise ValueError("Factorial is not defined for negative integers")
    elif n == 0:
        return 1
    return n * factorial(n - 1)<jupyter_output><empty_output><jupyter_text>`factorial()` works as before, but now also accepts, for example, `float` numbers.<jupyter_code>factorial(0)
factorial(3)
factorial(3.0)<jupyter_output><empty_output><jupyter_text>With the keyword-only argument `strict`, we can control whether or not a passed in `float` object may come with decimals that are then truncated. By default, this is not allowed and results in a `TypeError`.<jupyter_code>factorial(3.1)<jupyter_output><empty_output><jupyter_text>In non-strict mode, the passed in `3.1` is truncated into `3` resulting in a factorial of `6`.<jupyter_code>factorial(3.1, strict=False)<jupyter_output><empty_output><jupyter_text>For `complex` numbers, `factorial()` still raises a `TypeError` because they are neither an `Integral` nor a `Real` number.<jupyter_code>factorial(1 + 2j)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12409196934355216, 'fraction_numerical': 0.01432855714761746, 'mean_word_length': 4.954761904761905, 'pattern_counts': {'":': 0, '<': 96, '<?xml version=': 0, '>': 96, 'https://': 48, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '23', 'dir': 'jupyter-structured-clean-dedup', 'id': '630183', 'license': 'MIT', 'n_tokens_mistral': 4766, 'n_tokens_neox': 4471, 'n_words': 1667, 'path': '05_numbers/02_content.ipynb', 'repo_name': 'jads-nl/intro-to-python', 'size': '15021'}