Python memory aliasing (simple)

Question:

Sorry I uploaded an image so I delete it.

Asked By: Andy Junghyun Kim

||

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__,

Answered By: jedwards

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.

Answered By: George Hanna

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.

Answered By: PM 2Ring
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.