13

If we have x = type(a) and x == y, does it necessarily imply that x is y?

Here is a counter-example, but it's a cheat:

>>> class BrokenEq(type):
...     def __eq__(cls, other):
...         return True
...     
>>> class A(metaclass=BrokenEq):
...     pass
... 
>>> a = A()
>>> x = type(a)
>>> x == A, x is A
(True, True)
>>> x == BrokenEq, x is BrokenEq
(True, False)

And I could not create a counterexample like this:

>>> A1 = type('A', (), {})
>>> A2 = type('A', (), {})
>>> a = A1()
>>> x = type(a)
>>> x == A1, x is A1
(True, True)
>>> x == A2, x is A2
(False, False)

To clarify my question - without overriding equality operators to do something insane, is it possible for a class to exist at two different memory locations or does the import system somehow prevent this?

If so, how can we demonstrate this behavior - for example, doing weird things with reload or __import__?

If not, is that guaranteed by the language or documented anywhere?


Epilogue:

# thing.py
class A:
    pass

Finally, this is what clarified the real behaviour for me (and it's supporting the claims in Blckknght answer)

>>> import sys
>>> from thing import A
>>> a = A()
>>> isinstance(a, A), type(a) == A, type(a) is A
(True, True, True)
>>> del sys.modules['thing']
>>> from thing import A
>>> isinstance(a, A), type(a) == A, type(a) is A
(False, False, False)

So, although code that uses importlib.reload could break type checking by class identity, it will also break isinstance anyway.

martineau
  • 119,623
  • 25
  • 170
  • 301
wim
  • 338,267
  • 99
  • 616
  • 750
  • 1
    Interesting question! I think they would have to be, simply because an object of any given type would presumably only be created in one consistent way. Can I cite something saying so? Not yet. – Tom Zych Nov 25 '15 at 20:17
  • Since everything is an object in python the classes are "just" instances of their metaclass - why couldn't we have other instances of the "same" class? However, I've failed in finding an example where importing a class or otherwise binding a name to the class doesn't just get another reference to the same object. – wim Nov 25 '15 at 20:28
  • Perhaps it's just one of those things that happens to be true in a given implementation, but isn't addressed in the language spec and isn't guaranteed. – Tom Zych Nov 25 '15 at 20:32
  • If you import the same class from different import paths, they will exist at two different memory locations, but they will not evaluate identical (i.e. x = type(a) and x == y --> x is y still holds). – DylanYoung Mar 15 '17 at 15:17

2 Answers2

6

No, there's no way to create two class objects that compare equal without being identical, except by messing around with metaclass __eq__ methods.

This behavior though is not something unique to classes. It's the default behavior for any object without an __eq__ method defined in its class. The behavior is inherited from object, which is the base class for all other (new-style) classes. It's only overridden for builtin types that have some other semantic for equality (e.g. container types which compare their contents) and for custom classes that define an __eq__ operator of their own.

As for getting two different refernces to the same class at different memory locations, that's not really possible due to Python's object semantics. The memory location of the object is its identity (in cpython at least). Another class with identical contents can exist somewhere else, but like in your A1 and A2 example, it's going to be seen as a different object by all Python logic.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Hi! So the answer seems to be: no, they are not singleton (which would mean the creation of classobjects has to be intercepted). But their default `__eq__`, which they get from `object`, is dumb - even if the classes are generated from identical source code they don't compare equal. Is it right? – wim Nov 25 '15 at 21:40
  • Right, the class objects are not instances of some singelton class, they're just normal instances of the `type` metatype. Calling `type(obj)` doesn't create a new type, it just looks up the type of the instance `obj` (from obj.__class__`) and returns a reference to that pre-exisiting object. – Blckknght Nov 25 '15 at 21:53
6

I'm not aware of any documentation about how == works for types, but it definitely works by identity. You can see that the CPython 2.7 implementation is a pointer comparison:

static PyObject*
type_richcompare(PyObject *v, PyObject *w, int op)
{
    ...

    /* Compare addresses */
    vv = (Py_uintptr_t)v;
    ww = (Py_uintptr_t)w;
    switch (op) {
    ...
    case Py_EQ: c = vv == ww; break;

In CPython 3.5, type doesn't implement its own tp_richcompare, so it inherits the default equality comparison from object, which is a pointer comparison:

PyTypeObject PyType_Type = {
    ...
    0,                                          /* tp_richcompare */
user2357112
  • 260,549
  • 28
  • 431
  • 505
  • For posterity, this remains true in [CPython 3.10](https://github.com/python/cpython/blob/3.10/Objects/typeobject.c#L4338). `type` objects still use the default equality comparison from `object`. – Brian61354270 Aug 19 '21 at 17:21