Blog post: https://kaustubh.page/posts/objects-and-types-in-python/
An object
isinstance(1, object)
True
isinstance([1,2,3], object)
True
isinstance(tuple(), object)
True
isinstance(object(), object)
True
3 things taken together:
They have:
type
@classmethod
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})
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__
()
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,)
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
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
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
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
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!
h = a.hello
h()
Hello, world!
A.hello = lambda: print("Foo bar")
h()
Hello, world!
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
__getattribute__
that will make it work this way?¶__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
__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!