user1592380
user1592380

Reputation: 36317

Relationship between between type and object in python

I'm reading through this blog, which contains:

Since in Python everything is an object, everything is the instance of a class, even classes. Well, type is the class that is instanced to get classes. So remember this: object is the base of every object, type is the class of every type. Sounds puzzling? It is not your fault, don't worry. However, just to strike you with the finishing move, this is what Python is built on.

>>> type(object)
<class 'type'>
>>> type.__bases__
(<class 'object'>,)

I'm having trouble understanding this. Can anyone explain this relationship in a different way to make it clearer?

Upvotes: 9

Views: 1387

Answers (4)

Tadhg McDonald-Jensen
Tadhg McDonald-Jensen

Reputation: 21464

you can sum up all the mystifying terminology and relationships into these 2 sentences:

  1. Every class defines methods that act on its instances, behaviours of classes themselves is defined in the type class.

  2. Behaviour that is common to all objects - the kind of things you usually just write off as builtin - are defined by object so all objects share those behaviours (unless a subclass overrides it :)

The rest of this answer will try to go in depth with a few examples using stuff in python that is useful to be aware of, addressing each part of the description in small chunks.

In Python everything is an object

Practically this means operations that are standard across all python objects will be defined in object, things like attribute lookup and getting the size of an object in memory. Concretely (but not as useful) it also implies:

  • the statement isinstance(x, object) will always give True for any possible value x.

  • This also means that any class that is defined will be considered a subclass of object. So issubclass(x, object) is always True for any class x.

I want to take a really quick detour to how python converts stuff to strings, this is from the documentation:

object.__repr__(self)

Called by the repr() built-in function to compute the “official” string representation of an object. If at all possible, this should look like a valid Python expression. [...] If a class defines __repr__() but not __str__(), then __repr__() is also used when an “informal” string representation of instances of that class is required.

This is typically used for debugging, so it is important that the representation is information-rich and unambiguous.

object.__str__(self)

[...] to compute the “informal” or nicely printable string representation of an object. [...]

The default implementation defined by the built-in type object calls object.__repr__().

For beginners in python knowing that both str and repr exist is really useful: when you print data it uses str and when you evaluate stuff on the command line it uses repr which is mostly noticeable with strings:

>>> x = "123"
>>> print(x) # prints string as is which since it contains digits is misleading
123
>>> x # shows "official" representation of x with quotes to indicate it is a string
'123'
>>> print(repr(x)) # from a script you can get this behaviour by calling repr()
'123'

Where this relates to our conversation about object is that the sentence about "The default implementation [for __str__] defined by the built-in type object calls object.__repr__()." So that means if we define a __repr__ method then when we print the object we get that as well

class Test:
    def __init__(self, x):
        self.x = x
    def __repr__(self):
        return "Test(x={})".format(repr(self.x))

x = Test("hi") # make a new object
print(x) # prints Test(x='hi')
# and the process it goes through is this chain:
assert (   str(x)           # converting to a string
        == Test.__str__(x)  # is the same as calling __str__ on the class
        == object.__str__(x)# which falls back to the method defined in superclass
        == repr(x)          # which calls repr(x) (according to those docs)
        == Test.__repr__(x) # which calls the __repr__ method on the class
        )

For the most part we don't care about this much detail - we just care that python can behave reasonably when we print out our data. The only part that really matters is that that default reasonable behaviour is defined inside of object!

type is the class that is instanced to get classes

type is the class of every type

so in the same way that int defines what 1+3 should do or str defines methods for strings, type defines behaviour that is specific to type objects. For example calling a class object (like int("34")) will create a new instance of that class - this behaviour of creating new objects is defined in type.__call__ method. For completion we have the technical implications:

  • We could say "1 is an int" which in code translates to isinstance(1,int) == True. Similarly we could say "int is a type" which translates to isinstance(int, type) == True.

  • all classes are considered instances of type. So isinstance(x, type) will be true for all classes. When I say classes I mean things like int, str, bool or the variable created by the class keyword.

everything is an object - even classes.

This means that the standard behaviour that exists for all objects is also applied to class objects. So writing str.join will lookup the join method by looking it up with the same attribute lookup as every other object, which makes sense. (right now I'm not calling the method just accessing the attribute)

As a more concrete example we can look at str.join which is an arbitrarily selected method on a familiar data type (strings) and its duality with type.mro which is a method on type objects in the same way:

>>> x = "hello"
>>> str.join # unbound method of strings
<method 'join' of 'str' objects>
>>> x.join #bound method of x
<built-in method join of str object at 0x109bf23b0>
>>> hex(id(x)) # the memory address of x as seen above
'0x109bf23b0'

>>> type.mro #unbound method
<method 'mro' of 'type' objects>
>>> int.mro #mro method bound to int
<built-in method mro of type object at 0x106afeca0>
>>> hex(id(int)) # address of int object as seen above
'0x106afeca0'
>>> int.mro() #mro stands for Method Resolution Order, is related to __bases__
[<class 'int'>, <class 'object'>]

So at this point I'd recommend you go back and re-read the 2 statements I made at the beginning of this answer and will hopefully feel more confident in believing it.

P.S. In the same way you can make a subclass of str to create special string objects that have different behaviour, you can create a subclass of type to create special classes that have different behaviour. This is called a meta-class (the class of a class object) and the practical applications of using meta-classes are usually abstract. (pun intended)

Upvotes: 0

Raymond Hettinger
Raymond Hettinger

Reputation: 226644

Relationships

The relationship between type() and object() is tightly interwoven.

Each is an instance of the other:

>>> isinstance(object, type)
True
>>> isinstance(object, object)
True
>>> isinstance(type, type)
True
>>> isinstance(type, object)
True

With respect to subclassing, a type is a kind of object, but not vice versa:

>>> issubclass(object, type)
False
>>> issubclass(type, object)
True

Mechanics

I think of object() as providing the baseline capabilities for all objects:

>>> dir(object)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

The only connection to type() is that __class__ is set to type(); otherwise the latter is not used at all:

>>> object.__class__
<class 'type'>

Then type() inherits from object(), but overrides several critical methods for creating classes:

for methname in dir(type):
    if 'type' in repr(getattr(type, methname, '')):
        print(methname)

        
    __call__
    __class__
    __delattr__
    __dict__
    __dir__
    __doc__
    __getattribute__
    __init__
    __init_subclass__
    __instancecheck__
    __mro__
    __name__
    __new__
    __or__
    __prepare__
    __qualname__
    __repr__
    __ror__
    __setattr__
    __sizeof__
    __subclasscheck__
    __subclasses__
    __subclasshook__
    mro

With these methods and attributes, type can now create classes. Classes aren't really special. They can just an instance of object with methods suitable for making subclasses and instances.

Upvotes: 1

Tadhg McDonald-Jensen
Tadhg McDonald-Jensen

Reputation: 21464

The relationship between type(x) is basically the same as the result of x.__class__:

for obj in (object,type,1,str):
    assert type(obj) is obj.__class__

print("type(obj) and obj.__class__ gave the same results in all test cases")

__bases__ represents the bases that a class is derived from:

class parent:
    pass

class child(parent,int):
    pass

print(child.__bases__) # prints (<class '__main__.parent'>, <class 'int'>)

however if you are asking about the odd relationship between object and type:

   # are `object` and `type` both instances of the `object` class?
>>> isinstance(object, object) and isinstance(type, object)
True
   # are they also instances of the `type` class?
>>> isinstance(object, type) and isinstance(type, type)
True
   # `type` is a subclass of `object` and not the other way around (this makes sense)
>>> [issubclass(type, object), issubclass(object, type)]
[True, False]

that is more of a chicken vs. egg question: which came first?

The answer is PyObject which is defined in C.

before either object or type is available to the python interpreter their underlying mechanisms are defined in C and the instance checking is overrided after they are defined. (act like abstract classes, see PEP 3119)

you can consider it something like this python implementation:

#this wouldn't be available in python
class superTYPE(type):
    def __instancecheck__(cls,inst):
        if inst ==TYPE:
            return True
        else:
            return NotImplemented #for this demo
    
class TYPE(type,metaclass=superTYPE):
    def __instancecheck__(cls,inst):
        if inst in (OBJECT,TYPE):
            return True
        else:
            return NotImplemented #for this demo

class OBJECT(metaclass=TYPE):
    pass

# these all pass
assert isinstance(TYPE,OBJECT)
assert isinstance(OBJECT,TYPE)
assert isinstance(TYPE,TYPE)
assert isinstance(OBJECT,OBJECT)

actually it may be better represented as:

#this isn't available in python
class superTYPE(type):
    def __instancecheck__(cls,inst):
        if inst in (TYPE,OBJECT):
            return True
        else:
            return NotImplemented #for this demo
    
class OBJECT(metaclass=superTYPE):
    pass


class TYPE(OBJECT):
    pass

but again if you want to know exactly how it works you would need to look at the source code written in C.

Upvotes: 5

Wayne Werner
Wayne Werner

Reputation: 51897

TL;DR - probably not. But I tried.

This is really weird and feels like turtles all the way down. I've actually not delved into this arena very much before, though it's something that sounded fun and powerful. This explanation was confusing, and so was the rest of the information on that page, but I feel like I have some enlightenment. Whether or not I can explain that clearly, I'm not sure, but I'll have a go.

Let's look at the turtles, first:

>>> isinstance(type, object)
True
>>> isinstance(object, type)
True

Wait, what?

How is object an instance of type, when type is an instance of object? That feels like saying something like:

class Parrot: pass

ex = Parrot()

isinstance(ex, Parrot)
isinstance(Parrot, ex)

Should be True both times. But obviously it's not. Even (as Tadhg McDonald-Jensen pointed out)

>>> isinstance(type, type)
True

This should indicate to you that there is some magic going on behind the scenes. So at this point, let's just completely forget about Python (I know, why would we ever want to do such a horrible thing?)

In general, all computer programs are are 1's and 0's (and more accurately they're just a bunch of logic gates and electrons at >~2.5v and ~<2.5v, but 0's and 1's are good enough). Whether you wrote it in assembly, actual machine code, Python, C#, Java, Perl, whatever - they're all just bits.

If you write a class definition, that class is just bits. An instance of that class is just more bits. And a programming language and a compiler and an interpreter is just even more bits.

In the case of Python, it's the python interpreter that gives meaning to the bits that are our Python programs. As an interesting point, a lot of what we typically consider to be Python is actually written in Python (though most of it is C, for us CPython folks, Java for Jython, etc.).

So now we come to this thing we call type and object. As the article points out, they're kind of special. So, we know that we can create a class, and then that class is an object:

>>> class Confusion: pass
...
>>> isinstance(Confusion, object)

Which makes sense, if you think about it - you may have created class-level variables:

>>> class Counter:
...  count = 0
...  def __init__(self):
...   Counter.count += 1
...   print(self.count)
...
>>> Counter()
1
<__main__.Counter object at 0x7fa03fca4518>
>>> Counter()
2
<__main__.Counter object at 0x7fa03fca4470>
>>> Counter()
3
<__main__.Counter object at 0x7fa03fca4518>
>>> Counter()
4
<__main__.Counter object at 0x7fa03fca4470>
>>> Counter.count
4
>>> Counter.__repr__(Counter)
'<type object at 0x1199738>'

But as this last example shows (and is mentioned in the post), a class declaration, what you get with class SomeClass: pass, that declaration of a class is actually an instance of another class. In particular, it's an instance of the type class. And that instance (which we call a class) when called will produce an instance of itself:

>>> Counter.__call__()
5
<__main__.Counter object at 0x7fa03fca4518>

So what does all this have to do with the relationship between type and object?

Well, somewhere, python creates a series of bits that is object, and a series of bits that is type, and then wires them together in such a way that

>>> type.__bases__
(<class 'object'>,)
>>> object.__bases__
()

Because I currently don't feel like looking through the source, I'm going to make a guess that type is created first, and object is produced from that type and that type.__bases__ is set to (class 'object'). By creating this circular relationships between type and object, it gives the appearance that it's just turtles all the way down, when really the last two turtles are just standing on top of each other.

I don't think there's really a better way to explain what's going on here than how the article describes it - at least in a classical OOP is-a/has-a style of thinking, because it's not actually that sort of thing. Like trying to plot a 3d figure in 2d space, you're going to have problems.

It's just two sets of bits that have some bits inside them that happen to be the address of one another.

Upvotes: 1

Related Questions