Skip to content

Commit a622764

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 f5fef03 commit a622764

1 file changed

Lines changed: 129 additions & 61 deletions

File tree

bindings/pyroot/pythonizations/doc/pythonizations.md

Lines changed: 129 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,131 @@
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+
75+
Base *foo() {
76+
static Derived obj;
77+
return &obj;
78+
}
79+
```
80+
The same is also possible for smart pointers, like `std::unique_ptr` or
81+
`std::shared_ptr`. For example:
82+
```cpp
83+
std::unique_ptr<Base> foo_unique() {
84+
return std::unique_ptr<Base>{new Derived};
85+
}
86+
```
87+
Using the `Derived` interface on the return value is not possible in C++ without
88+
type casting (e.g. a `dynamic_cast`). Since explicit type casting is not natural
89+
in Python, ROOT attempts to automatically downcast raw pointer or smart pointer
90+
return values to their actual type. Demonstrating this with the types above:
91+
```python
92+
p1 = ROOT.foo()
93+
p2 = ROOT.foo_unique()
94+
95+
# if you absolutely need a base class proxy, there is a way:
96+
p3 = ROOT.bind_object(p1, "Base")
97+
98+
print(p1)
99+
print(p2)
100+
print(p3)
101+
```
102+
will give you something like:
103+
```txt
104+
<cppyy.gbl.Derived object>
105+
<cppyy.gbl.Derived object held by std::unique_ptr<Base>>
106+
<cppyy.gbl.Base object>
107+
```
108+
109+
**Note 1**: keep in mind that the auto downcasting also affects overload
110+
resolution. For example, consider these two overloads:
111+
```cpp
112+
void consume(Base *) {} // overload 1
113+
void consume(Derived *) {} // overload 2
114+
```
115+
In C++, `consume(foo())` will hit overload 1. In Python,
116+
`ROOT.consume(ROOT.foo())` resolves to the second overload because the pointee
117+
was automatically downcast. If you really need to hit the first `Base` overload, you'll have to explicitly cast back to the base class type with `ROOT.bind_object`, as shown before.
118+
119+
**Note 2**: while the type of the pointee gets downcast, smart pointer types
120+
remain unchanged. That's because `std::unique_ptr<Base>` and
121+
`std::unique_ptr<Derived>` are distinct, unrelated types. Template
122+
instantiations don't inherit from one another even when their type arguments do.
123+
124+
**Note 3**: automatic downcasting is not enabled
125+
unconditionally. It is only available for polymorphic base
126+
classes, which is why the `Base` class in the example has a virtual destructor.
127+
128+
129+
### Custom pythonizations for C++ user classes by example
8130
9131
This example shows how to use the `@pythonization` decorator to add extra
10132
behaviour to C++ user classes that are used from Python via PyROOT.
@@ -24,17 +146,17 @@ class MyClass {};
24146
~~~
25147
26148
Next, we define a pythonizor function: the function that will be responsible
27-
for injecting new behaviour in our C++ class `MyClass`.
149+
for injecting new behaviour in our C++ class `MyClass`.
28150
To convert a given Python function into a pythonizor, we need to decorate it
29151
with the @pythonization decorator. Such decorator allows us to define which
30152
which class we want to pythonize by providing its class name and its
31153
namespace (if the latter is not specified, it defaults to the global
32-
namespace, i.e. '::').
154+
namespace, i.e. '::').
33155
The decorated function - the pythonizor - must accept either one or two
34156
parameters:
35157
1. The class to be pythonized (proxy object where new behaviour can be
36158
injected)
37-
2. The fully-qualified name of that class (optional).
159+
2. The fully-qualified name of that class (optional).
38160
Let's see all this with a simple example. Suppose I would like to define how
39161
`MyClass` objects are represented as a string in Python (i.e. what would be
40162
shown when I print that object). For that purpose, I can define the following
@@ -113,7 +235,7 @@ for o in o1, o2:
113235
In addition, @pythonization also accepts prefixes of classes in a certain
114236
namespace in order to match multiple classes in that namespace. To signal that
115237
what we provide to @pythonization is a prefix, we need to set the `is_prefix`
116-
argument to `True` (default is `False`).
238+
argument to `True` (default is `False`).
117239
A common case where matching prefixes is useful is when we have a templated
118240
class and we want to pythonize all possible instantiations of that template.
119241
For example, we can pythonize the `std::vector` (templated) class like so:
@@ -199,7 +321,7 @@ first time in the application.
199321
However, it can also happen that our target class/es have already been
200322
accessed by the time we register a pythonization. In such a scenario, the
201323
pythonizor is applied immediately (at registration time) to the target
202-
class/es
324+
class/es
203325
Let's see an example of what was just explained. We will define a new class
204326
and immediately create an object of that class. We can check how the object
205327
still does not have a new attribute `pythonized` that we are going to inject
@@ -230,57 +352,3 @@ Now our object does have the `pythonized` attribute:
230352
~~~{.py}
231353
print(o.pythonized) # prints True
232354
~~~
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)