Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
pyroot002_pythonizationDecorator.py
Go to the documentation of this file.
1## \file
2## \ingroup tutorial_pyroot
3## \notebook -nodraw
4## This tutorial shows how to use the @pythonization decorator to add extra
5## behaviour to C++ user classes that are used from Python via PyROOT.
6##
7## \macro_code
8## \macro_output
9##
10## \date November 2021
11## \author Enric Tejedor
12
13import ROOT
14from ROOT import pythonization
15
16# Let's first define a new C++ class. In this tutorial, we will see how we can
17# "pythonize" this class, i.e. how we can add some extra behaviour to it to
18# make it more pythonic or easier to use from Python.
19#
20# Note: In this example, the class is defined dynamically for demonstration
21# purposes, but it could also be a C++ class defined in some library or header.
22# For more information about loading C++ user code to be used from Python with
23# PyROOT, please see:
24# https://root.cern.ch/manual/python/#loading-user-libraries-and-just-in-time-compilation-jitting
25ROOT.gInterpreter.Declare('''
26class MyClass {};
27''')
28
29# Next, we define a pythonizor function: the function that will be responsible
30# for injecting new behaviour in our C++ class `MyClass`.
31#
32# To convert a given Python function into a pythonizor, we need to decorate it
33# with the @pythonization decorator. Such decorator allows us to define which
34# which class we want to pythonize by providing its class name and its
35# namespace (if the latter is not specified, it defaults to the global
36# namespace, i.e. '::').
37#
38# The decorated function - the pythonizor - must accept either one or two
39# parameters:
40# 1. The class to be pythonized (proxy object where new behaviour can be
41# injected)
42# 2. The fully-qualified name of that class (optional).
43#
44# Let's see all this with a simple example. Suppose I would like to define how
45# `MyClass` objects are represented as a string in Python (i.e. what would be
46# shown when I print that object). For that purpose, I can define the following
47# pythonizor function. There are two important things to be noted here:
48# - The @pythonization decorator has one argument that specifies our target
49# class is `MyClass`.
50# - The pythonizor function `pythonizor_of_myclass` provides and injects a new
51# implementation for `__str__`, the mechanism that Python provides to define
52# how to represent objects as strings. This new implementation
53# always returns the string "This is a MyClass object".
54@pythonization('MyClass')
55def pythonizor_of_myclass(klass):
56 klass.__str__ = lambda o : 'This is a MyClass object'
57
58# Once we have defined our pythonizor function, let's see it in action.
59# We will now use the `MyClass` class for the first time from Python: we will
60# create a new instance of that class. At this moment, the pythonizor will
61# execute and modify the class - pythonizors are always lazily run when a given
62# class is used for the first time from a Python script.
63my_object = ROOT.MyClass()
64
65# Since the pythonizor already executed, we should now see the new behaviour.
66# For that purpose, let's print `my_object` (should show "This is a MyClass
67# object").
68print(my_object)
69
70# The previous example is just a simple one, but there are many ways in which a
71# class can be pythonized. Typical examples are the redefinition of dunder
72# methods (e.g. `__iter__` and `__next__` to make your objects iterable from
73# Python). If you need some inspiration, many ROOT classes are pythonized in
74# the way we just saw; their pythonizations can be seen at:
75# https://github.com/root-project/root/tree/master/bindings/pyroot/pythonizations/python/ROOT/pythonization
76
77# The @pythonization decorator offers a few more options when it comes to
78# matching classes that you want to pythonize. We saw that we can match a
79# single class, but we can also specify a list of classes to pythonize.
80#
81# The following code defines a couple of new classes:
82ROOT.gInterpreter.Declare('''
83namespace NS {
84 class Class1 {};
85 class Class2 {};
86}
87''')
88
89# Note that these classes belong to the `NS` namespace. As mentioned above, the
90# @pythonization decorator accepts a parameter with the namespace of the class
91# or classes to be pythonized. Therefore, a pythonizor that matches both classes
92# would look like this:
93@pythonization(['Class1', 'Class2'], ns='NS')
94def pythonize_two_classes(klass):
95 klass.new_attribute = 1
96
97# Both classes will have the new attribute:
98o1 = ROOT.NS.Class1()
99o2 = ROOT.NS.Class2()
100print("Printing new attribute")
101for o in o1, o2:
102 print(o.new_attribute)
103
104# In addition, @pythonization also accepts prefixes of classes in a certain
105# namespace in order to match multiple classes in that namespace. To signal that
106# what we provide to @pythonization is a prefix, we need to set the `is_prefix`
107# argument to `True` (default is `False`).
108#
109# A common case where matching prefixes is useful is when we have a templated
110# class and we want to pythonize all possible instantiations of that template.
111# For example, we can pythonize the `std::vector` (templated) class like so:
112@pythonization('vector<', ns='std', is_prefix=True)
113def vector_pythonizor(klass):
114 # first_elem returns the first element of the vector if it exists
115 klass.first_elem = lambda v : v[0] if v else None
116
117# Since we defined a prefix to do the match, the pythonization will be applied
118# both if we instantiate e.g. a vector of integers and a vector of doubles.
119v_int = ROOT.std.vector['int']([1,2,3])
120v_double = ROOT.std.vector['double']([4.,5.,6.])
121print("First element of integer vector: {}".format(v_int.first_elem()))
122print("First element of double vector: {}".format(v_double.first_elem()))
123
124# Note that specifying a list of class name prefixes is also possible (similarly
125# to what we saw with a list of class names). Again, `is_prefix=True` is
126# required to signal that we are providing a list of prefixes.
127
128# These are some examples of combinations of prefixes and namespaces and the
129# corresponding classes that they match:
130# - '' : all classes in the global namespace.
131# - '', ns='NS1::NS2' : all classes in the `NS1::NS2` namespace.
132# - 'Prefix' : classes whose name starts with `Prefix` in the global namespace.
133# - 'Prefix', ns='NS' : classes whose name starts with `Prefix` in the `NS`
134# namespace.
135
136# Moreover, a pythonizor function can have a second optional parameter that
137# contains the fully-qualified name of the class being pythonized. This can be
138# useful e.g. if we would like to do some more complex filtering of classes in
139# our pythonizor, for instance using regular expressions.
140@pythonization('pair<', ns='std', is_prefix=True)
141def pair_pythonizor(klass, name):
142 print('Pythonizing class ' + name)
143
144# The pythonizor above will be applied to any instantiation of `std::pair` - we
145# can see this with the print we did inside the pythonizor.
146# Note that we could use the `name` parameter to e.g. further filter which
147# particular instantiations we would like to pythonize.
148p1 = ROOT.std.pair['int','int'](1,2) # prints 'Pythonizing class std::pair<int,int>'
149p2 = ROOT.std.pair['int','double'](1,2.) # prints 'Pythonizing class std::pair<int,double>'
150
151# Note that, to pythonize multiple classes in different namespaces, we can
152# stack multiple @pythonization decorators. For example, if we define these
153# classes:
154ROOT.gInterpreter.Declare('''
155class FirstClass {};
156namespace NS {
157 class SecondClass {};
158}
159''')
160
161# We can pythonize both of them with a single pythonizor function like so:
162@pythonization('FirstClass')
163@pythonization('SecondClass', ns='NS')
164def pythonizor_for_first_and_second(klass, name):
165 print('Executed for class ' + name)
166
167# If we now access both classes, we should see that the pythonizor runs twice.
168f = ROOT.FirstClass()
169s = ROOT.NS.SecondClass()
170
171# So far we have seen how pythonizations can be registered for classes that
172# have not been used yet. We have discussed how, in that case, the pythonizor
173# functions are executed lazily when their target class/es are used for the
174# first time in the application.
175# However, it can also happen that our target class/es have already been
176# accessed by the time we register a pythonization. In such a scenario, the
177# pythonizor is applied immediately (at registration time) to the target
178# class/es.
179
180# Let's see an example of what was just explained. We will define a new class
181# and immediately create an object of that class. We can check how the object
182# still does not have a new attribute `pythonized` that we are going to inject
183# in the next step.
184ROOT.gInterpreter.Declare('''
185class MyClass2 {};
186''')
187o = ROOT.MyClass2()
188try:
189 print(o.pythonized)
190except AttributeError:
191 print("Object has not been pythonized yet!")
192
193# After that, we will register a pythonization for `MyClass2`. Since the class
194# has already been used, the pythonization will happen right away.
195@pythonization('MyClass2')
196def pythonizor_for_myclass2(klass):
197 klass.pythonized = True
198
199# Now our object does have the `pythonized` attribute:
200print(o.pythonized) # prints True