Skip to content

Commit 16cb058

Browse files
authored
Merge pull request #667 from Mathics3/atom-caching
Remove @lru_cache in favor of homegrown cache
2 parents 5a44e82 + dd06009 commit 16cb058

File tree

1 file changed

+136
-74
lines changed

1 file changed

+136
-74
lines changed

mathics/core/atoms.py

Lines changed: 136 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,36 @@ class Number(Atom, ImmutableValueMixin, NumericOperators):
3737
being: Integer, Rational, Real, Complex.
3838
"""
3939

40+
def __getnewargs__(self):
41+
"""
42+
__getnewargs__ is used in pickle loading to ensure __new__ is
43+
called with the right value.
44+
45+
Most of the time a number takes one argument - its value
46+
When there is a kind of number, like Rational, or Complex,
47+
that has more than one argument, it should define this method
48+
accordingly.
49+
"""
50+
return (self._value,)
51+
4052
def __str__(self) -> str:
4153
return str(self.value)
4254

43-
def is_numeric(self, evaluation=None) -> bool:
44-
return True
55+
# FIXME: can we refactor or subclass objects to remove pattern_sort?
56+
def get_sort_key(self, pattern_sort=False) -> tuple:
57+
"""
58+
get_sort_key is used in Expression evaluation to determine how to
59+
order its list of elements. The tuple returned contains
60+
rank orders for different level as is found in say
61+
Python version release numberso or Python package version numbers.
62+
63+
This is the default routine for Number. Subclasses of Number like
64+
Complex may need to define this differently.
65+
"""
66+
if pattern_sort:
67+
return super().get_sort_key(True)
68+
else:
69+
return (0, 0, self._value, 0, 1)
4570

4671
@property
4772
def is_literal(self) -> bool:
@@ -51,8 +76,25 @@ def is_literal(self) -> bool:
5176
"""
5277
return True
5378

79+
def is_numeric(self, evaluation=None) -> bool:
80+
return True
81+
82+
def to_mpmath(self):
83+
"""
84+
Convert self._value to an mnpath number.
85+
86+
This is the default implementation for Number.
87+
There are kinds of numbers, like Rational, or Complex, that
88+
need to work differently than this default, and they will
89+
change the implementation accordingly.
90+
"""
91+
return mpmath.mpf(self._value)
92+
5493
@property
55-
def value(self) -> bool:
94+
def value(self):
95+
"""
96+
Returns this number's value.
97+
"""
5698
return self._value
5799

58100

@@ -101,27 +143,35 @@ class Integer(Number):
101143
value: int
102144
class_head_name = "System`Integer"
103145

104-
# Collection of Integers defined so far.
146+
# Dictionary of Integer constant values defined so far.
105147
# We use this for object uniqueness.
148+
# The key is the Integer's Python `int` value, and the
149+
# dictionary's value is the corresponding Mathics Integer object.
106150
_integers = {}
107151

108-
# We use __new__ here to unsure that two Integer's that have the same value
109-
# return the same object.
152+
# We use __new__ here to ensure that two Integer's that have the same value
153+
# return the same object, and to set an object hash value.
154+
# Consider also @lru_cache, and mechanisms for limiting and
155+
# clearing the cache and the object store which might be useful in implementing
156+
# Builtin Share[].
110157
def __new__(cls, value) -> "Integer":
111158

112159
n = int(value)
113-
key = (cls, n)
114-
self = cls._integers.get(key)
160+
self = cls._integers.get(value)
115161
if self is None:
116162
self = super().__new__(cls)
117163
self._value = n
118164

119165
# Cache object so we don't allocate again.
120-
self._integers[key] = self
166+
self._integers[value] = self
121167

122168
# Set a value for self.__hash__() once so that every time
123-
# it is used this is fast.
124-
self.hash = hash(key)
169+
# it is used this is fast. Note that in contrast to the
170+
# cached object key, the hash key needs to be unique across all
171+
# Python objects, so we include the class in the
172+
# event that different objects have the same Python value
173+
self.hash = hash((cls, n))
174+
125175
return self
126176

127177
def __eq__(self, other) -> bool:
@@ -145,6 +195,8 @@ def __gt__(self, other) -> bool:
145195
else super().__gt__(other)
146196
)
147197

198+
# __hash__ is defined so that we can store Number-derived objects
199+
# in a set or dictionary.
148200
def __hash__(self):
149201
return self.hash
150202

@@ -188,9 +240,6 @@ def make_boxes(self, form) -> "String":
188240
def to_sympy(self, **kwargs):
189241
return sympy.Integer(self._value)
190242

191-
def to_mpmath(self):
192-
return mpmath.mpf(self._value)
193-
194243
def to_python(self, *args, **kwargs):
195244
return self.value
196245

@@ -211,21 +260,12 @@ def sameQ(self, other) -> bool:
211260
"""Mathics SameQ"""
212261
return isinstance(other, Integer) and self._value == other.value
213262

214-
def get_sort_key(self, pattern_sort=False) -> tuple:
215-
if pattern_sort:
216-
return super().get_sort_key(True)
217-
else:
218-
return (0, 0, self._value, 0, 1)
219-
220263
def do_copy(self) -> "Integer":
221264
return Integer(self._value)
222265

223266
def user_hash(self, update):
224267
update(b"System`Integer>" + str(self._value).encode("utf8"))
225268

226-
def __getnewargs__(self):
227-
return (self._value,)
228-
229269
def __neg__(self) -> "Integer":
230270
return Integer(-self._value)
231271

@@ -304,11 +344,6 @@ def __ne__(self, other) -> bool:
304344
def atom_to_boxes(self, f, evaluation):
305345
return self.make_boxes(f.get_name())
306346

307-
def get_sort_key(self, pattern_sort=False) -> tuple:
308-
if pattern_sort:
309-
return super().get_sort_key(True)
310-
return (0, 0, self._value, 0, 1)
311-
312347
def is_nan(self, d=None) -> bool:
313348
return isinstance(self.value, sympy.core.numbers.NaN)
314349

@@ -326,32 +361,36 @@ class MachineReal(Real):
326361
Stored internally as a python float.
327362
"""
328363

329-
# Collection of MachineReals defined so far.
364+
# Dictionary of MachineReal constant values defined so far.
330365
# We use this for object uniqueness.
366+
# The key is the MachineReal's Python `float` value, and the
367+
# dictionary's value is the corresponding Mathics MachineReal object.
331368
_machine_reals = {}
332369

333370
def __new__(cls, value) -> "MachineReal":
334371
n = float(value)
335372
if math.isinf(n) or math.isnan(n):
336373
raise OverflowError
337374

338-
key = (cls, n)
339-
self = cls._machine_reals.get(key)
375+
self = cls._machine_reals.get(n)
340376
if self is None:
341377
self = Number.__new__(cls)
342378
self._value = n
343379

344380
# Cache object so we don't allocate again.
345-
self._machine_reals[key] = self
381+
self._machine_reals[n] = self
346382

347383
# Set a value for self.__hash__() once so that every time
348-
# it is used this is fast.
349-
self.hash = hash(key)
350-
return self
384+
# it is used this is fast. Note that in contrast to the
385+
# cached object key, the hash key needs to be unique across all
386+
# Python objects, so we include the class in the
387+
# event that different objects have the same Python value
388+
self.hash = hash((cls, n))
351389

352-
def __getnewargs__(self):
353-
return (self.value,)
390+
return self
354391

392+
# __hash__ is defined so that we can store Number-derived objects
393+
# in a set or dictionary.
355394
def __hash__(self):
356395
return self.hash
357396

@@ -424,13 +463,6 @@ def to_python(self, *args, **kwargs) -> float:
424463
def to_sympy(self, *args, **kwargs):
425464
return sympy.Float(self.value)
426465

427-
def to_mpmath(self):
428-
return mpmath.mpf(self.value)
429-
430-
@property
431-
def value(self) -> bool:
432-
return self._value
433-
434466

435467
MachineReal0 = MachineReal(0)
436468

@@ -444,32 +476,35 @@ class PrecisionReal(Real):
444476
Note: Plays nicely with the mpmath.mpf (float) type.
445477
"""
446478

447-
# Collection of MachineReals defined so far.
479+
# Dictionary of PrecisionReal constant values defined so far.
448480
# We use this for object uniqueness.
481+
# The key is the PrecisionReal's sympy.Float, and the
482+
# dictionary's value is the corresponding Mathics PrecisionReal object.
449483
_precision_reals = {}
450484

451485
value: sympy.Float
452486

453487
def __new__(cls, value) -> "PrecisionReal":
454488
n = sympy.Float(value)
455-
key = (cls, n)
456-
self = cls._precision_reals.get(key)
489+
self = cls._precision_reals.get(n)
457490
if self is None:
458491
self = Number.__new__(cls)
459492
self._value = n
460493

461494
# Cache object so we don't allocate again.
462-
self._precision_reals[key] = self
495+
self._precision_reals[n] = self
463496

464497
# Set a value for self.__hash__() once so that every time
465-
# it is used this is fast.
466-
self.hash = hash(key)
498+
# it is used this is fast. Note that in contrast to the
499+
# cached object key, the hash key needs to be unique across all
500+
# Python objects, so we include the class in the
501+
# event that different objects have the same Python value.
502+
self.hash = hash((cls, n))
467503

468504
return self
469505

470-
def __getnewargs__(self):
471-
return (self.value,)
472-
506+
# __hash__ is defined so that we can store Number-derived objects
507+
# in a set or dictionary.
473508
def __hash__(self):
474509
return self.hash
475510

@@ -527,20 +562,16 @@ def to_python(self, *args, **kwargs):
527562
def to_sympy(self, *args, **kwargs):
528563
return self.value
529564

530-
def to_mpmath(self):
531-
return mpmath.mpf(self.value)
532-
533-
@property
534-
def value(self):
535-
return self._value
536-
537565

538566
class ByteArrayAtom(Atom, ImmutableValueMixin):
539567
value: str
540568
class_head_name = "System`ByteArrayAtom"
541569

542-
# We use __new__ here to unsure that two ByteArrayAtom's that have the same value
543-
# return the same object.
570+
# We use __new__ here to ensure that two ByteArrayAtom's that have the same value
571+
# return the same object, and to set an object hash value.
572+
# Consider also @lru_cache, and mechanisms for limiting and
573+
# clearing the cache and the object store which might be useful in implementing
574+
# Builtin Share[].
544575
def __new__(cls, value):
545576
self = super().__new__(cls)
546577
if type(value) in (bytes, bytearray):
@@ -629,8 +660,18 @@ class Complex(Number):
629660
real: Type[Number]
630661
imag: Type[Number]
631662

663+
# Dictionary of Complex constant values defined so far.
664+
# We use this for object uniqueness.
665+
# The key is the Complex value's real and imaginary parts as a tuple,
666+
# dictionary's value is the corresponding Mathics Complex object.
667+
_complex_numbers = {}
668+
669+
# We use __new__ here to ensure that two Integer's that have the same value
670+
# return the same object, and to set an object hash value.
671+
# Consider also @lru_cache, and mechanisms for limiting and
672+
# clearing the cache and the object store which might be useful in implementing
673+
# Builtin Share[].
632674
def __new__(cls, real, imag):
633-
self = super().__new__(cls)
634675
if isinstance(real, Complex) or not isinstance(real, Number):
635676
raise ValueError("Argument 'real' must be a Real number.")
636677
if imag is SymbolInfinity:
@@ -646,14 +687,26 @@ def __new__(cls, real, imag):
646687
if isinstance(imag, MachineReal) and not isinstance(real, MachineReal):
647688
real = real.round()
648689

649-
self.real = real
650-
self.imag = imag
690+
value = (real, imag)
691+
self = cls._complex_numbers.get(value)
692+
if self is None:
651693

652-
# Set a value for self.__hash__() once so that every time
653-
# it is used this is fast.
654-
self.hash = hash(("Complex", real, imag))
694+
self = super().__new__(cls)
695+
self.real = real
696+
self.imag = imag
697+
698+
self._value = value
699+
700+
# Cache object so we don't allocate again.
701+
self._complex_numbers[value] = self
702+
703+
# Set a value for self.__hash__() once so that every time
704+
# it is used this is fast. Note that in contrast to the
705+
# cached object key, the hash key needs to be unique across all
706+
# Python objects, so we include the class in the
707+
# event that different objects have the same Python value
708+
self.hash = hash((cls, value))
655709

656-
self._value = (self.real, self.imag)
657710
return self
658711

659712
def __hash__(self):
@@ -684,7 +737,14 @@ def default_format(self, evaluation, form) -> str:
684737
self.imag.default_format(evaluation, form),
685738
)
686739

740+
# Note we can
687741
def get_sort_key(self, pattern_sort=False) -> tuple:
742+
"""
743+
get_sort_key is used in Expression evaluation to determine how to
744+
order its list of elements. The tuple returned contains
745+
rank orders for different level as is found in say
746+
Python version release numberso or Python package version numbers.
747+
"""
688748
if pattern_sort:
689749
return super().get_sort_key(True)
690750
else:
@@ -775,8 +835,11 @@ class Rational(Number):
775835
# Collection of integers defined so far.
776836
_rationals = {}
777837

778-
# We use __new__ here to unsure that two Rationals's that have the same value
779-
# return the same object.
838+
# We use __new__ here to ensure that two Rationals's that have the same value
839+
# return the same object, and to set an object hash value.
840+
# Consider also @lru_cache, and mechanisms for limiting and
841+
# clearing the cache and the object store which might be useful in implementing
842+
# Builtin Share[].
780843
def __new__(cls, numerator, denominator=1) -> "Rational":
781844

782845
value = sympy.Rational(numerator, denominator)
@@ -795,6 +858,8 @@ def __new__(cls, numerator, denominator=1) -> "Rational":
795858
self.hash = hash(key)
796859
return self
797860

861+
# __hash__ is defined so that we can store Number-derived objects
862+
# in a set or dictionary.
798863
def __hash__(self):
799864
return self.hash
800865

@@ -806,9 +871,6 @@ def atom_to_boxes(self, f, evaluation):
806871
def to_sympy(self, **kwargs):
807872
return self.value
808873

809-
def to_mpmath(self):
810-
return mpmath.mpf(self.value)
811-
812874
def to_python(self, *args, **kwargs) -> float:
813875
return float(self.value)
814876

0 commit comments

Comments
 (0)