Skip to content

Commit 2685f09

Browse files
committed
[Python] Document auto-downcasting and restructure pythonizations.md
This commit documents the auto-downcasting for return types of C++ functions, and also improves the structure of the pythonization docs as follows: * The term "pythonizations" is very overloaded, so expand the explanation in the beginning a bit * Cleanly separate into explanation of pythonization in ROOT, and later pythonization of use classes. Before, the section on user pythonization was just labeled "Pythonization example", which was confusing to me. Will this be an example of a pythonization in ROOT? * Move the explanation of the user pythonizations to the end, as this is an advanced feature.
1 parent 489977c commit 2685f09

1 file changed

Lines changed: 121 additions & 61 deletions

File tree

bindings/pyroot/pythonizations/doc/pythonizations.md

Lines changed: 121 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,123 @@
22
\ingroup Python
33
\brief Python-specific functionalities offered by ROOT
44

5-
This page lists the so-called "pythonizations", that is those functionalities offered by ROOT for classes and functions which are specific to Python usage of the package and provide a more pythonic experience.
5+
This page describes ROOT's *pythonizations*: functionality that makes ROOT's C++ classes and functions behave more naturally when used from Python. Rather than simply reproducing C++ behavior, a pythonization augments how you interact with a C++ type. For instance, letting you use NumPy arrays in place of C++ arrays in function calls, or automatic downcasting of C++ instances to their actual types.
66

7-
### Pythonization example
7+
Some pythonizations are general and applied automatically to most C++ types, while others apply only to specific ROOT types. Below we explain some of the automatic pythonizations in detail, then show how to implement custom pythonizations for your own C++ classes.
8+
9+
10+
### Pretty-printing pythonization
11+
This example illustrates the pretty printing feature of PyROOT, which reveals
12+
the content of the object if a string representation is requested, e.g., by
13+
Python's print statement. The printing behaves similar to the ROOT prompt
14+
powered by the C++ interpreter cling.
15+
Create an object with PyROOT
16+
17+
~~~{.py}
18+
obj = ROOT.std.vector("int")(3)
19+
for i in range(obj.size()):
20+
obj[i] = i
21+
~~~
22+
23+
Print the object, which reveals the content. Note that `print` calls the special
24+
method `__str__` of the object internally.
25+
26+
~~~{.py}
27+
print(obj)
28+
~~~
29+
30+
The output can be retrieved as string by any function that triggers the `__str__`
31+
special method of the object, e.g., `str` or `format`.
32+
33+
~~~{.py}
34+
print(str(obj))
35+
print("{}".format(obj))
36+
~~~
37+
38+
Note that the interactive Python prompt does not call `__str__`, it calls
39+
`__repr__`, which implements a formal and unique string representation of
40+
the object.
41+
42+
~~~{.py}
43+
print(repr(obj))
44+
obj
45+
~~~
46+
47+
The print output behaves similar to the ROOT prompt, e.g., here for a ROOT histogram.
48+
49+
~~~{.py}
50+
hist = ROOT.TH1F("name", "title", 10, 0, 1)
51+
print(hist)
52+
~~~
53+
54+
If cling cannot produce any nice representation for the class, we fall back to a
55+
"<ClassName at address>" format, which is what `__repr__` returns
56+
57+
~~~{.py}
58+
ROOT.gInterpreter.Declare('class MyClass {};')
59+
m = ROOT.MyClass()
60+
print(m)
61+
print(str(m) == repr(m))
62+
~~~
63+
64+
### Automatic downcasting pythonization
65+
66+
In C++, it is possible to use a base-class pointer to refer to an instance of a
67+
derived type.
68+
```cpp
69+
class Base {
70+
public:
71+
virtual ~Base() = default;
72+
};
73+
class Derived : public Base {};
74+
Base *foo() {
75+
return new Derived;
76+
}
77+
```
78+
The same is also possible for smart pointers, like `std::unique_ptr` or
79+
`std::shared_ptr`. For example:
80+
```cpp
81+
std::unique_ptr foo_unique() {
82+
return std::unique_ptr{new Derived};
83+
}
84+
```
85+
Using the `Derived` interface on the return value is not possible in C++ without
86+
type casting (e.g. a `dynamic_cast`). Since explicit type casting is not natural
87+
in Python, ROOT attempts to automatically downcast raw pointer or smart pointer
88+
return values to their actual type. Demonstrating this with the types above:
89+
```python
90+
p1 = ROOT.foo()
91+
p2 = ROOT.foo_unique()
92+
print(p1)
93+
print(p2)
94+
```
95+
will give you:
96+
```txt
97+
<cppyy.gbl.Derived object>
98+
<cppyy.gbl.Derived object held by std::unique_ptr<Base>>
99+
```
100+
101+
**Note 1**: keep in mind that the auto downcasting also affects overload
102+
resolution. For example, consider these two overloads:
103+
```cpp
104+
void consume(Base *) {} // overload 1
105+
void consume(Derived *) {} // overload 2
106+
```
107+
In C++, `consume(foo())` will hit overload 1. In Python,
108+
`ROOT.consume(ROOT.foo())` resolves to the second overload because the pointee
109+
was automatically downcast.
110+
111+
**Note 2**: while the type of the pointee gets downcast, smart pointer types
112+
remain unchanged. That's because `std::unique_ptr<Base>` and
113+
`std::unique_ptr<Derived>` are distinct, unrelated types. Template
114+
instantiations don't inherit from one another even when their type arguments do.
115+
116+
**Note 3**: automatic downcasting is not enabled
117+
unconditionally. It is only available for polymorphic base
118+
classes, which is why the `Base` class in the example has a virtual destructor.
119+
120+
121+
### Custom pythonizations for C++ user classes by example
8122
9123
This example shows how to use the `@pythonization` decorator to add extra
10124
behaviour to C++ user classes that are used from Python via PyROOT.
@@ -24,17 +138,17 @@ class MyClass {};
24138
~~~
25139
26140
Next, we define a pythonizor function: the function that will be responsible
27-
for injecting new behaviour in our C++ class `MyClass`.
141+
for injecting new behaviour in our C++ class `MyClass`.
28142
To convert a given Python function into a pythonizor, we need to decorate it
29143
with the @pythonization decorator. Such decorator allows us to define which
30144
which class we want to pythonize by providing its class name and its
31145
namespace (if the latter is not specified, it defaults to the global
32-
namespace, i.e. '::').
146+
namespace, i.e. '::').
33147
The decorated function - the pythonizor - must accept either one or two
34148
parameters:
35149
1. The class to be pythonized (proxy object where new behaviour can be
36150
injected)
37-
2. The fully-qualified name of that class (optional).
151+
2. The fully-qualified name of that class (optional).
38152
Let's see all this with a simple example. Suppose I would like to define how
39153
`MyClass` objects are represented as a string in Python (i.e. what would be
40154
shown when I print that object). For that purpose, I can define the following
@@ -113,7 +227,7 @@ for o in o1, o2:
113227
In addition, @pythonization also accepts prefixes of classes in a certain
114228
namespace in order to match multiple classes in that namespace. To signal that
115229
what we provide to @pythonization is a prefix, we need to set the `is_prefix`
116-
argument to `True` (default is `False`).
230+
argument to `True` (default is `False`).
117231
A common case where matching prefixes is useful is when we have a templated
118232
class and we want to pythonize all possible instantiations of that template.
119233
For example, we can pythonize the `std::vector` (templated) class like so:
@@ -199,7 +313,7 @@ first time in the application.
199313
However, it can also happen that our target class/es have already been
200314
accessed by the time we register a pythonization. In such a scenario, the
201315
pythonizor is applied immediately (at registration time) to the target
202-
class/es
316+
class/es
203317
Let's see an example of what was just explained. We will define a new class
204318
and immediately create an object of that class. We can check how the object
205319
still does not have a new attribute `pythonized` that we are going to inject
@@ -230,57 +344,3 @@ Now our object does have the `pythonized` attribute:
230344
~~~{.py}
231345
print(o.pythonized) # prints True
232346
~~~
233-
234-
### Pythonization printing example
235-
This example illustrates the pretty printing feature of PyROOT, which reveals
236-
the content of the object if a string representation is requested, e.g., by
237-
Python's print statement. The printing behaves similar to the ROOT prompt
238-
powered by the C++ interpreter cling.
239-
Create an object with PyROOT
240-
241-
~~~{.py}
242-
obj = ROOT.std.vector("int")(3)
243-
for i in range(obj.size()):
244-
obj[i] = i
245-
~~~
246-
247-
Print the object, which reveals the content. Note that `print` calls the special
248-
method `__str__` of the object internally.
249-
250-
~~~{.py}
251-
print(obj)
252-
~~~
253-
254-
The output can be retrieved as string by any function that triggers the `__str__`
255-
special method of the object, e.g., `str` or `format`.
256-
257-
~~~{.py}
258-
print(str(obj))
259-
print("{}".format(obj))
260-
~~~
261-
262-
Note that the interactive Python prompt does not call `__str__`, it calls
263-
`__repr__`, which implements a formal and unique string representation of
264-
the object.
265-
266-
~~~{.py}
267-
print(repr(obj))
268-
obj
269-
~~~
270-
271-
The print output behaves similar to the ROOT prompt, e.g., here for a ROOT histogram.
272-
273-
~~~{.py}
274-
hist = ROOT.TH1F("name", "title", 10, 0, 1)
275-
print(hist)
276-
~~~
277-
278-
If cling cannot produce any nice representation for the class, we fall back to a
279-
"<ClassName at address>" format, which is what `__repr__` returns
280-
281-
~~~{.py}
282-
ROOT.gInterpreter.Declare('class MyClass {};')
283-
m = ROOT.MyClass()
284-
print(m)
285-
print(str(m) == repr(m))
286-
~~~

0 commit comments

Comments
 (0)