Skip to content

Commit 3eaf70d

Browse files
authoredSep 7, 2022
GH-96465: Cache hashes for Fraction instances (GH-96483)
1 parent 0cd992c commit 3eaf70d

File tree

2 files changed

+36
-30
lines changed

2 files changed

+36
-30
lines changed
 

‎Lib/fractions.py

+35-30
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""Fraction, infinite-precision, rational numbers."""
55

66
from decimal import Decimal
7+
import functools
78
import math
89
import numbers
910
import operator
@@ -20,6 +21,39 @@
2021
# _PyHASH_MODULUS.
2122
_PyHASH_INF = sys.hash_info.inf
2223

24+
@functools.lru_cache(maxsize = 1 << 14)
25+
def _hash_algorithm(numerator, denominator):
26+
27+
# To make sure that the hash of a Fraction agrees with the hash
28+
# of a numerically equal integer, float or Decimal instance, we
29+
# follow the rules for numeric hashes outlined in the
30+
# documentation. (See library docs, 'Built-in Types').
31+
32+
try:
33+
dinv = pow(denominator, -1, _PyHASH_MODULUS)
34+
except ValueError:
35+
# ValueError means there is no modular inverse.
36+
hash_ = _PyHASH_INF
37+
else:
38+
# The general algorithm now specifies that the absolute value of
39+
# the hash is
40+
# (|N| * dinv) % P
41+
# where N is self._numerator and P is _PyHASH_MODULUS. That's
42+
# optimized here in two ways: first, for a non-negative int i,
43+
# hash(i) == i % P, but the int hash implementation doesn't need
44+
# to divide, and is faster than doing % P explicitly. So we do
45+
# hash(|N| * dinv)
46+
# instead. Second, N is unbounded, so its product with dinv may
47+
# be arbitrarily expensive to compute. The final answer is the
48+
# same if we use the bounded |N| % P instead, which can again
49+
# be done with an int hash() call. If 0 <= i < P, hash(i) == i,
50+
# so this nested hash() call wastes a bit of time making a
51+
# redundant copy when |N| < P, but can save an arbitrarily large
52+
# amount of computation for large |N|.
53+
hash_ = hash(hash(abs(numerator)) * dinv)
54+
result = hash_ if numerator >= 0 else -hash_
55+
return -2 if result == -1 else result
56+
2357
_RATIONAL_FORMAT = re.compile(r"""
2458
\A\s* # optional whitespace at the start,
2559
(?P<sign>[-+]?) # an optional sign, then
@@ -646,36 +680,7 @@ def __round__(self, ndigits=None):
646680

647681
def __hash__(self):
648682
"""hash(self)"""
649-
650-
# To make sure that the hash of a Fraction agrees with the hash
651-
# of a numerically equal integer, float or Decimal instance, we
652-
# follow the rules for numeric hashes outlined in the
653-
# documentation. (See library docs, 'Built-in Types').
654-
655-
try:
656-
dinv = pow(self._denominator, -1, _PyHASH_MODULUS)
657-
except ValueError:
658-
# ValueError means there is no modular inverse.
659-
hash_ = _PyHASH_INF
660-
else:
661-
# The general algorithm now specifies that the absolute value of
662-
# the hash is
663-
# (|N| * dinv) % P
664-
# where N is self._numerator and P is _PyHASH_MODULUS. That's
665-
# optimized here in two ways: first, for a non-negative int i,
666-
# hash(i) == i % P, but the int hash implementation doesn't need
667-
# to divide, and is faster than doing % P explicitly. So we do
668-
# hash(|N| * dinv)
669-
# instead. Second, N is unbounded, so its product with dinv may
670-
# be arbitrarily expensive to compute. The final answer is the
671-
# same if we use the bounded |N| % P instead, which can again
672-
# be done with an int hash() call. If 0 <= i < P, hash(i) == i,
673-
# so this nested hash() call wastes a bit of time making a
674-
# redundant copy when |N| < P, but can save an arbitrarily large
675-
# amount of computation for large |N|.
676-
hash_ = hash(hash(abs(self._numerator)) * dinv)
677-
result = hash_ if self._numerator >= 0 else -hash_
678-
return -2 if result == -1 else result
683+
return _hash_algorithm(self._numerator, self._denominator)
679684

680685
def __eq__(a, b):
681686
"""a == b"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fraction hashes are now cached.

0 commit comments

Comments
 (0)