3.11: Objetos y Clases en R
- Page ID
- 55127
\( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \)
\( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)
\( \newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\)
( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\)
\( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\)
\( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\)
\( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\)
\( \newcommand{\Span}{\mathrm{span}}\)
\( \newcommand{\id}{\mathrm{id}}\)
\( \newcommand{\Span}{\mathrm{span}}\)
\( \newcommand{\kernel}{\mathrm{null}\,}\)
\( \newcommand{\range}{\mathrm{range}\,}\)
\( \newcommand{\RealPart}{\mathrm{Re}}\)
\( \newcommand{\ImaginaryPart}{\mathrm{Im}}\)
\( \newcommand{\Argument}{\mathrm{Arg}}\)
\( \newcommand{\norm}[1]{\| #1 \|}\)
\( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\)
\( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\AA}{\unicode[.8,0]{x212B}}\)
\( \newcommand{\vectorA}[1]{\vec{#1}} % arrow\)
\( \newcommand{\vectorAt}[1]{\vec{\text{#1}}} % arrow\)
\( \newcommand{\vectorB}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \)
\( \newcommand{\vectorC}[1]{\textbf{#1}} \)
\( \newcommand{\vectorD}[1]{\overrightarrow{#1}} \)
\( \newcommand{\vectorDt}[1]{\overrightarrow{\text{#1}}} \)
\( \newcommand{\vectE}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash{\mathbf {#1}}}} \)
\( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \)
\( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)
\(\newcommand{\avec}{\mathbf a}\) \(\newcommand{\bvec}{\mathbf b}\) \(\newcommand{\cvec}{\mathbf c}\) \(\newcommand{\dvec}{\mathbf d}\) \(\newcommand{\dtil}{\widetilde{\mathbf d}}\) \(\newcommand{\evec}{\mathbf e}\) \(\newcommand{\fvec}{\mathbf f}\) \(\newcommand{\nvec}{\mathbf n}\) \(\newcommand{\pvec}{\mathbf p}\) \(\newcommand{\qvec}{\mathbf q}\) \(\newcommand{\svec}{\mathbf s}\) \(\newcommand{\tvec}{\mathbf t}\) \(\newcommand{\uvec}{\mathbf u}\) \(\newcommand{\vvec}{\mathbf v}\) \(\newcommand{\wvec}{\mathbf w}\) \(\newcommand{\xvec}{\mathbf x}\) \(\newcommand{\yvec}{\mathbf y}\) \(\newcommand{\zvec}{\mathbf z}\) \(\newcommand{\rvec}{\mathbf r}\) \(\newcommand{\mvec}{\mathbf m}\) \(\newcommand{\zerovec}{\mathbf 0}\) \(\newcommand{\onevec}{\mathbf 1}\) \(\newcommand{\real}{\mathbb R}\) \(\newcommand{\twovec}[2]{\left[\begin{array}{r}#1 \\ #2 \end{array}\right]}\) \(\newcommand{\ctwovec}[2]{\left[\begin{array}{c}#1 \\ #2 \end{array}\right]}\) \(\newcommand{\threevec}[3]{\left[\begin{array}{r}#1 \\ #2 \\ #3 \end{array}\right]}\) \(\newcommand{\cthreevec}[3]{\left[\begin{array}{c}#1 \\ #2 \\ #3 \end{array}\right]}\) \(\newcommand{\fourvec}[4]{\left[\begin{array}{r}#1 \\ #2 \\ #3 \\ #4 \end{array}\right]}\) \(\newcommand{\cfourvec}[4]{\left[\begin{array}{c}#1 \\ #2 \\ #3 \\ #4 \end{array}\right]}\) \(\newcommand{\fivevec}[5]{\left[\begin{array}{r}#1 \\ #2 \\ #3 \\ #4 \\ #5 \\ \end{array}\right]}\) \(\newcommand{\cfivevec}[5]{\left[\begin{array}{c}#1 \\ #2 \\ #3 \\ #4 \\ #5 \\ \end{array}\right]}\) \(\newcommand{\mattwo}[4]{\left[\begin{array}{rr}#1 \amp #2 \\ #3 \amp #4 \\ \end{array}\right]}\) \(\newcommand{\laspan}[1]{\text{Span}\{#1\}}\) \(\newcommand{\bcal}{\cal B}\) \(\newcommand{\ccal}{\cal C}\) \(\newcommand{\scal}{\cal S}\) \(\newcommand{\wcal}{\cal W}\) \(\newcommand{\ecal}{\cal E}\) \(\newcommand{\coords}[2]{\left\{#1\right\}_{#2}}\) \(\newcommand{\gray}[1]{\color{gray}{#1}}\) \(\newcommand{\lgray}[1]{\color{lightgray}{#1}}\) \(\newcommand{\rank}{\operatorname{rank}}\) \(\newcommand{\row}{\text{Row}}\) \(\newcommand{\col}{\text{Col}}\) \(\renewcommand{\row}{\text{Row}}\) \(\newcommand{\nul}{\text{Nul}}\) \(\newcommand{\var}{\text{Var}}\) \(\newcommand{\corr}{\text{corr}}\) \(\newcommand{\len}[1]{\left|#1\right|}\) \(\newcommand{\bbar}{\overline{\bvec}}\) \(\newcommand{\bhat}{\widehat{\bvec}}\) \(\newcommand{\bperp}{\bvec^\perp}\) \(\newcommand{\xhat}{\widehat{\xvec}}\) \(\newcommand{\vhat}{\widehat{\vvec}}\) \(\newcommand{\uhat}{\widehat{\uvec}}\) \(\newcommand{\what}{\widehat{\wvec}}\) \(\newcommand{\Sighat}{\widehat{\Sigma}}\) \(\newcommand{\lt}{<}\) \(\newcommand{\gt}{>}\) \(\newcommand{\amp}{&}\) \(\definecolor{fillinmathshade}{gray}{0.9}\)En los capítulos que cubren Python, pasamos bastante tiempo discutiendo objetos y sus planos, conocidos como clases. En términos generales, un objeto es una colección de datos junto con funciones (llamadas “métodos” en este contexto) diseñadas específicamente para trabajar en esos datos. Las clases comprenden las definiciones de esos métodos y datos.
Resulta que, si bien las funciones son el foco principal en R, los objetos también son una parte importante del lenguaje. (De ninguna manera ninguno de estos conceptos son mutuamente excluyentes.) Si bien las definiciones de clase están muy bien encapsuladas en Python, en R, las piezas están más distribuidas, al menos para el sistema “S3” más antiguo y más comúnmente utilizado lo discutiremos aquí. [1] Teniendo esto en cuenta, lo mejor es examinar algunos objetos y métodos existentes antes de intentar diseñar los nuestros propios. Consideremos la creación de un modelo pequeño y lineal para algunos datos de muestra.

En el capítulo 30, “Listas y atributos”, aprendimos que funciones como lm ()
y anova ()
generalmente devuelven una lista (o un marco de datos, que es un tipo de lista), y podemos inspeccionar la estructura con str ()
.

Aquí hay un muestreo de las líneas de salida para cada llamada (hay bastantes piezas de datos contenidas en la lista lm_result
):

Si estos dos resultados son tan similares, ambos tipos de listas, entonces, ¿por qué las salidas son tan diferentes cuando llamamos a print (lm_result)

e imprimir (anova_result)
?

La forma en que se producen estas impresiones viene dictada por el atributo “class”
de estas listas, “lm”
para lm_result
y “anova”
para anova_result
. Si tuviéramos que eliminar este atributo, obtendríamos una salida impresa predeterminada similar al resultado de str ()
. Hay varias formas de modificar o eliminar el atributo class de un dato: usando la función de acceso attr ()
con attr (lm_result, “class”) <- NULL
, configurándola usando el accessor class ()
más preferido, como en class (lm_result) <- NULL
, o usando la función unclass ()
aún más especializada, como en lm_result <- unclass (lm_result)
. En cualquier caso, ejecutar print (lm_result)
después de una de estas tres opciones dará como resultado str ()
como impresión predeterminada.
Ahora bien, ¿cómo produce R una salida diferente en función de este atributo de clase? Cuando llamamos print (lm_result)
, el intérprete nota que el atributo “class”
está establecido en “lm”
, y busca otra función con un nombre diferente para ejecutar realmente: print.lm ()
. Del mismo modo, print (anova_result)
llama a print.anova ()
sobre la base de la clase de la entrada. Estas funciones especializadas asumen que la lista de entrada tendrá ciertos elementos y producirá una salida específica para esos datos. Podemos ver esto tratando de confundir R estableciendo el atributo class incorrectamente con class (anova_result) <- “lm”
y luego print (anova_result)
:

Observe que los nombres de clase forman parte de los nombres de las funciones. Esta es la forma de R de crear métodos, afirmando que los objetos con clase “x”
deben imprimirse con print.x ()
; esto se conoce como despacho y la función general print ()
se conoce como función genérica, cuyo propósito es despachar a un método apropiado (función específica de clase) basado en el atributo class de la entrada.
En resumen, cuando llamamos print (result)
en R, porque print ()
es una función genérica, el intérprete comprueba el atributo “class”
de result
; supongamos que la clase es “x”
. Si existe un print.x ()
, se llamará a esa función; de lo contrario, la impresión volverá a print.default ()
, que produce una salida similar a str ()
.

Hay muchas “impresiones” diferentes.
métodos; podemos verlos con métodos (“print”)
.

De igual manera, hay una variedad de métodos “.lm”
especializados en tratar datos que tienen un atributo de “clase”
de “lm”
. Podemos verlas con métodos (class = “lm”)
.

El mensaje sobre las funciones no visibles que están siendo asteriscadas indica que, si bien estas funciones existen, no podemos llamarlas directamente como en print.lm (lm_result)
; debemos usar el genérico print ()
. Muchas funciones que hemos tratado son en realidad genéricos, incluyendo length ()
, mean ()
, hist ()
e incluso str ()
.
Entonces, a su manera R, también está bastante “orientado a objetos”. Una lista (u otro tipo, como un vector o un marco de datos) con un atributo de clase dado constituye un objeto, y los diversos métodos especializados forman parte de la definición de clase.
Creando nuestras propias clases
Crear nuevos tipos de objetos y métodos no es algo que los programadores principiantes de R probablemente hagan a menudo. Aún así, un ejemplo descubrirá más del funcionamiento interno de R y bien podría ser útil.
Primero, necesitaremos algún tipo de datos que queramos representar con un objeto. Para fines ilustrativos, utilizaremos los datos devueltos por la función nrorm_trunc ()
definida en el capítulo 35, “Programación estructural”. En lugar de producir un vector de muestras, también podríamos querer almacenar con ese vector la media del muestreo original y la desviación estándar (porque los datos truncados tendrán una media real y una desviación estándar diferentes). También podríamos desear almacenar en este objeto los límites superior e inferior solicitados. Debido a que todos estos datos son de diferentes tipos, tiene sentido almacenarlos en una lista.

La función anterior devuelve una lista con los diversos elementos, incluyendo la propia muestra. También establece el atributo class de la lista en truncated_normal_sample
—by convention, este atributo class es el mismo que el nombre de la función. Tal función que crea y devuelve un objeto con una clase definida se llama constructor.
Ahora, podemos crear una instancia de un objeto “truncated_normal_sample”
e imprimirlo.

Debido a que no hay ninguna función print.truncated_normal_sample ()
, sin embargo, el genérico print ()
despacha a print.default ()
, y la salida no es agradable.

Si queremos estilizar la impresión, necesitamos crear el método personalizado. También podríamos querer crear una función mean ()
personalizada que devuelva la media de la muestra almacenada.

La salida:

Esta función de impresión personalizada es bastante cruda; técnicas de impresión más sofisticadas (como cat ()
y paste ()
) podrían usarse para producir una salida más amigable.
Hasta ahora, hemos definido un método personalizado mean.truncated_normal_sample ()
, que devuelve la media de la muestra cuando llamamos a la función genérica mean ()
. Esto funciona porque la función genérica mean ()
ya existe en R. ¿Y si quisiéramos llamar a un genérico llamado originalmean ()
, que devuelve original_mean
del objeto? En este caso, necesitamos crear nuestro propio método especializado así como la función genérica que despacha a ese método. Así es como se ve eso:

Estas funciones, el constructor, los métodos especializados y las funciones genéricas que aún no existen en R, necesitan definirse solo una vez, pero se pueden llamar tantas veces como queramos. De hecho, los paquetes en R que se instalan usando install.packages ()
a menudo son solo una colección de funciones, junto con documentación y otros materiales.
La programación orientada a objetos es un tema grande, y solo hemos rayado la superficie. En particular, no hemos cubierto temas como el polimorfismo, donde un objeto puede tener múltiples clases listadas en el atributo “class”
. En R, el tema del polimorfismo no es difícil de describir en un sentido técnico, aunque hacer un uso efectivo del mismo es un desafío en la ingeniería de software. Si un objeto tiene varias clases, como “anova”
y “data.frame”
, y se llama a un genérico como print ()
, el intérprete primero buscará print.anova ()
, y si eso falla, intentará print.data.frame ()
, y en su defecto retrocederá en print.default ()
. Esto permite a los objetos capturar “es un tipo de” relaciones, por lo que los métodos que funcionan con marcos de datos no tienen que ser reescritos para los objetos de clase anova.
Ejercicios
- Muchas funciones en R son genéricas, incluyendo (como exploraremos en el capítulo 37, “Ploting Data y
ggplot2
”) la funciónplot ()
, que produce salida gráfica. ¿Cuáles son todas las diferentes clases que se pueden trazar con laparcela genérica ()
? Un ejemplo esplot.lm ()
; usehelp (“plot.lm”)
para determinar qué se traza cuando se le da una entrada con el atributo class de“lm”
. - ¿Qué métodos están disponibles para los datos con un atributo de clase de
“matriz”
? (Por ejemplo, ¿hay unplot.matrix ()
olm.matrix ()
? ¿Qué otros hay?) - Crea tu propia clase de algún tipo, completa con un constructor que devuelve una lista con su conjunto de atributos de clase, un método especializado para
print ()
y un nuevo método genérico y asociado. - Explore usando otros recursos la diferencia entre el sistema de objetos S3 de R y su sistema de objetos S4.
-
Las versiones modernas de R no tienen uno, no dos, sino tres sistemas diferentes para crear y trabajar con objetos. Estaremos discutiendo solo el más antiguo y aún más usado, conocido como S3. Los otros dos se llaman S4 y Clases de Referencia, esta última de las cuales es más similar al sistema clase/objeto utilizado por Python. Para obtener más información sobre estos y otros sistemas de objetos (y muchos otros temas avanzados de R), consulte Norman Matloff, The Art of R Programming (San Francisco: No Starch Press, 2011), y Hadley Wickham, Advanced R (Londres: Chapman y Hall/CRC, 2014).