-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[doc] Fixathon 2: documentation sprint #22289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
siliataider
wants to merge
17
commits into
master
Choose a base branch
from
fixathon_docs
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
dcf02fb
[doxygen-infra] Call the main page "main page" in the navigation tree.
hageboeck 1984c7f
[doxygen] Fix doxygen warnings.
hageboeck 7d806c2
[doxygen-groups] Clean up doxygen groups.
hageboeck a256f17
[doc] Rename Core ROOT classes
siliataider 1f7884b
[doxygen-dataframe] Overhaul RDataFrame doxygen group
martinfoell 2012485
[doxygen-files] Remove RNTuple source files from the corresponding do…
hageboeck 61bcd4d
[doc][Python] Move the Python Interface section to top level
siliataider acfdbf1
[doc][Python] Add the Getting started page
siliataider 6ff4b92
[doc][Python] Add ML cheat sheet and doc page
siliataider 3886479
[doc][Python] Update ML section
siliataider 937f92b
[doc][Python] Update getting started section
siliataider b704d98
[doxygen-groups] Extend the description of the RVec group.
hageboeck a76151b
[doxygen] Remove file documentation pages from doxygen groups.
hageboeck 6982863
[doc][Python] Add UHI cheatsheet and update section
siliataider 3ece66b
[doxygen] Update Doxyfile and Makefile
hageboeck e608034
[doc][Python] Add doxygen docstrings to the ML section + refactoring
siliataider 9b486d3
[doc][Python] add _pythonization directory to Doxygen INPUT
siliataider File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,285 +1,119 @@ | ||
| \defgroup Pythonizations Python interface | ||
| \brief Python-specific functionalities offered by ROOT | ||
|
|
||
| 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. | ||
|
|
||
| ### Pythonization example | ||
|
|
||
| This example shows how to use the `@pythonization` decorator to add extra | ||
| behaviour to C++ user classes that are used from Python via PyROOT. | ||
| Let's first define a new C++ class. In this tutorial, we will see how we can | ||
| "pythonize" this class, i.e. how we can add some extra behaviour to it to | ||
| make it more pythonic or easier to use from Python. | ||
| Note: In this example, the class is defined dynamically for demonstration | ||
| purposes, but it could also be a C++ class defined in some library or header. | ||
| For more information about loading C++ user code to be used from Python with | ||
| PyROOT, please see: | ||
| https://root.cern.ch/manual/python#loading-user-libraries-and-just-in-time-compilation-jitting | ||
|
|
||
| ~~~{.py} | ||
| ROOT.gInterpreter.Declare(""" | ||
| class MyClass {}; | ||
| """) | ||
| ~~~ | ||
|
|
||
| Next, we define a pythonizor function: the function that will be responsible | ||
| for injecting new behaviour in our C++ class `MyClass`. | ||
| To convert a given Python function into a pythonizor, we need to decorate it | ||
| with the @pythonization decorator. Such decorator allows us to define which | ||
| which class we want to pythonize by providing its class name and its | ||
| namespace (if the latter is not specified, it defaults to the global | ||
| namespace, i.e. '::'). | ||
| The decorated function - the pythonizor - must accept either one or two | ||
| parameters: | ||
| 1. The class to be pythonized (proxy object where new behaviour can be | ||
| injected) | ||
| 2. The fully-qualified name of that class (optional). | ||
| Let's see all this with a simple example. Suppose I would like to define how | ||
| `MyClass` objects are represented as a string in Python (i.e. what would be | ||
| shown when I print that object). For that purpose, I can define the following | ||
| pythonizor function. There are two important things to be noted here: | ||
| - The @pythonization decorator has one argument that specifies our target | ||
| class is `MyClass`. | ||
| - The pythonizor function `pythonizor_of_myclass` provides and injects a new | ||
| implementation for `__str__`, the mechanism that Python provides to define | ||
| how to represent objects as strings. This new implementation | ||
| always returns the string "This is a MyClass object". | ||
|
|
||
| ~~~{.py} | ||
| @pythonization('MyClass') | ||
| def pythonizor_of_myclass(klass): | ||
| klass.__str__ = lambda o : 'This is a MyClass object' | ||
| ~~~ | ||
| Once we have defined our pythonizor function, let's see it in action. | ||
| We will now use the `MyClass` class for the first time from Python: we will | ||
| create a new instance of that class. At this moment, the pythonizor will | ||
| execute and modify the class - pythonizors are always lazily run when a given | ||
| class is used for the first time from a Python script. | ||
|
|
||
| ~~~{.py} | ||
| my_object = ROOT.MyClass() | ||
| ~~~ | ||
|
|
||
| Since the pythonizor already executed, we should now see the new behaviour. | ||
| For that purpose, let's print `my_object` (should show "This is a MyClass | ||
| object"). | ||
|
|
||
| ~~~{.py} | ||
| print(my_object) | ||
| ~~~ | ||
|
|
||
| The previous example is just a simple one, but there are many ways in which a | ||
| class can be pythonized. Typical examples are the redefinition of dunder | ||
| methods (e.g. `__iter__` and `__next__` to make your objects iterable from | ||
| Python). If you need some inspiration, many ROOT classes are pythonized in | ||
| the way we just saw; their pythonizations can be seen at: | ||
| <https://github.com/root-project/root/tree/master/bindings/pyroot/pythonizations/python/ROOT/_pythonization> | ||
| The @pythonization decorator offers a few more options when it comes to | ||
| matching classes that you want to pythonize. We saw that we can match a | ||
| single class, but we can also specify a list of classes to pythonize. | ||
| The following code defines a couple of new classes: | ||
|
|
||
| ~~~{.py} | ||
| ROOT.gInterpreter.Declare(""" | ||
| namespace NS { | ||
| class Class1 {}; | ||
| class Class2 {}; | ||
| \defgroup Python Python Interface | ||
| \ingroup Python | ||
| \brief Python bindings and utilities for ROOT. | ||
|
|
||
|
|
||
| ROOT is a C++ framework used across HEP for data storage, analysis and visualisation. Its full API is available directly in Python through dynamic bindings powered by [cppyy](https://cppyy.readthedocs.io/). Every ROOT class you see in the | ||
| C++ documentation is accessible from Python under the `ROOT` module. | ||
|
|
||
| On top of that, a set of @ref Pythonizations adapt selected classes to feel more natively Pythonic: operator overloading, iterators, NumPy interoperability, and more. | ||
|
|
||
|
|
||
| # Installation | ||
|
|
||
| \htmlonly | ||
| <div class="install-tabs"> | ||
| <div class="tab-buttons"> | ||
| <button class="tab-btn active" onclick="switchTab(this, 'conda')">conda</button> | ||
| <button class="tab-btn" onclick="switchTab(this, 'pip')">pip</button> | ||
| </div> | ||
| <div id="conda" class="tab-panel active"> | ||
| <pre><code>conda install -c conda-forge root</code></pre> | ||
| </div> | ||
| <div id="pip" class="tab-panel" style="display:none;"> | ||
| <pre><code>pip install root</code></pre> | ||
| <p style="margin:6px 12px 10px;font-size:12px;color:#b45309;background:#fffbeb; | ||
| border:1px solid #fcd34d;border-radius:4px;padding:6px 10px;"> | ||
| ⚠ Alpha - Linux only. | ||
| </p> | ||
| </div> | ||
| </div> | ||
|
|
||
| <style> | ||
| .install-tabs { | ||
| border: 1px solid var(--page-foreground-color, #ccc); | ||
| border-radius: 6px; | ||
| overflow: hidden; | ||
| max-width: 420px; | ||
| font-family: monospace; | ||
| } | ||
| """) | ||
| ~~~ | ||
|
|
||
| Note that these classes belong to the `NS` namespace. As mentioned above, the | ||
| @pythonization decorator accepts a parameter with the namespace of the class | ||
| or classes to be pythonized. Therefore, a pythonizor that matches both classes | ||
| would look like this: | ||
|
|
||
| ~~~{.py} | ||
| @pythonization(['Class1', 'Class2'], ns='NS') | ||
| def pythonize_two_classes(klass): | ||
| klass.new_attribute = 1 | ||
| ~~~ | ||
|
|
||
| Both classes will have the new attribute: | ||
|
|
||
| ~~~{.py} | ||
| o1 = ROOT.NS.Class1() | ||
| o2 = ROOT.NS.Class2() | ||
| print("Printing new attribute") | ||
| for o in o1, o2: | ||
| print(o.new_attribute) | ||
| ~~~ | ||
|
|
||
| In addition, @pythonization also accepts prefixes of classes in a certain | ||
| namespace in order to match multiple classes in that namespace. To signal that | ||
| what we provide to @pythonization is a prefix, we need to set the `is_prefix` | ||
| argument to `True` (default is `False`). | ||
| A common case where matching prefixes is useful is when we have a templated | ||
| class and we want to pythonize all possible instantiations of that template. | ||
| For example, we can pythonize the `std::vector` (templated) class like so: | ||
|
|
||
| ~~~{.py} | ||
| @pythonization('vector<', ns='std', is_prefix=True) | ||
| def vector_pythonizor(klass): | ||
| # first_elem returns the first element of the vector if it exists | ||
| klass.first_elem = lambda v : v[0] if v else None | ||
| ~~~ | ||
|
|
||
| Since we defined a prefix to do the match, the pythonization will be applied | ||
| both if we instantiate e.g. a vector of integers and a vector of doubles. | ||
|
|
||
| ~~~{.py} | ||
| v_int = ROOT.std.vector['int']([1,2,3]) | ||
| v_double = ROOT.std.vector['double']([4.,5.,6.]) | ||
| print("First element of integer vector: {}".format(v_int.first_elem())) | ||
| print("First element of double vector: {}".format(v_double.first_elem())) | ||
| ~~~ | ||
|
|
||
| These are some examples of combinations of prefixes and namespaces and the | ||
| corresponding classes that they match: | ||
| - '' : all classes in the global namespace. | ||
| - '', ns='NS1::NS2' : all classes in the `NS1::NS2` namespace. | ||
| - 'Prefix' : classes whose name starts with `Prefix` in the global namespace. | ||
| - 'Prefix', ns='NS' : classes whose name starts with `Prefix` in the `NS` | ||
| namespace | ||
| Moreover, a pythonizor function can have a second optional parameter that | ||
| contains the fully-qualified name of the class being pythonized. This can be | ||
| useful e.g. if we would like to do some more complex filtering of classes in | ||
| our pythonizor, for instance using regular expressions. | ||
|
|
||
| ~~~{.py} | ||
| @pythonization('pair<', ns='std', is_prefix=True) | ||
| def pair_pythonizor(klass, name): | ||
| print('Pythonizing class ' + name) | ||
| ~~~ | ||
|
|
||
| The pythonizor above will be applied to any instantiation of `std::pair` - we | ||
| can see this with the print we did inside the pythonizor. | ||
| Note that we could use the `name` parameter to e.g. further filter which | ||
| particular instantiations we would like to pythonize. | ||
|
|
||
| ~~~{.py} | ||
| p1 = ROOT.std.pair['int','int'](1,2) # prints 'Pythonizing class std::pair<int,int>' | ||
| p2 = ROOT.std.pair['int','double'](1,2.) # prints 'Pythonizing class std::pair<intdouble>' | ||
| ~~~ | ||
|
|
||
| Note that, to pythonize multiple classes in different namespaces, we can | ||
| stack multiple @pythonization decorators. For example, if we define these | ||
| classes: | ||
|
|
||
| ~~~{.py} | ||
| ROOT.gInterpreter.Declare(""" | ||
| class FirstClass {}; | ||
| namespace NS { | ||
| class SecondClass {}; | ||
| .tab-buttons { | ||
| display: flex; | ||
| background: var(--code-background, #f0f0f0); | ||
| border-bottom: 1px solid var(--page-foreground-color, #ccc); | ||
| } | ||
| """) | ||
| ~~~ | ||
|
|
||
| We can pythonize both of them with a single pythonizor function like so: | ||
|
|
||
| ~~~{.py} | ||
| @pythonization('FirstClass') | ||
| @pythonization('SecondClass', ns='NS') | ||
| def pythonizor_for_first_and_second(klass, name): | ||
| print('Executed for class ' + name) | ||
| ~~~ | ||
|
|
||
| If we now access both classes, we should see that the pythonizor runs twice. | ||
|
|
||
| ~~~{.py} | ||
| f = ROOT.FirstClass() | ||
| s = ROOT.NS.SecondClass() | ||
| ~~~ | ||
| .tab-btn { | ||
| padding: 6px 18px; | ||
| border: none; | ||
| background: none; | ||
| cursor: pointer; | ||
| font-size: 13px; | ||
| color: var(--page-foreground-color, #333); | ||
| border-bottom: 2px solid transparent; | ||
| } | ||
| .tab-btn.active { | ||
| border-bottom: 2px solid #1a73e8; | ||
| font-weight: 600; | ||
| color: #1a73e8; | ||
| } | ||
| .tab-panel pre { | ||
| margin: 0; | ||
| padding: 12px 16px; | ||
| background: var(--code-background, #fff); | ||
| color: var(--page-foreground-color, #333); | ||
| } | ||
| </style> | ||
|
|
||
| <script> | ||
| function switchTab(btn, id) { | ||
| document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); | ||
| document.querySelectorAll('.tab-panel').forEach(p => p.style.display = 'none'); | ||
| btn.classList.add('active'); | ||
| document.getElementById(id).style.display = 'block'; | ||
| } | ||
| </script> | ||
| \endhtmlonly | ||
|
|
||
| So far we have seen how pythonizations can be registered for classes that | ||
| have not been used yet. We have discussed how, in that case, the pythonizor | ||
| functions are executed lazily when their target class/es are used for the | ||
| first time in the application. | ||
| However, it can also happen that our target class/es have already been | ||
| accessed by the time we register a pythonization. In such a scenario, the | ||
| pythonizor is applied immediately (at registration time) to the target | ||
| class/es | ||
| Let's see an example of what was just explained. We will define a new class | ||
| and immediately create an object of that class. We can check how the object | ||
| still does not have a new attribute `pythonized` that we are going to inject | ||
| in the next step. | ||
| See <a href="https://root.cern/install" style="color:#b45309;">root.cern/install</a> for all installation options. | ||
|
|
||
| ~~~{.py} | ||
| ROOT.gInterpreter.Declare(""" | ||
| class MyClass2 {}; | ||
| """) | ||
| o = ROOT.MyClass2() | ||
| try: | ||
| print(o.pythonized) | ||
| except AttributeError: | ||
| print("Object has not been pythonized yet!") | ||
| ~~~ | ||
| # Quickstart | ||
|
|
||
| After that, we will register a pythonization for `MyClass2`. Since the class | ||
| has already been used, the pythonization will happen right away. | ||
| The entry point to ROOT in Python is one import: | ||
|
|
||
| ~~~{.py} | ||
| @pythonization('MyClass2') | ||
| def pythonizor_for_myclass2(klass): | ||
| klass.pythonized = True | ||
| import ROOT | ||
| ~~~ | ||
|
|
||
| Now our object does have the `pythonized` attribute: | ||
| Every ROOT class and function is available under the `ROOT` module. | ||
|
|
||
| ~~~{.py} | ||
| print(o.pythonized) # prints True | ||
| ~~~ | ||
|
|
||
| ### Pythonization printing example | ||
| This example illustrates the pretty printing feature of PyROOT, which reveals | ||
| the content of the object if a string representation is requested, e.g., by | ||
| Python's print statement. The printing behaves similar to the ROOT prompt | ||
| powered by the C++ interpreter cling. | ||
| Create an object with PyROOT | ||
| Now let's create a histogram, fill it from a [NumPy array](https://numpy.org/devdocs/reference/generated/numpy.ndarray.html) and write it to a file: | ||
|
|
||
| ~~~{.py} | ||
| obj = ROOT.std.vector("int")(3) | ||
| for i in range(obj.size()): | ||
| obj[i] = i | ||
| ~~~ | ||
| import numpy as np | ||
|
|
||
| Print the object, which reveals the content. Note that `print` calls the special | ||
| method `__str__` of the object internally. | ||
| # Create a 1D histogram | ||
| h = ROOT.TH1D("h", "Gaussian distribution;x;counts", 100, -5, 5) | ||
|
|
||
| ~~~{.py} | ||
| print(obj) | ||
| ~~~ | ||
|
|
||
| The output can be retrieved as string by any function that triggers the `__str__` | ||
| special method of the object, e.g., `str` or `format`. | ||
| # Fill it from a NumPy array | ||
| data = np.random.normal(0, 1, 10000) | ||
| h.Fill(data) | ||
|
|
||
| ~~~{.py} | ||
| print(str(obj)) | ||
| print("{}".format(obj)) | ||
| # Write it to a ROOT file | ||
| with ROOT.TFile.Open("output.root", "RECREATE") as f: | ||
| f.WriteObject(h, "my_histogram") | ||
| ~~~ | ||
|
|
||
| Note that the interactive Python prompt does not call `__str__`, it calls | ||
| `__repr__`, which implements a formal and unique string representation of | ||
| the object. | ||
| Now we create an @ref dataframe - ROOT's high-level interface for columnar data analysis - from scratch, define a new column and draw a histogram: | ||
|
|
||
| ~~~{.py} | ||
| print(repr(obj)) | ||
| obj | ||
| ~~~ | ||
| import numpy as np | ||
|
|
||
| The print output behaves similar to the ROOT prompt, e.g., here for a ROOT histogram. | ||
| # Create an RDataFrame with 10000 rows | ||
| rdf = ROOT.RDataFrame(10000) | ||
|
|
||
| ~~~{.py} | ||
| hist = ROOT.TH1F("name", "title", 10, 0, 1) | ||
| print(hist) | ||
| ~~~ | ||
| # Define a column x representing a normal distribution | ||
| rdf = rdf.Define("x", "gRandom->Gaus(0, 1)") | ||
|
|
||
| If cling cannot produce any nice representation for the class, we fall back to a | ||
| "<ClassName at address>" format, which is what `__repr__` returns | ||
|
|
||
| ~~~{.py} | ||
| ROOT.gInterpreter.Declare('class MyClass {};') | ||
| m = ROOT.MyClass() | ||
| print(m) | ||
| print(str(m) == repr(m)) | ||
| # Draw a histogram of x | ||
| rdf.Histo1D("x").Draw() | ||
| ~~~ | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.