Python memory aliasing (simple)
Question:
Sorry I uploaded an image so I delete it.
Answers:
Defining equivalence (==
) for objects is pretty simple by implementing __eq__
(docs linked):
class Contact:
def __init__(self, phone, name):
self.phone = phone
self.name = name
def __eq__(self, other):
return (self.phone == other.phone) and (self.name == other.name)
c1 = Contact('647-000-000', 'Andy')
print(c1 == Contact('647-000-000', 'Andy')) # True
Here, __eq__
is a method accepting one argument (the thing it’s being compared to) and should return either True
or False
(or something coercible to a boolean). This is a very simple and “flimsy” implementation as things like c1 == "string"
will throw an error, but it demonstrates the idea.
However, there is no magic method you can implement to define reference equality (is
). In general you should think of is
being used to test whether they’re the exact same object, and ==
being used to test whether the two objects are equivalent.
[1] Yes, it is nonetheless sort of possible with caching and either using metaclasses or __new__
,
Alright so basically Python has aliasing. And to check if 2 objects are an alias to each other you write
obj1 is obj2
If it returns True then they are an alias. Well, basically Python checks for the two objects id. The above statement is equivalent to:
id(obj1) == id(obj2)
id(object)
is used to evaluate the address of an object in the memory.
When your writing b is Contact('647-000-000', 'Andy')
b already got an address in memory but when you’re comparing it with Contact('647-000-000', 'Andy')
, Contact('647-000-000', 'Andy')
has a different address in memory.
I don’t understand why are you comparing the whole class instead of the attributes.
b.number == '647-000-000'
and b.name == 'Andy'
will solve your problem.
Each time you call Contact()
a new instance is created, even if you pass it identical args. How is Python to know that you want contacts with the same args to actually be the same object? In general, that would not be desirable. If you want two names for the same instance, just do a simple assignment, eg
c1 = Contact('647-000-000', 'Andy')
c2 = c1
If you really do want two (or more) calls to Contact()
with identical args to return the same object you can give the __new__
constructor a cache, eg functools.lru_cache
. Here’s a short demo.
from functools import lru_cache
class Contact:
@lru_cache(None)
def __new__(cls, *args):
return super().__new__(cls)
def __init__(self, phone, name):
self.phone = phone
self.name = name
def __str__(self):
return f'Contact({self.phone}, {self.name})'
c1 = Contact('647-000-000', 'Andy')
c2 = Contact('647-000-001', 'Amos')
c3 = Contact('647-000-000', 'Andy')
for c in (c1, c2, c3):
print(c, repr(c))
output
Contact(647-000-000, Andy) <__main__.Contact object at 0xb7285d4c>
Contact(647-000-001, Amos) <__main__.Contact object at 0xb7285dac>
Contact(647-000-000, Andy) <__main__.Contact object at 0xb7285d4c>
When you call Contact
, its __new__
method is called to construct the new instance object. That object is then passed to __init__
to get initialised. In most classes, the __new__
method isn’t defined, so the __new__
method of the parent class is called, normally that’s the default __new__
inherited from the object
base class.
In the above code we define __new__
and decorate it with lru_cache
. So when we call Contact()
the Contact
type and any other args get processed by lru_cache
, which maintains an invisible dictionary of all Contact
instances we create, keyed by the args passed to __new__
(including the the Contact
type). If that key is in the dict, the corresponding instance gets returned by __new__
. Otherwise, a new Contact
is allocated and added to the dict
. In either case, the instance is then passed to __init__
for initialization.
The above code is a proof of concept. I do not recommend doing this in real code. The invisible dict
maintained by lru_cache
keeps a reference to every contact you create, so they will not get deleted when they (appear to) go out of scope, even if you pass them to del
, until the program terminates. To force a contact to be deleted you need to clear it from the cache, you could do that with:
Contact.__new__.cache_clear()
but of course that clears the entire cache.
Sorry I uploaded an image so I delete it.
Defining equivalence (==
) for objects is pretty simple by implementing __eq__
(docs linked):
class Contact:
def __init__(self, phone, name):
self.phone = phone
self.name = name
def __eq__(self, other):
return (self.phone == other.phone) and (self.name == other.name)
c1 = Contact('647-000-000', 'Andy')
print(c1 == Contact('647-000-000', 'Andy')) # True
Here, __eq__
is a method accepting one argument (the thing it’s being compared to) and should return either True
or False
(or something coercible to a boolean). This is a very simple and “flimsy” implementation as things like c1 == "string"
will throw an error, but it demonstrates the idea.
However, there is no magic method you can implement to define reference equality (is
). In general you should think of is
being used to test whether they’re the exact same object, and ==
being used to test whether the two objects are equivalent.
[1] Yes, it is nonetheless sort of possible with caching and either using metaclasses or __new__
,
Alright so basically Python has aliasing. And to check if 2 objects are an alias to each other you write
obj1 is obj2
If it returns True then they are an alias. Well, basically Python checks for the two objects id. The above statement is equivalent to:
id(obj1) == id(obj2)
id(object)
is used to evaluate the address of an object in the memory.
When your writing b is Contact('647-000-000', 'Andy')
b already got an address in memory but when you’re comparing it with Contact('647-000-000', 'Andy')
, Contact('647-000-000', 'Andy')
has a different address in memory.
I don’t understand why are you comparing the whole class instead of the attributes.
b.number == '647-000-000'
and b.name == 'Andy'
will solve your problem.
Each time you call Contact()
a new instance is created, even if you pass it identical args. How is Python to know that you want contacts with the same args to actually be the same object? In general, that would not be desirable. If you want two names for the same instance, just do a simple assignment, eg
c1 = Contact('647-000-000', 'Andy')
c2 = c1
If you really do want two (or more) calls to Contact()
with identical args to return the same object you can give the __new__
constructor a cache, eg functools.lru_cache
. Here’s a short demo.
from functools import lru_cache
class Contact:
@lru_cache(None)
def __new__(cls, *args):
return super().__new__(cls)
def __init__(self, phone, name):
self.phone = phone
self.name = name
def __str__(self):
return f'Contact({self.phone}, {self.name})'
c1 = Contact('647-000-000', 'Andy')
c2 = Contact('647-000-001', 'Amos')
c3 = Contact('647-000-000', 'Andy')
for c in (c1, c2, c3):
print(c, repr(c))
output
Contact(647-000-000, Andy) <__main__.Contact object at 0xb7285d4c>
Contact(647-000-001, Amos) <__main__.Contact object at 0xb7285dac>
Contact(647-000-000, Andy) <__main__.Contact object at 0xb7285d4c>
When you call Contact
, its __new__
method is called to construct the new instance object. That object is then passed to __init__
to get initialised. In most classes, the __new__
method isn’t defined, so the __new__
method of the parent class is called, normally that’s the default __new__
inherited from the object
base class.
In the above code we define __new__
and decorate it with lru_cache
. So when we call Contact()
the Contact
type and any other args get processed by lru_cache
, which maintains an invisible dictionary of all Contact
instances we create, keyed by the args passed to __new__
(including the the Contact
type). If that key is in the dict, the corresponding instance gets returned by __new__
. Otherwise, a new Contact
is allocated and added to the dict
. In either case, the instance is then passed to __init__
for initialization.
The above code is a proof of concept. I do not recommend doing this in real code. The invisible dict
maintained by lru_cache
keeps a reference to every contact you create, so they will not get deleted when they (appear to) go out of scope, even if you pass them to del
, until the program terminates. To force a contact to be deleted you need to clear it from the cache, you could do that with:
Contact.__new__.cache_clear()
but of course that clears the entire cache.