Objects and Types in Python¶
Blog post: https://kaustubh.page/posts/objects-and-types-in-python/
Everything is an object¶
An object
isinstance(1, object)
True
isinstance([1,2,3], object)
True
isinstance(tuple(), object)
True
isinstance(object(), object)
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
class A:
...
isinstance(A, type)
True
isinstance(int, type)
True
isinstance(list, type)
True
class B:
@classmethod
def foo(cls):
print("bar")
B.foo()
bar
class C:
spam = "spam"
ham = "ham"
C.__dict__
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.
isinstance(type, type)
True
Moreover, object
is a type
, and type
is an object
.
isinstance(object, type)
True
isinstance(type, object)
True
This looks like a chicken-egg problem
type
is actually a class that inherits from object
.
All classes implicitly inherit from object
.
issubclass(type, object)
True
type.__bases__
(object,)
object.__bases__
()
This is important, because this is why everything is an object¶
All types (classes) have object
as an implicit base class.
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
.
Foo.__class__
type
Foo.__bases__
(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
.
issubclass(type, object)
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.
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
# this is one form:
type(int)
type
# this is the other form:
A = type("A", tuple(), {"x": 10})
a = A()
a.x
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
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
@create_class()
def A():
x = 10
def __init__(self, y, z):
self.y = y
self.z = z
return locals()
a = A(5, 10)
A
__main__.A
a
<__main__.A at 0x10f47c250>
a.y
5
a.z
10
@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
@create_class(type)
def Meta():
def __repr__(self):
return f"Meta {self.__name__}"
return locals()
@create_class(metaclass=Meta)
def C():
return locals()
C
Meta C
Caveat: scoping¶
The class
keyword has a different scoping from functions.
x = 10
class A:
x = x + 10
A.x
20
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.
# we initialise with an empty class
class A:
pass
a = A()
# then add a method to that class
def hello(self):
print("Hello, world!")
A.hello = hello
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'
a.hello()
Hello, world!
# then call it from the object we initialised at the start
a.hello()
Hello, world!
# if "self" in locals():
# ...
function = property(fget=..., fset=...)
class A:
def hello(self):
print("Hello, world!")
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'
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.¶
h = a.hello
h()
Hello, world!
What's the magic here?¶
A.hello = lambda: print("Foo bar")
h()
Hello, world!
This happens during lookup time, and methods have their own type different from functions¶
type(a.hello)
method
type(A.hello)
function
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
.
class Ten:
def __get__(self, obj, objtype=None):
return 10
class X:
ten = Ten()
x = X()
x.ten
10
Functions use the method class to construct methods in their __get__
.¶
class B:
greeting = "Foo foo, bar bar"
def say(obj):
print(obj.greeting)
b = B()
b_say = say.__get__(b)
b_say()
Foo foo, bar bar
B.greeting = "Good morning!"
b_say()
Good morning!