2.5 Orientación a objetos

De manera un tanto vaga, la Programación Orientada a Objetos (POO) se refiere a un paradigma de programación en el que los usuarios/desarrolladores pueden crear objetos de una determinada “clase” (que deben tener una determinada estructura) y luego aplicar “métodos” para determinadas “funciones genéricas” a dichos objetos. Un simple ejemplo en R es la función summary(), que es una función genérica que elige, dependiendo de la clase de su argumento, el método de resumen definido para dicha clase. Por ejemplo, para el vector numérico x y el factor g usado arriba:

x <- c(1.8, 3.14, 4, 88.169, 13)
g <- factor(rep(c(0, 1), c(2, 4)), levels = c(0, 1), labels = c("male", "female"))

La llamada a la función summary() produce diferentes tipos de resultados:

summary(x)
#>    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
#>    1.80    3.14    4.00   22.02   13.00   88.17
summary(g)
#>   male female 
#>      2      4

Para el vector numérico x, se informa un resumen de cinco números (es decir, el mínimo, el máximo, la mediana, el primer y tercer cuartil) junto con la media, mientras que para el factor g devuelve una tabla de frecuencias simple. Esto muestra que R tiene diferente métodos disponibles para summary() dependiendo de los tipos de clases (en particular, sabe que un resumen de cinco números no es sensato para las variables categóricas). En R, cada objeto tiene una clase que se puede consultar usando la función class():

class(x)
#> [1] "numeric"
class(g)
#> [1] "factor"

La clase se utiliza internamente para llamar al método apropiado para una función genérica. De echo, R ofrece varios paradigmas de orientación a objetos. La instalación básica viene con dos sistemas POO diferentes, generalmente llamados S3 (Chambers y Hastie 1992) y S4 (Chambers 1998). La S3 es mucho más simple, utilizando un mecanismo de distribución basado en una convención de nomenclatura para los métodos. La S4 es más sofisticada y más cercana a otros conceptos de programación orientada a objetos utilizados en informática, pero también requiere más disciplina y experiencia. Para la mayoría de las tareas, S3 es suficiente y, por tanto, es el único sistema POO (brevemente) discutido aquí.

En S3, una función genérica se define como una función con una determinada lista de argumentos y luego una llamada UseMethod() con el nombre de la función genérica. Por ejemplo, imprimir la función summary() revela su definición:

summary
#> function (object, ...) 
#> UseMethod("summary")
#> <bytecode: 0x0000000015e7b230>
#> <environment: namespace:base>

Se necesita un primer objeto, como argumento obligatorio, más un número arbitrario de argumentos adicionales pasados a sus métodos. ¿Qué sucede si esta función se aplica a un objeto de la clase “foo?” En este contexto, R intenta aplicar la función summary.foo() si existiera. Si no, llamará summary.default() si existe tal método predeterminado (que es el caso de summary()).

Además, los objetos en R pueden tener un vector de clases (por ejemplo, c(“foo,” “bar”), lo que implica que esos objetos son de clase “foo” heredado de “bar”). En este caso, R hace los primeros intentos de aplicar summary.foo(), entonces (si esto no existe) summary.bar(), y luego (si ambos no existen) summary.default(). Todos los métodos que son actualmente definidos para una función genérica se pueden consultar usando methods(); por ejemplo, methods(summary) devolverá una lista (larga) de métodos para todo tipo de clases diferentes.

Entre los métodos disponibles a partir de las clases, existe un método summary.factor(), que se usa cuando se llama a summary(g). Sin embargo, no existe summary.numeric(); por ende, summary(x) es manejado por summary.default(). Como no se recomienda llamar a métodos directamente, algunos métodos están marcados como no visibles para el usuario y no se pueden llamar directamente (fácilmente). Sin embargo, incluso para los métodos visibles, se hace hincapié en que en la mayoría de las situaciones es claramente preferible su uso; por ejemplo, summary(g) en vez de summary.factor(g).

Para ilustrar lo fácil que es definir una clase y algunos métodos para ella, se considera un ejemplo simple. Se creará un objeto de clase “normsample,” que contiene una muestra de una distribución normal y luego se define el método summary() que informa la media empírica y la desviación estándar para dicha muestra. Primero, se codifica un creador de clases simple. En principio, podría tener cualquier nombre, pero a menudo se llaman como la propia clase:

normsample <- function(n, ...) {
    rval <- rnorm(n, ...)
    class(rval) <- "normsample"
    return(rval)
    }

Esta función toma un argumento requerido n ( el tamaño de la muestra) y otros argumentos , que se transmiten a rnorm(), la función para generar números aleatorios normales. Además del tamaño de la muestra, se necesitan más argumentos (la media y desviación estándar). Se recomienda consultar ?rnorm. Después de la generación del vector de números aleatorios normales, se le asigna la clase “normsample” Y luego se prueba:

set.seed(123)
x <- normsample(10, mean = 5)
class(x)
#> [1] "normsample"

Para definir un método summary(), se crea una función summary.normsample() que se ajusta a la lista de argumentos del genérico (aunque no se usa aquí) y calcula el tamaño de la muestra, la media empírica y la desviación estándar.

summary.normsample <- function(object, ...) {
    rval <- c(length(object), mean(object), sd(object))
    names(rval) <- c("sample size","mean","standard deviation")
    return(rval)
    }

En consecuencia, llamando a summary(x) se puede obtener automáticamente el nuevo método summary() y se produce el resultado deseado:

summary(x)
#>        sample size               mean standard deviation 
#>         10.0000000          5.0746256          0.9537841

Otras funciones genéricas con métodos para la mayoría de clases estándar en R son print(), plot() y str(), que imprimen, trazan y resumen la estructura de un objeto, respectivamente.