Overloading an assignment operator in the way you are suggesting is not very pythonic.
The assignment operator in Python is meant to refer to the same variable, and not to create a copy.
So, such an object may not behave as expected in some situations, such as the results of hash(object.attr)
or from using the pickle
module on such an object.
However, if you are up for some dark magic...
To be clear, I am providing this answer just to show Python does offer the ability to do such things.
One approach would be to use the __getattribute__()
function to create a copy of any attribute when it is accessed.
import copy
class MyClass:
def __init__(self):
self.attr = ["foo", "bar"]
def __getattribute__(self, name):
"""Access an attribute of this class"""
attr = object.__getattribute__(self, name)
return copy.copy(attr)
a = MyClass()
b = a
print(b.attr is a.attr) # prints False
print(b.attr == a.attr) # prints True
More information on __getattribute__()
can be found here: https://docs.python.org/3/reference/datamodel.html
Alternatively, property()
could be used to do this only for a single attribute on a class.
class MyClass:
def __init__(self):
self.__attr = ["foo", "bar"]
@property
def attr(self):
"""Get the attribute."""
# Copy could be used instead
# This works too, if we assume the attribute supports slicing
return self.__attr[:]
@attr.setter
def attr(self, value):
"""Setting the attribute."""
# Makes assignment work on this attribute
# a.attr = []
# a.attr is a.attr (False)
self.__attr = value
a = MyClass()
b = a
print(b.attr is a.attr) # prints False
print(b.attr == a.attr) # prints True
These solutions work for most object types. But this will actually fail under some situations. This is because some strings and some integers will be marked as having the same identity.
Such as 4 is 2*2
which will be True
, but
a = -6
b = -6
print(a is b) # Will print False
This is called "interning" and is discussed briefly in the sys
module: https://docs.python.org/3/library/sys.html?highlight=intern#sys.intern
According to Real Python: https://realpython.com/lessons/intern-objects/
in CPython 3.7, integers between -5 and 256 are interned, as with
strings that are less than 20 characters and contain only ASCII
letters, digits, or underscores.
For example, if attr = 5
or attr = 'foo'
then both approaches above fail.
a = MyClass()
a.attr = 5
b = a
print(b.attr is a.attr) # prints True
print(b.attr == a.attr) # prints True
This can be circumvented by wrapping these types in a subclass. According to RealPython, only strings and some integers need to be modified. The catch with this approach would be a type comparison would fail:
print(type(b.attr) is type(a.attr)) # prints False
So if you wanted to wrap the objects, making sure the is
operation always fails, you could do this:
import copy
from collections import UserString # Because of course Python has this built in
class UserInt(int): pass
class MyClass:
def __init__(self, attr = ["foo", "bar"]):
self.attr = attr
def __getattribute__(self, name):
"""Access an attribute of this class"""
attr = object.__getattribute__(self, name)
if isinstance(attr, int) and -5 <= attr <= 256:
return UserInt(attr) # Prevent a.attr is b.attr if this is an int
elif isinstance(attr, str):
return UserString(attr) # Prevent a.attr is b.attr for strings
#else
return copy.copy(attr)
a = MyClass()
b = a
print(b.attr is a.attr) # prints False
print(b.attr == a.attr) # prints True
a.attr = 7
b = a
print(b.attr is a.attr) # prints False
print(b.attr == a.attr) # prints True
a.attr = "Hello"
print(b.attr is a.attr) # prints False
print(b.attr == a.attr) # prints True