Objects and Types in Python¶

Blog post: https://kaustubh.page/posts/objects-and-types-in-python/

Everything is an object¶

An object

In [1]:
isinstance(1, object)
Out[1]:
True
In [2]:
isinstance([1,2,3], object)
Out[2]:
True
In [3]:
isinstance(tuple(), object)
Out[3]:
True
In [4]:
isinstance(object(), object)
Out[4]:
True

What is an object?¶

3 things taken together:

  • type
  • attributes
  • methods

Classes are also objects¶

They have:

  • type: type
  • methods: @classmethod
  • attributes: class attributes
In [5]:
class A:
    ...

isinstance(A, type)
Out[5]:
True
In [6]:
isinstance(int, type)
Out[6]:
True
In [7]:
isinstance(list, type)
Out[7]:
True
In [8]:
class B:
    @classmethod
    def foo(cls):
        print("bar")

B.foo()
bar
In [9]:
class C:
    spam = "spam"
    ham = "ham"

C.__dict__
Out[9]:
mappingproxy({'__module__': '__main__',
              'spam': 'spam',
              'ham': 'ham',
              '__dict__': <attribute '__dict__' of 'C' objects>,
              '__weakref__': <attribute '__weakref__' of 'C' objects>,
              '__doc__': None})

Holup¶

Classes (types) are objects whose type is type.

type is a class too, so it is an object of type type too.

In [10]:
isinstance(type, type)
Out[10]:
True

Moreover, object is a type, and type is an object.

In [11]:
isinstance(object, type)  
Out[11]:
True
In [12]:
isinstance(type, object)  
Out[12]:
True

This looks like a chicken-egg problem

type is actually a class that inherits from object.

All classes implicitly inherit from object.

In [13]:
issubclass(type, object)
Out[13]:
True
In [14]:
type.__bases__
Out[14]:
(object,)
In [15]:
object.__bases__
Out[15]:
()

This is important, because this is why everything is an object¶

All types (classes) have object as an implicit base class.

In [16]:
class Foo:
    ...

foo = Foo()
+------------------+         +------------------+         
|       foo        |    +===>|       Foo        |         
+------------------+    |    +------------------+         
|    type: Foo =========+    |    type: type    |         
|    *methods*     |         |     *methods*    |         
|     *attrs*      |         | bases: (object,) |         
+------------------+         |   *other attrs*  |         
                             +------------------+

In the same way, type is also an object.

In [17]:
Foo.__class__
Out[17]:
type
In [18]:
Foo.__bases__
Out[18]:
(object,)

What does it actually mean to be an instance of some type?¶

isinstance method can be roughly implemented like this:

def isinstance(obj, klass):
    return obj.__class__ == klass or klass in obj.__class__.__bases__

Either the immediate class, or one of its bases must match.

type is an object

+------------------+         +------------------+         +----------------+
|       type       |    +===>|       type       |    +===>|     object     |
+------------------+    |    +------------------+    |    +----------------+
|    type: type ========+    |    type: type    |    |    |   type: type   |
|    *methods*     |         |    *methods*     |    |    |    *methods*   |
| bases: (object,) |         | bases: (object,) =====+    |   bases: ()    |
|   *other attrs*  |         |   *other attrs*  |         |  *other attrs* |
+------------------+         +------------------+         +----------------+

The type of type is a subclass of object.

In [19]:
issubclass(type, object)
Out[19]:
True

A type is just another object¶

Can we initialise them like normal objects?¶

We can use the type constructor¶

The behaviour of type changes depending on the number of arguments.

In [20]:
help(type)
Help on class type in module builtins:

class type(object)
 |  type(object) -> the object's type
 |  type(name, bases, dict, **kwds) -> a new type
 |  
 |  Methods defined here:
 |  
 |  __call__(self, /, *args, **kwargs)
 |      Call self as a function.
 |  
 |  __delattr__(self, name, /)
 |      Implement delattr(self, name).
 |  
 |  __dir__(self, /)
 |      Specialized __dir__ implementation for types.
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __init__(self, /, *args, **kwargs)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  __instancecheck__(self, instance, /)
 |      Check if an object is an instance.
 |  
 |  __or__(self, value, /)
 |      Return self|value.
 |  
 |  __repr__(self, /)
 |      Return repr(self).
 |  
 |  __ror__(self, value, /)
 |      Return value|self.
 |  
 |  __setattr__(self, name, value, /)
 |      Implement setattr(self, name, value).
 |  
 |  __sizeof__(self, /)
 |      Return memory consumption of the type object.
 |  
 |  __subclasscheck__(self, subclass, /)
 |      Check if a class is a subclass.
 |  
 |  __subclasses__(self, /)
 |      Return a list of immediate subclasses.
 |  
 |  mro(self, /)
 |      Return a type's method resolution order.
 |  
 |  ----------------------------------------------------------------------
 |  Class methods defined here:
 |  
 |  __prepare__(...)
 |      __prepare__() -> dict
 |      used to create the namespace for the class statement
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  __new__(*args, **kwargs)
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __abstractmethods__
 |  
 |  __annotations__
 |  
 |  __dict__
 |  
 |  __text_signature__
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  __base__ = <class 'object'>
 |      The base class of the class hierarchy.
 |      
 |      When called, it accepts no arguments and returns a new featureless
 |      instance that has no instance attributes and cannot be given any.
 |  
 |  
 |  __bases__ = (<class 'object'>,)
 |  
 |  __basicsize__ = 888
 |  
 |  __dictoffset__ = 264
 |  
 |  __flags__ = 2148031744
 |  
 |  __itemsize__ = 40
 |  
 |  __mro__ = (<class 'type'>, <class 'object'>)
 |  
 |  __weakrefoffset__ = 368

In [21]:
# this is one form:
type(int)
Out[21]:
type
In [22]:
# this is the other form:
A = type("A", tuple(), {"x": 10})
a = A()
In [23]:
a.x
Out[23]:
10

You don't need the class keyword¶

When we write a class block, we create a type with the locally scoped variables as the dict.

Bases and metaclass come from the "parameter declaration".

Can we do it with functions and decorators?

If we make our funtions return the locals, that makes it easy for our decorator to initialise with type.

Can we support this syntax?

@create_class()
def A():
    x = 10	
    def __init__(self, y, z):
        self.y = y
        self.z = z
    return locals()

a = A(5, 10)

With inheritance:

@create_class(A)
def B():
    x = 20
    return locals()

b = B(1, 2)
print(b.x, b.y, b.z)  # 20, 1, 2

With metaclasses:

@create_class(type)
def Meta():
    def __repr__(self):
        return f"Meta {self.__name__}"
    return locals()

@create_class(metaclass=Meta)
def C():
    pass
In [24]:
def create_class(*bases, **kwds):
    def wrapper(func):
        metaclass = kwds.get("metaclass", type)
        klass_dict = func()
        klass = metaclass(func.__name__, bases, klass_dict)
        return klass
    return wrapper
In [51]:
@create_class()
def A():
	x = 10	
	def __init__(self, y, z):
	    self.y = y
	    self.z = z
	return locals()

a = A(5, 10)
In [52]:
A
Out[52]:
__main__.A
In [53]:
a
Out[53]:
<__main__.A at 0x10f47c250>
In [54]:
a.y
Out[54]:
5
In [55]:
a.z
Out[55]:
10
In [28]:
@create_class(A)
def B():
    x = 20
    return locals()

b = B(1, 2)
print(b.x, b.y, b.z)  # 20, 1, 2
20 1 2
In [29]:
@create_class(type)
def Meta():
    def __repr__(self):
        return f"Meta {self.__name__}"
    return locals()

@create_class(metaclass=Meta)
def C():
    return locals()
In [30]:
C
Out[30]:
Meta C

Caveat: scoping¶

The class keyword has a different scoping from functions.

In [56]:
x = 10

class A:
    x = x + 10
In [57]:
A.x
Out[57]:
20
In [32]:
x = 10

def f():
    x = x + 10

f()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
Input In [32], in <cell line: 6>()
      3 def f():
      4     x = x + 10
----> 6 f()

Input In [32], in f()
      3 def f():
----> 4     x = x + 10

UnboundLocalError: local variable 'x' referenced before assignment

Method lookup¶

Lookup order¶

Lookup happens in the order Object -> Class -> Super classes. Attributes that are loaded from classes are loaded dynamically.

In [59]:
# we initialise with an empty class

class A:
    pass

a = A()
In [60]:
# then add a method to that class

def hello(self):
    print("Hello, world!")

A.hello = hello
In [61]:
A.hello()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [61], in <cell line: 1>()
----> 1 A.hello()

TypeError: hello() missing 1 required positional argument: 'self'
In [62]:
a.hello()
Hello, world!
In [35]:
# then call it from the object we initialised at the start

a.hello()
Hello, world!
In [ ]:
# if "self" in locals():
#    ...
In [ ]:
function = property(fget=..., fset=...)

self injection¶

Why is self only there when calling from the initialised object?¶

In [36]:
class A:
    def hello(self):
        print("Hello, world!")
In [37]:
A.hello()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [37], in <cell line: 1>()
----> 1 A.hello()

TypeError: A.hello() missing 1 required positional argument: 'self'
In [38]:
a = A()
a.hello()
Hello, world!

Moreover, this happens before the method is called. We can set the method to a differnt variable and call that instead.¶

In [39]:
h = a.hello
h()
Hello, world!

What's the magic here?¶

In [40]:
A.hello = lambda: print("Foo bar")
In [41]:
h()
Hello, world!

This happens during lookup time, and methods have their own type different from functions¶

In [42]:
type(a.hello)
Out[42]:
method
In [43]:
type(A.hello)
Out[43]:
function
In [44]:
method_class = type(a.hello)
help(method_class)
Help on class method in module builtins:

class method(object)
 |  method(function, instance)
 |  
 |  Create a bound instance method object.
 |  
 |  Methods defined here:
 |  
 |  __call__(self, /, *args, **kwargs)
 |      Call self as a function.
 |  
 |  __delattr__(self, name, /)
 |      Implement delattr(self, name).
 |  
 |  __eq__(self, value, /)
 |      Return self==value.
 |  
 |  __ge__(self, value, /)
 |      Return self>=value.
 |  
 |  __get__(self, instance, owner=None, /)
 |      Return an attribute of instance, which is of type owner.
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __gt__(self, value, /)
 |      Return self>value.
 |  
 |  __hash__(self, /)
 |      Return hash(self).
 |  
 |  __le__(self, value, /)
 |      Return self<=value.
 |  
 |  __lt__(self, value, /)
 |      Return self<value.
 |  
 |  __ne__(self, value, /)
 |      Return self!=value.
 |  
 |  __reduce__(...)
 |      Helper for pickle.
 |  
 |  __repr__(self, /)
 |      Return repr(self).
 |  
 |  __setattr__(self, name, value, /)
 |      Implement setattr(self, name, value).
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __func__
 |      the function (or other callable) implementing a method
 |  
 |  __self__
 |      the instance to which a method is bound

Can you think of a definition for __getattribute__ that will make it work this way?¶

Functions are descriptors¶

When an attribute has a __get__ method, it's called instead of returning the actual value of the attribute.¶

This is similar to @property.

In [45]:
class Ten:
    def __get__(self, obj, objtype=None):
        return 10

class X:
    ten = Ten()

x = X()
x.ten
Out[45]:
10

Functions use the method class to construct methods in their __get__.¶

In [46]:
class B:
    greeting = "Foo foo, bar bar"

def say(obj):
    print(obj.greeting)

b = B()
b_say = say.__get__(b)
In [47]:
b_say()
Foo foo, bar bar
In [48]:
B.greeting = "Good morning!"
In [49]:
b_say()
Good morning!