I am implementing a dataclass that allows adding dataclasses of the same type, like this:
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True)
class Example:
main_data: tuple[Any, ...] = ()
extra_data: object = None
def __add__(self, other: Example | object) -> Example:
if isinstance(other, Example) and self.extra_data == other.extra_data:
return Example(self.main_data + other.main_data, self.extra_data)
else:
return NotImplemented
The problem with this approach is that in addition to being slow, if you do Example((), []) + Example((), []), then it silently discards the second list. The only solution I see it to do self.extra_data is other.extra_data.
However, because extra_data can be anything, it might be an immutable constant, making the expression similar to 0 is 0. Obviously, a = 0; b = 0; a is b is undefined and may change in minor versions without warning. But what about a = 0; b = a; a is b? This seems equivalent to 0 is 0, which causes a SyntaxWarning. If this could be False, then my object could break if I repeatedly added it to itself.
I wasn’t able to find a definitive answer in the specification. The Python Language Reference 3.1. Data model § Objects, values and types states:
Types affect almost all aspects of object behavior. Even the importance of object identity is affected in some sense: for immutable types, operations that compute new values may actually return a reference to any existing object with the same type and value, while for mutable objects this is not allowed. For example, after
a = 1; b = 1, a and b may or may not refer to the same object with the value one, depending on the implementation. This is becauseintis an immutable type, so the reference to1can be reused. This behaviour depends on the implementation used, so should not be relied upon, but is something to be aware of when making use of object identity tests.
It doesn’t define exactly what “operations that compute new values” are. This might actually create a problem for some implementations’ optimizations. For example, say func(a) is called in a loop from 0-255, and JIT optimizes it to use a register that holds the value of a instead of the actual object (assuming the function only performs arithmetic operations on a). If you call id(a) inside the function, is it now required to be the same ID as the other one, or can the interpreter create a new integer of the same value?