Are classobjects singletons?
Asked Answered
C

2

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.

Carse answered 25/11, 2015 at 19:30 Comment(4)
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.Doctrinal
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.Carse
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.Doctrinal
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).Scrapbook
S
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.

Scharf answered 25/11, 2015 at 21:18 Comment(2)
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?Carse
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.Scharf
D
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 */
Dirigible answered 25/11, 2015 at 21:31 Comment(1)
For posterity, this remains true in CPython 3.10. type objects still use the default equality comparison from object.Mccartney

© 2022 - 2024 — McMap. All rights reserved.