numbers

Numbers Module in Python

The numbers module in Python defines a hierarchy of abstract base classes that represent different types of numbers, such as complex, real, and integral numbers. These classes provide a common interface for representing and manipulating numbers, and are used as base classes for specific numeric types in Python, such as int, float, and complex.

Abstract Base Classes

An abstract base class (ABC) is a class that defines a set of abstract methods, which are methods that do not have an implementation. The purpose of an ABC is to define a common interface that can be shared by different classes, while allowing each class to provide its own implementation of the abstract methods.

The numbers module defines the following ABCs:

  • Number: The base class for all numeric types. It defines a single abstract method, __add__, which represents addition.

  • Complex: The class for complex numbers. It defines additional abstract methods for complex arithmetic operations, such as __mul__ (multiplication) and __div__ (division).

  • Real: The class for real numbers. It defines additional abstract methods for real arithmetic operations, such as __sub__ (subtraction) and __truediv__ (true division).

  • Integral: The class for integral numbers. It defines additional abstract methods for integer arithmetic operations, such as __floordiv__ (floor division) and __mod__ (modulus).

Here is a simplified example of an abstract base class:

from abc import ABC, abstractmethod

class Shape(ABC):

    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

In this example, the Shape class is an ABC that defines two abstract methods, area and perimeter. Any class that inherits from Shape must implement these two methods.

Concrete Classes

Concrete classes are classes that implement the abstract methods of an ABC. In the context of the numbers module, concrete classes represent specific numeric types, such as int, float, and complex.

Here is an example of a concrete class that inherits from the Number ABC:

class Integer(Number):

    def __init__(self, value):
        self.value = value

    def __add__(self, other):
        return Integer(self.value + other)

In this example, the Integer class inherits from the Number ABC and implements the __add__ method. The __add__ method defines the addition operation for Integer objects.

Real-World Applications

The numbers module is used in a variety of real-world applications, including:

  • Numeric computation: The ABCs defined in the numbers module provide a common interface for representing and manipulating numbers, which is essential for numeric computation tasks such as scientific simulations and financial modeling.

  • Data validation: The ABCs can be used to validate that a value is of a specific numeric type. For example, a function that takes a real number as input could use the Real ABC to check that the input is a valid real number.

  • Type checking: The ABCs can be used to check the type of a value at runtime. For example, a function that needs to operate on a list of integers could use the Integral ABC to check that each element of the list is an integer.


Number

In Python, a number is any object that represents a numerical value. This includes integers, floats, and complex numbers. You can check if an object is a number using the isinstance() function:

>>> isinstance(123, Number)
True

Integer

An integer is a whole number, such as 1, 2, or 3. Integers can be positive or negative. You can create an integer using the int() function:

>>> int('123')
123

Float

A float is a decimal number, such as 1.23, 4.56, or 7.89. Floats can be positive or negative. You can create a float using the float() function:

>>> float('1.23')
1.23

Complex

A complex number is a number that has a real part and an imaginary part. The real part is a regular number, and the imaginary part is a number multiplied by the imaginary unit i. You can create a complex number using the complex() function:

>>> complex(1, 2)
(1+2j)

Applications

Numbers are used in a wide variety of applications, including:

  • Mathematics

  • Science

  • Engineering

  • Finance

  • Business

Here are some real-world examples of how numbers are used:

  • A scientist might use numbers to calculate the speed of light.

  • An engineer might use numbers to design a bridge.

  • A financial analyst might use numbers to track the stock market.

  • A business owner might use numbers to track sales and expenses.


Numeric Tower

Complex

Complex numbers are numbers that have a "real" part and an "imaginary" part. For example, 5+4i is a complex number with real part 5 and imaginary part 4. Complex numbers can be used to represent a wide variety of mathematical concepts, such as rotations in 3D space, quantum states, and wave functions.

The complex class in Python represents complex numbers. You can create a complex number by calling the complex function, passing in the real and imaginary parts as arguments. For example:

>>> z = complex(5, 4)
>>> z
(5+4j)

You can access the real and imaginary parts of a complex number using the real and imag attributes. For example:

>>> z.real
5
>>> z.imag
4

You can perform arithmetic operations on complex numbers using the usual operators. For example:

>>> z1 = complex(1, 2)
>>> z2 = complex(3, 4)
>>> z1 + z2
(4+6j)
>>> z1 - z2
(-2-2j)
>>> z1 * z2
(-5+10j)
>>> z1 / z2
(0.4444444444444444-0.2222222222222222j)
>>> z1 ** z2
(0.3678794411714423-0.9302847925323812j)

You can also use the abs() function to find the absolute value of a complex number, and the conjugate() method to find the complex conjugate. For example:

>>> abs(z1)
2.23606797749979
>>> z1.conjugate()
(1-2j)

Complex numbers are often used in science and engineering to represent physical quantities that have both a magnitude and a direction. For example, complex numbers can be used to represent the voltage and current in an electrical circuit, or the position and momentum of a particle in quantum mechanics.


Real

Definition: The Real class in Python is a subclass of the Complex class that represents real numbers. Real numbers are numbers that can be written without an imaginary part, such as 5 or -2.3.

Operations: Real adds the following operations to those provided by Complex:

  • Conversion to float: You can convert a Real number to a float using the float() function. For example:

>>> x = Real(5)
>>> float(x)
5.0
  • Truncation: Truncation removes the decimal part of a number. You can truncate a Real number using the math.trunc() function. For example:

>>> x = Real(5.4)
>>> math.trunc(x)
5
  • Rounding: Rounding rounds a number to the nearest integer. You can round a Real number using the round() function. For example:

>>> x = Real(5.4)
>>> round(x)
5
  • Floor: Floor returns the greatest integer less than or equal to a number. You can floor a Real number using the math.floor() function. For example:

>>> x = Real(5.4)
>>> math.floor(x)
5
  • Ceiling: Ceiling returns the least integer greater than or equal to a number. You can ceiling a Real number using the math.ceil() function. For example:

>>> x = Real(5.4)
>>> math.ceil(x)
6
  • Division and remainder: You can perform division and remainder operations on Real numbers using the divmod() function. For example:

>>> x = Real(5)
>>> y = Real(2)
>>> divmod(x, y)
(2, 1)
  • Integer division: Integer division returns the quotient of two numbers, rounded towards negative infinity. You can perform integer division on Real numbers using the // operator. For example:

>>> x = Real(5)
>>> y = Real(2)
>>> x // y
2
  • Modulo: Modulo returns the remainder of two numbers. You can perform modulo on Real numbers using the % operator. For example:

>>> x = Real(5)
>>> y = Real(2)
>>> x % y
1
  • Comparisons: You can compare Real numbers using the <, <=, >, and >= operators. For example:

>>> x = Real(5)
>>> y = Real(2)
>>> x > y
True

Defaults: Real also provides defaults for the following attributes and methods of Complex:

  • Real part: The real part of a Complex number is the part without the imaginary part. The real part of a Real number is simply the number itself.

  • Imaginary part: The imaginary part of a Complex number is the part with the i in front of it. The imaginary part of a Real number is always 0.

  • Conjugate: The conjugate of a Complex number is the number with the same real part but the opposite imaginary part. The conjugate of a Real number is the number itself.

Real World Applications

Real numbers are used in a wide variety of applications, including:

  • Science: Real numbers are used to measure physical quantities such as distance, speed, and temperature.

  • Engineering: Real numbers are used to design and build structures, machines, and systems.

  • Finance: Real numbers are used to track and manage money.

  • Medicine: Real numbers are used to measure and track vital signs and other medical data.


Class :class:Rational

The Rational class in the "numbers" module is a subclass of the Real class and represents rational numbers, which are numbers that can be expressed as a fraction of two integers. Rational numbers can be used to represent exact values, such as 1/2 or 3/4, or they can be used to approximate real numbers, such as pi or the square root of 2.

The Rational class has two attributes: numerator and denominator. The numerator attribute stores the numerator of the rational number, and the denominator attribute stores the denominator. The numerator and denominator attributes must be instances of the Integral class, and they must be in lowest terms, with the denominator positive.

The following code creates a Rational object representing the rational number 1/2:

from numbers import Rational

r = Rational(1, 2)

The r object has the following attributes:

r.numerator == 1
r.denominator == 2

The Rational class also provides a float() method that returns the floating-point representation of the rational number. The following code prints the floating-point representation of the r object:

print(float(r))

# Output: 0.5

Rational numbers are useful for representing quantities that are exact or that need to be represented precisely. For example, rational numbers can be used to represent the amount of money in a bank account or the length of a piece of wood.

Here is an example of how rational numbers can be used in a real-world application:

from numbers import Rational

# Create a rational number representing the amount of money in a bank account.
balance = Rational(100, 1)

# Withdraw $20 from the account.
withdrawal = Rational(20, 1)
balance -= withdrawal

# Print the remaining balance.
print(balance)

# Output: 80/1

In this example, we create a Rational object representing the balance in a bank account. We then withdraw $20 from the account by subtracting a Rational object representing the withdrawal amount from the balance object. Finally, we print the remaining balance, which is represented as a rational number.


Integral

The Integral class, which includes Rational as a subclass, brings a few additional features to the world of numbers in Python. Let's dive into them one by one:

Conversion to Integer:

This class allows you to convert a rational number to an integer, making it easier to represent whole numbers with more precision. For example:

>>> from fractions import Fraction
>>> rational_number = Fraction(3, 4)
>>> integer_number = int(rational_number)
>>> print(integer_number)
0

In this case, we convert the rational number 3/4 to its integer equivalent, which is 0.

Default Values:

The Integral class provides default values for some properties:

  • float: Returns the floating-point representation of the rational number.

  • numerator: The numerator of the rational number.

  • denominator: The denominator of the rational number.

For example:

>>> from fractions import Fraction
>>> rational_number = Fraction(3, 4)
>>> print(rational_number.float)
0.75
>>> print(rational_number.numerator)
3
>>> print(rational_number.denominator)
4

Abstract Methods:

The Integral class also introduces abstract methods, which are like placeholders for methods that must be implemented in subclasses. These methods include:

  • pow(self, other, modulus=None): Raises the rational number to the power of other, with optional modulus.

  • lshift(self, other): Shifts the bits of the rational number left by other positions.

  • rshift(self, other): Shifts the bits of the rational number right by other positions.

  • and(self, other): Performs bitwise AND operation with other.

  • xor(self, other): Performs bitwise XOR operation with other.

  • or(self, other): Performs bitwise OR operation with other.

  • invert(self): Performs bitwise NOT operation.

Real-World Applications:

Here are some potential applications of these features in real-world scenarios:

  • Financial calculations: Fraction objects can be used to represent and manipulate currency values and percentages.

  • Scientific computations: Rational numbers can represent exact values, which is important for applications that require precision.

  • Cryptography: Bitwise operations are commonly used in cryptographic algorithms to encrypt and decrypt messages.

  • Computer graphics: Bit shifting is used to manipulate images and animations.

  • Software engineering: Integer conversion can be useful for representing numbers in a compact form.


Hashing and Equality for Custom Types in Python

When you create your own custom types in Python, it's important to consider how they are handled by equality checks and hashing.

Equality Checks

For two objects to be considered equal, they must have the same value. For custom types, this means implementing the __eq__ method. For example:

class Fraction:
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator

    def __eq__(self, other):
        return self.numerator == other.numerator and self.denominator == other.denominator

Hashing

Hashing is used to determine the location of an object in a hash table, a data structure that stores key-value pairs. For custom types, this means implementing the __hash__ method.

Potential Collisions

When hashing objects, it's possible that two different objects have the same hash value. This is known as a collision. To minimize collisions, the hash value should be as unique as possible.

Example: Implementing Hashing for Fraction

The following example shows how to implement hashing for the Fraction class:

import math

class Fraction:
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator

    def __eq__(self, other):
        return self.numerator == other.numerator and self.denominator == other.denominator

    def __hash__(self):
        # Fast hash for integers
        if self.denominator == 1:
            return hash(self.numerator)

        # Use the built-in hash function for floating-point values
        if math.isclose(self, float(self)):
            return hash(float(self))

        # Fallback to a slower but more reliable hash
        return hash((self.numerator, self.denominator))

Real-World Applications

Hashing and equality checks are used in various real-world applications, such as:

  • Databases: To efficiently store and retrieve data based on keys.

  • Hash tables: To accelerate lookups in large datasets.

  • Sets: To check if an element is present in a collection without duplicates.


Adding More Numeric ABCs

In the hierarchy of numeric types in Python, there are several abstract base classes (ABCs) that define the behavior of different numeric types. These ABCs include:

  • Number: The base ABC for all numeric types.

  • Real: The ABC for real numbers, including integers, floats, and decimals.

  • Complex: The ABC for complex numbers.

You can add your own custom ABCs to this hierarchy by subclassing one of the existing ABCs and registering your new ABC with the existing ABC. For example, if you wanted to create a new ABC called MyFoo that represents a type of number that is a subclass of Complex, you would do the following:

class MyFoo(Complex):
    ...

MyFoo.register(Real)

This would create a new MyFoo ABC that is a subclass of Complex and a subclass of Real.

Real-world Application:

Customizing the numeric ABC hierarchy can be useful in a number of scenarios. For example, you could create a new ABC to represent a type of number that has specific properties or operations that are not supported by the existing ABCs. This could be useful in specialized domains, such as financial modeling or scientific computing.

Improved Code Snippet:

Here is an improved version of the code snippet that adds the MyFoo ABC to the numeric ABC hierarchy:

class MyFoo(Complex):
    def __init__(self, real, imaginary):
        super().__init__(real, imaginary)

    def my_foo_method(self):
        ...

MyFoo.register(Real)

This code creates a new MyFoo class that inherits from the Complex class and implements a custom my_foo_method method. The MyFoo class is then registered with the Real ABC using the register method.


Implementing Arithmetic Operations

In Python, we can implement arithmetic operations, like addition, subtraction, multiplication, and division, for our custom data types.

Mixed-Mode Operations

When dealing with different types of numbers (mixed-mode operations), we want to:

  1. Call an implementation that handles both types specifically.

  2. If not available, convert both numbers to the closest built-in type (e.g., int or float) and perform the operation there.

Integral Subtypes

For subtypes of Integral, which represent whole numbers, we can define __add__ and __radd__ methods like this:

class MyIntegral(Integral):

    def __add__(self, other):
        if isinstance(other, MyIntegral):
            # Both are our type, use a specific implementation
            return MyIntegral(self.value + other.value)
        else:
            # Convert both to int and perform the operation
            return int(self) + int(other)

Example Implementation

Here's an example implementation for a custom MyDecimal class:

from decimal import Decimal

class MyDecimal(Decimal):

    def __add__(self, other):
        if isinstance(other, MyDecimal):
            return MyDecimal(self.as_tuple()[0] + other.as_tuple()[0])
        else:
            return float(self) + float(other)

Real-World Applications

These arithmetic operations are used in various real-world scenarios, such as:

  • Financial calculations: Mixing currency types and converting to the appropriate exchange rates.

  • Scientific calculations: Dealing with different units of measurement and converting between them.

  • Custom data analysis: Creating specialized numeric types that handle specific data formats or constraints.


Magic methods or dunder methods are special methods in Python that are called automatically when specific operations are performed on an object.

The __add__ method is called when the + operator is used on an object. In the provided code, the __add__ method of the MyIntegral class is defined to handle addition with other MyIntegral objects, OtherTypeIKnowAbout objects, and primitive types like int, float, and complex. If the other object is not recognized, the method returns NotImplemented.

Here's a simplified example to illustrate the __add__ method in action:

class MyIntegral:
    def __init__(self, value):
        self.value = value

    def __add__(self, other):
        if isinstance(other, MyIntegral):
            return MyIntegral(self.value + other.value)
        else:
            raise TypeError("Cannot add MyIntegral with non-MyIntegral object")

i1 = MyIntegral(10)
i2 = MyIntegral(20)

result = i1 + i2
print(result.value)  # Output: 30

In this example, we define a simple MyIntegral class that represents an integral value. The __add__ method is defined to handle addition with other MyIntegral objects. When we add i1 and i2, the __add__ method is called and returns a new MyIntegral object with the sum of the two values.

The __radd__ method is similar to __add__, but it is called when the + operator is used on an object of the class from the right-hand side. In the provided code, the __radd__ method of the MyIntegral class is defined to handle addition with MyIntegral objects, OtherTypeIKnowAbout objects, Integral objects, Real objects, and Complex objects. If the other object is not recognized, the method returns NotImplemented.

Here's an example to illustrate the __radd__ method:

class MyIntegral:
    def __init__(self, value):
        self.value = value

    def __radd__(self, other):
        if isinstance(other, MyIntegral):
            return MyIntegral(self.value + other.value)
        else:
            raise TypeError("Cannot add MyIntegral with non-MyIntegral object")

i1 = MyIntegral(10)
i2 = 20

result = i2 + i1
print(result.value)  # Output: 30

In this example, we add a MyIntegral object (i1) with a primitive integer (i2). The __radd__ method of the MyIntegral class is called automatically and returns a new MyIntegral object with the sum of the two values.

Potential applications of magic methods include:

  • Defining custom behavior for arithmetic operators, such as addition, subtraction, multiplication, and division.

  • Defining custom behavior for comparison operators, such as equality, inequality, greater than, and less than.

  • Defining custom behavior for other operations, such as hashing, converting to a string, or iterating over the object.


Mixed-Type Operations on Complex Subclasses

Introduction

The Python numbers module provides a framework for defining numeric types that behave consistently in mathematical operations. When you define a new numeric type, you can choose to implement methods like __add__ (+) and __radd__ (+=) to define how your type behaves when it's involved in mathematical operations with other types.

Cases for Mixed-Type Operations

When defining mixed-type operations involving subclasses of Complex, there are five potential cases to consider:

Case 1: Specific Implementation in Subclass

If a subclass of Complex (e.g., A) defines a specific implementation for __add__ that accepts the other type (e.g., B), everything works as expected.

Example:

class A(Complex):
    def __add__(self, other):
        if isinstance(other, B):
            # Custom logic for A + B
            pass

Case 2: Fallback to Boilerplate Code

If A doesn't define __add__ or returns NotImplemented, the Python interpreter falls back to a "boilerplate" implementation that checks if the other type (e.g., B) defines __radd__.

Case 3: Specific Implementation in Other Type

If B defines a specific implementation for __radd__ that accepts A, everything works as expected.

Example:

class B(Complex):
    def __radd__(self, other):
        if isinstance(other, A):
            # Custom logic for B + A
            pass

Case 4: Default Implementation in Boilerplate Code

If neither A nor B define specific implementations, the default implementation in the boilerplate code is used.

Example:

def __add__(self, other):
    return NotImplemented  # Trigger radd() method

def __radd__(self, other):
    return other + self  # Default implementation

Case 5: Subclass Relationship

If B is a subclass of A (e.g., B <: A), Python will first try B.__radd__ before A.__add__. This is because B might have more specific logic for handling instances of A.

Real-World Example

Suppose you want to define a custom numeric type called MyIntegral. You might implement the __add__ method to handle operations with other MyIntegral instances and return NotImplemented for operations with other types.

class MyIntegral:
    def __add__(self, other):
        if isinstance(other, MyIntegral):
            # Custom logic for MyIntegral + MyIntegral
            pass
        else:
            return NotImplemented

When you then try to add a MyIntegral instance with a Complex instance, the boilerplate code will trigger Complex.__radd__ and the default implementation will be used.

Potential Applications

Mixed-type operations allow you to define custom numeric types that can interact with built-in numeric types like Complex. This can be useful in areas like numerical simulation, physics, or financial modeling.


Topic 1: Operator Overloading

  • Concept: Python allows you to define custom operators for your objects, such as +, -, or *.

  • Simplified Explanation: Imagine a magical calculator that understands your own operations, like "combine" instead of "add."

Topic 2: Forward and Reverse Operators

  • Concept: When you overload an operator, you need to define two versions: a forward operator (e.g., __add__) and a reverse operator (e.g., __radd__).

  • Simplified Explanation: The forward operator is called when your object is on the left side of the operation (e.g., a + b) and the reverse operator is called when your object is on the right side (e.g., b + a).

Topic 3: Helper Function for Operators

  • Concept: The _operator_fallbacks helper function in Python's fractions module automatically generates both the forward and reverse operators for you.

  • Simplified Explanation: It's like a magic wand that saves you from writing redundant code.

Code Snippet:

def _operator_fallbacks(monomorphic_operator, fallback_operator):
    def forward(a, b):
        # Forward version
        ...

    def reverse(b, a):
        # Reverse version
        ...

    return forward, reverse

Real World Implementation:

The following code shows how to overload the + and - operators for a Fraction object:

class Fraction:
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator

    def __add__(self, other):
        # Forward version
        return Fraction(self.numerator * other.denominator +
                        other.numerator * self.denominator,
                        self.denominator * other.denominator)

    def __radd__(self, other):
        # Reverse version
        return Fraction(other.numerator * self.denominator +
                        self.numerator * other.denominator,
                        other.denominator * self.denominator)

    def __sub__(self, other):
        # Forward version
        return Fraction(self.numerator * other.denominator -
                        other.numerator * self.denominator,
                        self.denominator * other.denominator)

    def __rsub__(self, other):
        # Reverse version
        return Fraction(other.numerator * self.denominator -
                        self.numerator * other.denominator,
                        other.denominator * self.denominator)

Potential Applications:

  • Simplifying mathematical operations with custom types

  • Creating specialized calculators for specific domains (e.g., financial calculations)

  • Data analysis and modeling with custom data structures