class
, a dynamic definition uses an inline class type
.
Type metaclass
The type class is often used to get the type of an object. For example like this:
h = "hello"
type(h)
<class 'str'>
But it has other uses. It can initialize new types.As you know, everything in Python is an object. It follows that all definitions have types, including classes and objects. For instance:
class A:
pass
type(A)
<class 'type'>
It may not be entirely clear why a class is assigned a class type
type
, as opposed to its instances:
a = A()
type(a)
<class '__main__.A'>
The object
a
is assigned a class as a type. This is how the interpreter treats the object as an instance of the class. The class itself has a class type type
because it inherits it from the base class object
:
A.__bases__
(<class 'object'>,)
Class type
object
:
type(object)
<class 'type'>
The class is
object
inherited by all classes by default, that is:
class A(object):
pass
Same as:
class A:
pass
The class being defined inherits the base class as a type. However, this does not explain why the base class
object
is of the class type type
. The point is, it type
is a metaclass. As you already know, all classes inherit from the base class object
, which is of the metaclass type type
. Therefore, all classes also have this type, including the metaclass itself type
:
type(type)
<class 'type'>
This is the "endpoint of typing" in Python. The type inheritance chain is closed on the class
type
. The metaclass type
serves as the base for all classes in Python. It is easy to verify this:
builtins = [list, dict, tuple]
for obj in builtins:
type(obj)
<class 'type'>
<class 'type'>
<class 'type'>
A class is an abstract data type, and instances of it have a class reference as a type.
Initializing new types with the type class
When checking types, the class is
type
initialized with a single argument:
type(object) -> type
In doing so, it returns the type of the object. However, the class implements a different initialization method with three arguments, which returns a new type:
type(name, bases, dict) -> new type
Type initialization parameters
name
A string that defines the name of the new class (type).bases
A tuple of base classes (classes that the new class will inherit).dict
Dictionary with the attributes of the future class. Usually with strings in keys and callable types in values.
Dynamic class definition
We initialize the class of the new type, providing all the necessary arguments and call it:
MyClass = type("MyClass", (object, ), dict())
MyClass
<class '__main__.MyClass'>
You can work with the new class as usual:
m = MyClass()
m
<__main__.MyClass object at 0x7f8b1d69acc0>
Moreover, the method is equivalent to the usual class definition:
class MyClass:
pass
Dynamically defining class attributes
There is little point in an empty class, so the question arises: how to add attributes and methods?
To answer this question, consider the initial initialization code:
MyClass = type(“MyClass”, (object, ), dict())
Typically, attributes are added to a class at the initialization stage as a third argument - a dictionary. You can specify attribute names and values in the dictionary. For example, it could be a variable:
MyClass = type(“MyClass”, (object, ), dict(foo=“bar”)
m = MyClass()
m.foo
'bar'
Dynamic method definition
Callable objects can also be passed to the dictionary, for example, methods:
def foo(self):
return “bar”
MyClass = type(“MyClass”, (object, ), dict(foo=foo))
m = MyClass()
m.foo
'bar'
This method has one significant drawback - the need to define the method statically (I think that in the context of metaprogramming tasks, this can be considered a drawback). Besides, defining a method with a parameter
self
outside the class body looks strange. Therefore, let's go back to dynamic initialization of a class without attributes:
MyClass = type(“MyClass”, (object, ), dict())
After initializing an empty class, you can add methods to it dynamically, that is, without an explicit static definition:
code = compile('def foo(self): print(“bar”)', "<string>", "exec")
compile
Is a built-in function that compiles source code into an object. The code can be executed by functions exec()
or eval()
.
Compile function parameters
- source
Source code, can be a link to a module. - filename
The name of the file into which the object will be compiled. - mode
If specified"exec"
, the function will compile the source code into a module.
The result of the work
compile
is a class object code
:
type(code)
<class 'code'>
The object
code
needs to be converted to a method. Since a method is a function, we start by converting a class code
object to a class object function
. To do this, import the module types
:
from types import FunctionType, MethodType
I will import
MethodType
it because I will need it later to convert the function to a class method.
function = FunctionType(code.co_consts[0], globals(), “foo”)
FunctionType initialization method parameters
code
Class objectcode
.code.co_consts[0]
Is a call to aco_consts
class descriptorcode
, which is a tuple with constants in the object's code. Imagine an objectcode
as a module with a single function that we are trying to add as a class method.0
Is its index, since it is the only constant in the module.globals()
Dictionary of global variables.name
Optional parameter specifying the name of the function.
The result is a function:
function
<function foo at 0x7fc79cb5ed90>
type(function)
<class 'function'>
Next, you need to add this function as a class method
MyClass
:
MyClass.foo = MethodType(function, MyClass)
A fairly simple expression that assigns our function to a class method
MyClass
.
m = MyClass()
m.foo()
bar
Warning
In 99% of cases, you can get by with static class definitions. However, the concept of metaprogramming is good at revealing the internals of Python. Most likely, it will be difficult for you to find the application of the methods described here, although in my practice there was such a case.
Have you worked with dynamic objects? Maybe in other languages?