尚未翻译
本页面尚未翻译成中文,因此显示英文原文。 帮助翻译
numbers Module Complexity¶
The numbers module defines the numeric tower as abstract base classes: Number, then
Complex, Real, Rational and Integral, each a subclass of the one before. It does two
jobs. Its ABCs answer isinstance() and issubclass() for any numeric type, through inheritance
or register(), never by looking for methods. And they carry mixin methods: a Real subclass
gets divmod(), complex() and .real for free, each built from the abstract methods it
supplies.
Mixins are priced in calls to the abstract methods a subclass supplies, each counted as O(1).
For the runtime checks, L is the length of the checked class's MRO, and S and R are the
subclasses and the registered classes a check can reach from the ABC, counted transitively
through both. register(cls) is priced for a cls that is not itself an ABC.
d is the digits in a rational's numerator and denominator.
Complexity Reference¶
Runtime checks¶
| Operation | Time | Space | Notes |
|---|---|---|---|
isinstance(x, numbers.Real), issubclass(cls, numbers.Integral) and the other ABCs, on a class already checked |
O(1) | O(1) | Both outcomes are cached per ABC, keyed by class; a register() that adds a new relationship, on any ABC anywhere, stales every negative cache, so the next negative answer from each ABC is walked for again |
| First check of a class against an ABC | O(L·(1 + R + S)) | O(R + S) | No structural hook: the MRO, then every registered class, then every subclass recursively, each of them rescanning the MRO, stopping at a match |
Number.register(cls), and the same on each ABC |
O(L·(1 + R + S)) | O(R + S) | Two issubclass() checks, then one weak-set entry; a new relationship bumps the token that stales every negative cache, and a class that already is a subclass is a no-op |
Number¶
| Operation | Time | Space | Notes |
|---|---|---|---|
numbers.Number |
O(1) | O(1) | The root; no methods, abstract or mixin. It sets __hash__ = None, so a subclass is unhashable until it defines __hash__. Decimal is registered here and on no lower ABC |
Complex¶
| Operation | Time | Space | Notes |
|---|---|---|---|
Complex.real, Complex.imag, Complex.conjugate(), __complex__, __add__, __mul__, __truediv__, __pow__, their reflected forms, __neg__, __pos__, __abs__, __eq__ |
O(1) | O(1) | Abstract: the subclass supplies them. complex is registered here |
bool(z) (Complex.__bool__()) |
O(1) | O(1) | One self != 0, which is one __eq__ unless the subclass defines __ne__ |
z - w (Complex.__sub__()), w - z (Complex.__rsub__()) |
O(1) | O(1) | One negation and one addition: self + -other and -self + other |
Real¶
| Operation | Time | Space | Notes |
|---|---|---|---|
__float__, __trunc__, __floor__, __ceil__, __round__, __floordiv__, __mod__, their reflected forms, __lt__, __le__ |
O(1) | O(1) | Abstract, on top of Complex's arithmetic. float is registered here |
Real.real, Real.conjugate() |
O(1) | O(1) | One +self |
Real.imag |
O(1) | O(1) | The constant 0 |
complex(x) (Real.__complex__()) |
O(1) | O(1) | One __float__ |
divmod(x, y) (Real.__divmod__(), Real.__rdivmod__()) |
O(1) | O(1) | One // and one %; override it where both come from one computation |
Rational¶
| Operation | Time | Space | Notes |
|---|---|---|---|
Rational.numerator, Rational.denominator |
O(1) | O(1) | Abstract properties, expected in lowest terms with a positive denominator |
float(q) (Rational.__float__()) |
O(d) | O(d) | One read of each term, then an integer true division of the whole numerator by the whole denominator; Fraction inherits it |
Integral¶
| Operation | Time | Space | Notes |
|---|---|---|---|
__int__, __pow__(exponent, modulus=None), __lshift__, __rshift__, __and__, __xor__, __or__, their reflected forms, __invert__ |
O(1) | O(1) | Abstract, on top of Real's. int is registered here, which covers bool |
operator.index(i) (Integral.__index__()) |
O(1) | O(1) | One __int__ |
float(i) (Integral.__float__()) |
O(1) | O(1) | One __int__, then float() of the result |
Integral.numerator |
O(1) | O(1) | One +self |
Integral.denominator |
O(1) | O(1) | The constant 1 |
Checking Against the Tower¶
None of the ABCs looks for methods. A class is numeric here because it inherits from one of
them or is registered with one: int on Integral, float on Real, complex on Complex,
and Decimal on Number alone, so a Decimal is not a Real. The first check of a class walks
the ABC's registry and subclasses, stopping at a match, and caches the answer; later checks of
the same class are O(1) cache lookups, whether the answer was yes or no, until a new
registration stales the negative answers.
import numbers
from decimal import Decimal
from fractions import Fraction
assert isinstance(42, numbers.Integral) # O(L·(1 + R + S)) the first time, then O(1)
assert isinstance(True, numbers.Integral) # bool inherits int's registration
assert isinstance(3.14, numbers.Real) and not isinstance(3.14, numbers.Rational)
assert isinstance(2 + 3j, numbers.Complex) and not isinstance(2 + 3j, numbers.Real)
assert isinstance(Fraction(1, 3), numbers.Rational) # a subclass, not a registration
# Decimal is registered with Number and nothing below it
assert isinstance(Decimal('1.5'), numbers.Number)
assert not isinstance(Decimal('1.5'), numbers.Real)
# A miss is cached too
assert not isinstance('42', numbers.Number) # walks the tower once
assert not isinstance('43', numbers.Number) # O(1) - same class, negative cache
Dispatching on the Tower¶
Each ABC is a subclass of the one above it, so a cascade must test the narrowest first. Every branch is a cached check, so the cascade is O(1) per call once each class has been seen.
import numbers
from decimal import Decimal
from fractions import Fraction
def kind(value):
if isinstance(value, numbers.Integral): # O(1) once the class is cached
return 'integral'
if isinstance(value, numbers.Rational):
return 'rational'
if isinstance(value, numbers.Real):
return 'real'
if isinstance(value, numbers.Complex):
return 'complex'
if isinstance(value, numbers.Number):
return 'number'
return 'not a number'
assert kind(7) == 'integral'
assert kind(Fraction(3, 4)) == 'rational'
assert kind(2.5) == 'real'
assert kind(2 + 3j) == 'complex'
assert kind(Decimal('2.5')) == 'number'
assert kind('7') == 'not a number'
Registering a Type¶
register() makes a class a virtual subclass: it passes the check but inherits no mixins. A
registration that adds a new relationship bumps the ABC cache token, which stales the negative
cache of every ABC in the process, so each later miss walks its tree again. Register once, at
import time.
import numbers
from abc import get_cache_token
class Meters(float):
pass
class Opaque:
pass
assert isinstance(Meters(1.0), numbers.Real) # a float subclass needs nothing
token = get_cache_token()
numbers.Real.register(Opaque) # O(L·(1 + R + S)) - and every negative cache goes stale
assert get_cache_token() != token
assert isinstance(Opaque(), numbers.Real)
assert not hasattr(Opaque, 'conjugate') # registered, so no mixins
token = get_cache_token()
numbers.Real.register(Meters) # already a subclass: a no-op
assert get_cache_token() == token
Writing a Numeric Type¶
Subclass the narrowest ABC that fits and implement its abstract methods; instantiating a subclass
that leaves one out raises TypeError. Number has none, so any subclass of it can be built,
but it inherits __hash__ = None.
import numbers
class Scalar(numbers.Number):
def __init__(self, value):
self.value = value
def __add__(self, other):
if isinstance(other, numbers.Real): # O(1) after the first check of each class
return Scalar(self.value + other)
return NotImplemented
total = Scalar(5) + 3
assert isinstance(total, numbers.Number) and total.value == 8
try:
hash(total)
except TypeError as error:
assert 'unhashable' in str(error)
else:
raise AssertionError('a Number subclass without __hash__ was hashable')
class Partial(numbers.Real):
def __float__(self):
return 0.0
try:
Partial()
except TypeError as error:
assert 'abstract' in str(error)
else:
raise AssertionError('a Real with abstract methods left was instantiated')
The Rational Float Conversion¶
Rational.__float__() divides the whole numerator by the whole denominator, so it costs their
length rather than a constant. Fraction does not override it: float() of a fraction with
large terms is linear in its digits.
import numbers
from fractions import Fraction
assert Fraction.__float__ is numbers.Rational.__float__
third = Fraction(1, 3)
assert float(third) == 1 / 3 # O(d)
huge = Fraction(10**400 + 1, 3 * 10**400)
assert float(huge) == 1 / 3 # O(d) - both terms are divided whole
Common Patterns¶
Validating Arguments¶
A check per value is O(1) once each class has been seen, so validating n values of a few types is O(n).
import numbers
from fractions import Fraction
def mean(values):
for value in values: # O(n)
if not isinstance(value, numbers.Real): # O(1) per value after its class's first check
raise TypeError(f'expected a real number, got {type(value).__name__}')
return sum(values) / len(values)
assert mean([1, 2, 3, 4, 5]) == 3
assert mean([Fraction(1, 2), Fraction(3, 2)]) == 1
try:
mean([1, 2, 'three'])
except TypeError as error:
assert 'str' in str(error)
else:
raise AssertionError('a string passed as a real number')
Performance Best Practices¶
✅ Do:
- Call
isinstance()directly rather than caching its answer yourself; the ABC already caches both answers per class, so the first-check walk is paid once per class either way, until a new registration - Register classes once, at import time; each new registration stales every ABC's negative cache
- Override
__divmod__on aRealsubclass that computes quotient and remainder together; the mixin makes one//and one%call
❌ Avoid:
register()inside a loop or a hot path: each new relationship makes every following miss walk the registry and subclass tree againfloat()of aFractionor otherRationalwith huge terms in a hot loop; the inherited conversion divides the whole numerator by the whole denominator
Version Notes¶
- All Python 3:
Decimalis registered withNumberonly, soisinstance(d, numbers.Real)isFalsefor aDecimal - All Python 3: the negative cache of every ABC is invalidated by any
register()call that adds a new relationship, through the tokenabc.get_cache_token()exposes
Related Modules¶
- fractions -
Fraction, the standard library'sRationalsubclass - decimal -
Decimal, registered as aNumberbut not aReal - abc - the metaclass, its caches and
register() - collections.abc - the container ABCs, checked by the same machinery