R’s R6 classes in rpy2/Python¶
Repository: https://github.com/rpy2/rpy2-R6
Introduction¶
[R6](https://r6.r-lib.org/) is one of the OOP systems available to R programmers, and is relatively similar to other OOP systems non-R programmers might be familiar with. In the R6 authors’ own words “this style of programming is also sometimes referred to as classical object-oriented programming”.
This Python package is using rpy2
to provide Pythonic
wrappers for R6 classes and instances, and make the integration
of R6-based class models into Python code as natural and as
readable as possible.
OOP in R’s R6 is relatively different from the way Python classes are defined and are working. In R6 a class definition is an instance of class ClassFactoryGenerator, that has at one method (new()) acting as a factory that can create instances inheriting from class R6. One can see this a some form of metaprogramming, with instances class ClassFactorGenerator acting like metaclasses by defining the corresponding child classes dynamically.
Despite the challenge, we are trying to have a wrapper
that is both faithful to the R code, since this can make
debugging or using the R documentation easier, while also
allowing a rather Pythonic feel. Currently there are two alternative wrappers:
rpy2_r6.r6a
and rpy2_r6.r6b
.
A first example¶
To demonstrate how it is working, we use the R package scales (a dependency of ggplot2).
import rpy2.rinterface
import rpy2.robjects
from rpy2.robjects.packages import importr
scales = importr('scales')
In this short tutorial we start with Range, an instance of class ClassFactoryGenerator in the package scales, and we wrap it to be an instance of our matching Python class.
In R, creating a new instance of Range would be done with:
library(scales)
obj <- Range$new()
That instance obj is of R class (“Range”, “R6”), meaning that this is an instance of class “Range” inheriting from class “R6” (on the R side).
> class(Range$new())
c("Range", "R6")
r6a¶
The wrapper was kindly contributed in a PR for rpy2 by Matthew Wardrop (@matthewwardrop), and was only slightly adapted to use the recent SupportsSEXP interface in rpy2.
In this approach the class ClassFactoryGenerator in R is mapped to rpy2_r6.r6a.R6Class
.
range_factory_r6a = r6a.R6Class(scales.Range)
range_r6a = range_factory_r6a.new()
The instance is a generic rpy2_r6.r6a.R6
in Python, with the R class name available
through a property of that object:
>>> type(range_r6a)
rpy2_r6.r6a.R6
>>> range_r6b.class_name
'Scale'
The properties and methods available for the object in R are dynamically resolved from the Python side, and private attributes in the R6 definitions have an underscore prefixed to their attribute name in Python. For example, the private method R6 clone for the object is accessed with _clone in Python:
range_r6a._clone()
r6b¶
range_factory_r6b = r6b.R6DynamicClassGenerator(scales.Range)
Note
Automatic wrapping could be achieved through rpy2’s own conversion system. It is planned to offer the option to facilitate this in this package.
We are able to write essentially the same code in Python:
range_r6b = range_factory_r6b.new()
The type of the resulting object is a Python class Range:
>>> type(range_r6b)
rpy2_r6.r6b.Range
Dynamic class generation¶
You’ll note that we never explicitly defined the class Range; it was dynamically created by our package to reflect the R class definition from the ClassFactoryGenerator instance.
The method new is a factory:
myrange = range_factory_r6b.new
The underlying class is:
Range = range_factory_r6b.__R6CLASS__
The lineage (inheritance tree) for the Python class Range is dynamically generated to match the one for the R6 class definition in R.
>>> import inspect
>>> inspect.getmro(Range)
(rpy2_r6.r6b.Range,
rpy2_r6.r6b.R6,
rpy2.rinterface_lib.sexp.SupportsSEXP,
object)
An other example with a longer lineage:
>>> DiscreteRange = r6b.R6DynamicClassGenerator(scales.DiscreteRange).new
>>> inspect.getmro(DiscreteRange)
(rpy2_r6.r6b.DiscreteRange,
rpy2_r6.r6b.Range,
rpy2_r6.r6b.R6,
rpy2.rinterface_lib.sexp.SupportsSEXP,
object)