library(sloop)
13 S3
13.1 Introducción
S3 es el primer y más simple sistema OO de R. S3 es informal y ad hoc, pero hay cierta elegancia en su minimalismo: no se le puede quitar ninguna parte y seguir teniendo un sistema OO útil. Por estas razones, debe usarlo, a menos que tenga una razón convincente para hacerlo de otra manera. S3 es el único sistema OO utilizado en los paquetes base y stats, y es el sistema más utilizado en los paquetes CRAN.
S3 es muy flexible, lo que significa que te permite hacer cosas que son bastante desaconsejables. Si viene de un entorno estricto como Java, esto parecerá bastante aterrador, pero le da a los programadores de R una gran libertad. Puede ser muy difícil evitar que las personas hagan algo que usted no quiere que hagan, pero sus usuarios nunca se detendrán porque hay algo que aún no ha implementado. Dado que S3 tiene pocas restricciones integradas, la clave para su uso exitoso es aplicar las restricciones usted mismo. Por lo tanto, este capítulo le enseñará las convenciones que debe seguir (casi) siempre.
El objetivo de este capítulo es mostrarle cómo funciona el sistema S3, no cómo usarlo de manera efectiva para crear nuevas clases y genéricos. Recomiendo combinar el conocimiento teórico de este capítulo con el conocimiento práctico codificado en el paquete vctrs.
Estructura
La Sección 13.2 brinda una descripción general rápida de todos los componentes principales de S3: clases, genéricos y métodos. También aprenderá sobre
sloop::s3_dispatch()
, que usaremos a lo largo del capítulo para explorar cómo funciona S3.La Sección 13.3 entra en los detalles de la creación de una nueva clase S3, incluidas las tres funciones que deberían acompañar a la mayoría de las clases: un constructor, un ayudante y un validador.
La Sección 13.4 describe cómo funcionan los métodos y genéricos de S3, incluidos los aspectos básicos del envío de métodos.
La Sección 13.5 analiza los cuatro estilos principales de los objetos de S3: vector, registro, marco de datos y escalar.
La Sección 13.6 demuestra cómo funciona la herencia en S3 y le muestra lo que necesita para hacer que una clase sea “subclasificable”.
La Sección 13.7 concluye el capítulo con una discusión de los detalles más finos del envío de métodos, incluidos los tipos base, los genéricos internos, los genéricos de grupo y el envío doble.
Requisitos previos
Las clases de S3 se implementan mediante atributos, así que asegúrese de estar familiarizado con los detalles descritos en la Sección 3.3. Usaremos vectores S3 base existentes para ejemplos y exploración, así que asegúrese de estar familiarizado con las clases factor, Date, difftime, POSIXct y POSIXlt descritas en la Sección 3.4.
Usaremos el paquete sloop para sus ayudantes interactivos.
13.2 Lo esencial
Un objeto S3 es un tipo base con al menos un atributo de “clase” (se pueden usar otros atributos para almacenar otros datos). Por ejemplo, tome el factor. Su tipo base es el vector entero, tiene un atributo clase
de “factor”, y un atributo niveles
que almacena los niveles posibles:
<- factor(c("a", "b", "c"))
f
typeof(f)
#> [1] "integer"
attributes(f)
#> $levels
#> [1] "a" "b" "c"
#>
#> $class
#> [1] "factor"
Puede obtener el tipo base subyacente al unclass()
, lo que elimina el atributo de clase, lo que hace que pierda su comportamiento especial:
unclass(f)
#> [1] 1 2 3
#> attr(,"levels")
#> [1] "a" "b" "c"
Un objeto de S3 se comporta de manera diferente a su tipo base subyacente cada vez que se pasa a un genérico (abreviatura de función genérica). La forma más fácil de saber si una función es genérica es usar sloop::ftype()
y buscar “genérica” en la salida:
ftype(print)
#> [1] "S3" "generic"
ftype(str)
#> [1] "S3" "generic"
ftype(unclass)
#> [1] "primitive"
Una función genérica define una interfaz, que utiliza una implementación diferente según la clase de un argumento (casi siempre el primer argumento). Muchas funciones básicas de R son genéricas, incluida la importante print()
:
print(f)
#> [1] a b c
#> Levels: a b c
# la eliminación de clase vuelve al comportamiento de entero
print(unclass(f))
#> [1] 1 2 3
#> attr(,"levels")
#> [1] "a" "b" "c"
Tenga en cuenta que str()
es genérico, y algunas clases de S3 usan ese genérico para ocultar los detalles internos. Por ejemplo, la clase POSIXlt
que se usa para representar datos de fecha y hora en realidad está construida encima de una lista, un hecho que está oculto por su método str()
:
<- strptime(c("2017-01-01", "2020-05-04 03:21"), "%Y-%m-%d")
time str(time)
#> POSIXlt[1:2], format: "2017-01-01" "2020-05-04"
str(unclass(time))
#> List of 11
#> $ sec : num [1:2] 0 0
#> $ min : int [1:2] 0 0
#> $ hour : int [1:2] 0 0
#> $ mday : int [1:2] 1 4
#> $ mon : int [1:2] 0 4
#> $ year : int [1:2] 117 120
#> $ wday : int [1:2] 0 1
#> $ yday : int [1:2] 0 124
#> $ isdst : int [1:2] 0 0
#> $ zone : chr [1:2] "UTC" "UTC"
#> $ gmtoff: int [1:2] 0 0
#> - attr(*, "tzone")= chr "UTC"
#> - attr(*, "balanced")= logi TRUE
El genérico es un intermediario: su trabajo es definir la interfaz (es decir, los argumentos) y luego encontrar la implementación correcta para el trabajo. La implementación para una clase específica se denomina método, y el genérico encuentra ese método realizando despacho de métodos.
Puede usar sloop::s3_dispatch()
para ver el proceso de envío del método:
s3_dispatch(print(f))
#> => print.factor
#> * print.default
Volveremos a los detalles del envío en la Sección 13.4.1, por ahora tenga en cuenta que los métodos S3 son funciones con un esquema de nombres especial, generic.class()
. Por ejemplo, el método factor
para el genérico print()
se llama print.factor()
. Nunca debe llamar al método directamente, sino confiar en el genérico para encontrarlo por usted.
En general, puede identificar un método por la presencia de .
en el nombre de la función, pero hay una serie de funciones importantes en base R que se escribieron antes de S3 y, por lo tanto, usan .
para unir palabras. Si no está seguro, verifique con sloop::ftype()
:
ftype(t.test)
#> [1] "S3" "generic"
ftype(t.data.frame)
#> [1] "S3" "method"
A diferencia de la mayoría de las funciones, no puede ver el código fuente de la mayoría de los métodos S3 1 simplemente escribiendo sus nombres. Esto se debe a que los métodos de S3 generalmente no se exportan: viven solo dentro del paquete y no están disponibles en el entorno global. En su lugar, puede usar sloop::s3_get_method()
, que funcionará independientemente de dónde resida el método:
weighted.mean.Date#> Error in eval(expr, envir, enclos): object 'weighted.mean.Date' not found
s3_get_method(weighted.mean.Date)
#> function (x, w, ...)
#> .Date(weighted.mean(unclass(x), w, ...))
#> <bytecode: 0x556daf77d0d8>
#> <environment: namespace:stats>
13.2.1 Ejercicios
Describe la diferencia entre
t.test()
yt.data.frame()
. ¿Cuándo se llama cada función?Haga una lista de las funciones básicas de R que se usan comúnmente y que contienen
.
en su nombre, pero que no son métodos de S3.¿Qué hace el método
as.data.frame.data.frame()
? ¿Por qué es confuso? ¿Cómo podría evitar esta confusión en su propio código?Describa la diferencia de comportamiento en estas dos llamadas.
set.seed(1014) <- as.Date("2017-01-31") + sample(10, 5) some_days mean(some_days) #> [1] "2017-02-06" mean(unclass(some_days)) #> [1] 17203
¿Qué clase de objeto devuelve el siguiente código? ¿Sobre qué tipo de base está construido? ¿Qué atributos utiliza?
<- ecdf(rpois(100, 10)) x x#> Empirical CDF #> Call: ecdf(rpois(100, 10)) #> x[1:18] = 2, 3, 4, ..., 2e+01, 2e+01
¿Qué clase de objeto devuelve el siguiente código? ¿Sobre qué tipo de base está construido? ¿Qué atributos utiliza?
<- table(rpois(100, 5)) x x#> #> 1 2 3 4 5 6 7 8 9 10 #> 7 5 18 14 15 15 14 4 5 3
13.3 Clases
Si ha realizado programación orientada a objetos en otros lenguajes, se sorprenderá al saber que S3 no tiene una definición formal de una clase: para convertir un objeto en una instancia de una clase, simplemente establezca el atributo de clase. Puedes hacerlo durante la creación con structure()
, o después del hecho con class<-()
:
# Crear y asignar clases en un solo paso
<- structure(list(), class = "my_class")
x
# Crear, luego establecer la clase
<- list()
x class(x) <- "my_class"
Puede determinar la clase de un objeto S3 con class(x)
y ver si un objeto es una instancia de una clase usando inherits(x, "classname")
.
class(x)
#> [1] "my_class"
inherits(x, "my_class")
#> [1] TRUE
inherits(x, "your_class")
#> [1] FALSE
El nombre de la clase puede ser cualquier cadena, pero recomiendo usar solo letras y _
. Evite .
porque (como se mencionó anteriormente) puede confundirse con el separador .
entre un nombre genérico y un nombre de clase. Al usar una clase en un paquete, recomiendo incluir el nombre del paquete en el nombre de la clase. Eso asegura que no chocará accidentalmente con una clase definida por otro paquete.
S3 no tiene comprobaciones de corrección, lo que significa que puede cambiar la clase de los objetos existentes:
# Crear un modelo lineal
<- lm(log(mpg) ~ log(disp), data = mtcars)
mod class(mod)
#> [1] "lm"
print(mod)
#>
#> Call:
#> lm(formula = log(mpg) ~ log(disp), data = mtcars)
#>
#> Coefficients:
#> (Intercept) log(disp)
#> 5.381 -0.459
# Conviértelo en una fecha (?!)
class(mod) <- "Date"
# Como era de esperar, esto no funciona muy bien
print(mod)
#> Error in as.POSIXlt(.Internal(Date2POSIXlt(x, tz)), tz = tz): 'list' object cannot be coerced to type 'double'
Si ha usado otros lenguajes orientados a objetos, esto podría hacerle sentir mareado, pero en la práctica esta flexibilidad causa pocos problemas. R no evita que te dispares en el pie, pero mientras no apuntes el arma a los dedos de los pies y aprietes el gatillo, no tendrás ningún problema.
Para evitar las intersecciones de pie y bala al crear su propia clase, le recomiendo que proporcione generalmente tres funciones:
Un constructor de bajo nivel,
new_myclass()
, que crea eficientemente nuevos objetos con la estructura correcta.Un validador,
validate_myclass()
, que realiza verificaciones más costosas desde el punto de vista computacional para garantizar que el objeto tenga los valores correctos.Un ayudante fácil de usar,
myclass()
, que proporciona una manera conveniente para que otros creen objetos de su clase.
No necesita un validador para clases muy simples, y puede omitir el asistente si la clase es solo para uso interno, pero siempre debe proporcionar un constructor.
13.3.1 Constructores
S3 no proporciona una definición formal de una clase, por lo que no tiene una forma integrada de garantizar que todos los objetos de una clase determinada tengan la misma estructura (es decir, el mismo tipo base y los mismos atributos con los mismos tipos). En su lugar, debe aplicar una estructura coherente mediante el uso de un constructor.
El constructor debe seguir tres principios:
Se llamará
new_myclass()
.Tener un argumento para el objeto base y uno para cada atributo.
Comprobar el tipo del objeto base y los tipos de cada atributo.
Ilustraré estas ideas creando constructores para las clases base2 con las que ya está familiarizado. Para comenzar, hagamos un constructor para la clase S3 más simple: Date
. Una fecha es simplemente un doble con un único atributo: su clase
es Date
. Esto lo convierte en un constructor muy simple:
<- function(x = double()) {
new_Date stopifnot(is.double(x))
structure(x, class = "Date")
}
new_Date(c(-1, 0, 1))
#> [1] "1969-12-31" "1970-01-01" "1970-01-02"
El propósito de los constructores es ayudarte a ti, el desarrollador. Eso significa que puede mantenerlos simples y no necesita optimizar los mensajes de error para el consumo público. Si espera que los usuarios también creen objetos, debe crear una función de ayuda amigable, llamada class_name()
, que describiré en breve.
Un constructor un poco más complicado es el de difftime
, que se usa para representar diferencias de tiempo. Se basa de nuevo en un doble, pero tiene un atributo de unidades
que debe tomar uno de un pequeño conjunto de valores:
<- function(x = double(), units = "secs") {
new_difftime stopifnot(is.double(x))
<- match.arg(units, c("secs", "mins", "hours", "days", "weeks"))
units
structure(x,
class = "difftime",
units = units
)
}
new_difftime(c(1, 10, 3600), "secs")
#> Time differences in secs
#> [1] 1 10 3600
new_difftime(52, "weeks")
#> Time difference of 52 weeks
El constructor es una función de desarrollador: será llamado en muchos lugares por un usuario experimentado. Eso significa que está bien intercambiar un poco de seguridad a cambio de rendimiento, y debe evitar verificaciones potencialmente lentas en el constructor.
13.3.2 Validadores
Las clases más complicadas requieren controles de validez más complicados. Tome factores, por ejemplo. Un constructor solo verifica que los tipos sean correctos, lo que permite crear factores con formato incorrecto:
<- function(x = integer(), levels = character()) {
new_factor stopifnot(is.integer(x))
stopifnot(is.character(levels))
structure(
x,levels = levels,
class = "factor"
)
}
new_factor(1:5, "a")
#> Error in as.character.factor(x): malformed factor
new_factor(0:1, "a")
#> Error in as.character.factor(x): malformed factor
En lugar de sobrecargar al constructor con controles complicados, es mejor ponerlos en una función separada. Si lo hace, le permite crear nuevos objetos de forma económica cuando sabe que los valores son correctos y reutilizar fácilmente las comprobaciones en otros lugares.
<- function(x) {
validate_factor <- unclass(x)
values <- attr(x, "levels")
levels
if (!all(!is.na(values) & values > 0)) {
stop(
"Todos los valores `x` deben ser no faltantes y mayores que cero",
call. = FALSE
)
}
if (length(levels) < max(values)) {
stop(
"Debe haber al menos tantos `levels` como valores posibles en `x`",
call. = FALSE
)
}
x
}
validate_factor(new_factor(1:5, "a"))
#> Error: Debe haber al menos tantos `levels` como valores posibles en `x`
validate_factor(new_factor(0:1, "a"))
#> Error: Todos los valores `x` deben ser no faltantes y mayores que cero
Esta función de validación se llama principalmente por sus efectos secundarios (arrojar un error si el objeto no es válido), por lo que esperaría que devuelva su entrada principal de forma invisible (como se describe en la Sección 6.7.2). Sin embargo, es útil que los métodos de validación regresen visiblemente, como veremos a continuación.
13.3.3 Ayudantes
Si desea que los usuarios construyan objetos de su clase, también debe proporcionar un método auxiliar que les haga la vida lo más fácil posible. Un ayudante siempre debe:
Tener el mismo nombre que la clase, p.
myclass()
.Termine llamando al constructor y al validador, si existe.
Cree mensajes de error cuidadosamente elaborados y adaptados a un usuario final.
Tenga una interfaz de usuario cuidadosamente diseñada con valores predeterminados cuidadosamente seleccionados y conversiones útiles.
La última viñeta es la más complicada y es difícil dar consejos generales. Sin embargo, hay tres patrones comunes:
A veces, todo lo que necesita hacer el ayudante es forzar sus entradas al tipo deseado. Por ejemplo,
new_difftime()
es muy estricto y viola la convención habitual de que puede usar un vector entero siempre que pueda usar un vector doble:new_difftime(1:10) #> Error in new_difftime(1:10): is.double(x) is not TRUE
No es el trabajo del constructor ser flexible, así que aquí creamos un ayudante que solo fuerza la entrada a un doble.
<- function(x = double(), units = "secs") { difftime <- as.double(x) x new_difftime(x, units = units) } difftime(1:10) #> Time differences in secs #> [1] 1 2 3 4 5 6 7 8 9 10
A menudo, la representación más natural de un objeto complejo es una cadena. Por ejemplo, es muy conveniente especificar factores con un vector de caracteres. El siguiente código muestra una versión simple de
factor()
: toma un vector de caracteres y supone que los niveles deberían ser valores únicos. Esto no siempre es correcto (ya que es posible que algunos niveles no se vean en los datos), pero es un valor predeterminado útil.<- function(x = character(), levels = unique(x)) { factor <- match(x, levels) ind validate_factor(new_factor(ind, levels)) } factor(c("a", "a", "b")) #> [1] a a b #> Levels: a b
Algunos objetos complejos se especifican de manera más natural mediante múltiples componentes simples. Por ejemplo, creo que es natural construir una fecha y hora proporcionando los componentes individuales (año, mes, día, etc.). Eso me lleva a este ayudante
POSIXct()
que se parece a la función existenteISODatetime()
3:<- function(year = integer(), POSIXct month = integer(), day = integer(), hour = 0L, minute = 0L, sec = 0, tzone = "") { ISOdatetime(year, month, day, hour, minute, sec, tz = tzone) } POSIXct(2020, 1, 1, tzone = "America/New_York") #> [1] "2020-01-01 EST"
Para clases más complicadas, debe sentirse libre de ir más allá de estos patrones para hacer la vida lo más fácil posible para sus usuarios.
13.3.4 Ejercicios
Escribe un constructor para los objetos
data.frame
. ¿Sobre qué tipo base se construye un marco de datos? ¿Qué atributos utiliza? ¿Cuáles son las restricciones impuestas a los elementos individuales? ¿Qué pasa con los nombres?Mejore mi ayudante
factor()
para que tenga un mejor comportamiento cuando uno o másvalores
no se encuentran en losniveles
. ¿Qué hacebase::factor()
en esta situación?Lee atentamente el código fuente de
factor()
. ¿Qué hace que mi constructor no hace?Los factores tienen un atributo opcional de “contrastes”. Lea la ayuda de
C()
y describa brevemente el propósito del atributo. ¿Qué tipo debe tener? Reescriba el constructornew_factor()
para incluir este atributo.Lea la documentación de
utils::as.roman()
. ¿Cómo escribirías un constructor para esta clase? ¿Necesita un validador? ¿Qué podría hacer un ayudante?
13.4 Genéricos y métodos
El trabajo de un genérico S3 es realizar el envío de métodos, es decir, encontrar la implementación específica para una clase. El envío de métodos se realiza mediante UseMethod()
, al que todos los genéricos llaman4. UseMethod()
toma dos argumentos: el nombre de la función genérica (obligatorio) y el argumento a usar para el envío del método (opcional). Si omite el segundo argumento, se enviará en función del primer argumento, que casi siempre es lo que se desea.
La mayoría de los genéricos son muy simples y consisten solo en una llamada a UseMethod()
. Tome mean()
por ejemplo:
mean#> function (x, ...)
#> UseMethod("mean")
#> <bytecode: 0x556da9ce4eb0>
#> <environment: namespace:base>
Crear su propio genérico es igualmente simple:
<- function(x) {
my_new_generic UseMethod("my_new_generic")
}
(Si se pregunta por qué tenemos que repetir my_new_generic
dos veces, piense en la Sección 6.2.3.)
No pasa ninguno de los argumentos del genérico a UseMethod()
; utiliza magia profunda para pasar al método automáticamente. El proceso preciso es complicado y con frecuencia sorprendente, por lo que debe evitar realizar cualquier cálculo de forma genérica. Para conocer todos los detalles, lea detenidamente la sección Detalles técnicos en ?UseMethod
.
13.4.1 Método de envío
¿Cómo funciona UseMethod()
? Básicamente, crea un vector de nombres de métodos, paste0("generic", ".", c(class(x), "default"))
, y luego busca cada método potencial a su vez. Podemos ver esto en acción con sloop::s3_dispatch()
. Le das una llamada a un genérico S3 y enumera todos los métodos posibles. Por ejemplo, ¿qué método se llama cuando imprime un objeto Date
?
<- Sys.Date()
x s3_dispatch(print(x))
#> => print.Date
#> * print.default
La salida aquí es simple:
=>
indica el método que se llama, aquíprint.Date()
*
indica un método que está definido, pero no llamado, aquíprint.default()
.
La clase “predeterminada” es una pseudoclase especial. Esta no es una clase real, pero se incluye para que sea posible definir un respaldo estándar que se encuentra siempre que un método específico de clase no está disponible.
La esencia del envío de métodos es bastante simple, pero a medida que avanza el capítulo, verá que se vuelve progresivamente más complicado para abarcar la herencia, los tipos base, los genéricos internos y los genéricos de grupo. El siguiente código muestra un par de casos más complicados a los que volveremos en las secciones, Sección 13.6 y Sección 13.7.
<- matrix(1:10, nrow = 2)
x s3_dispatch(mean(x))
#> mean.matrix
#> mean.integer
#> mean.numeric
#> => mean.default
s3_dispatch(sum(Sys.time()))
#> sum.POSIXct
#> sum.POSIXt
#> sum.default
#> => Summary.POSIXct
#> Summary.POSIXt
#> Summary.default
#> -> sum (internal)
13.4.2 Encontrar métodos
sloop::s3_dispatch()
te permite encontrar el método específico usado para una sola llamada. ¿Qué sucede si desea encontrar todos los métodos definidos para un genérico o asociados con una clase? Ese es el trabajo de sloop::s3_methods_generic()
y sloop::s3_methods_class()
:
s3_methods_generic("mean")
#> # A tibble: 7 × 4
#> generic class visible source
#> <chr> <chr> <lgl> <chr>
#> 1 mean Date TRUE base
#> 2 mean default TRUE base
#> 3 mean difftime TRUE base
#> 4 mean POSIXct TRUE base
#> 5 mean POSIXlt TRUE base
#> 6 mean quosure FALSE registered S3method
#> 7 mean vctrs_vctr FALSE registered S3method
s3_methods_class("ordered")
#> # A tibble: 4 × 4
#> generic class visible source
#> <chr> <chr> <lgl> <chr>
#> 1 as.data.frame ordered TRUE base
#> 2 Ops ordered TRUE base
#> 3 relevel ordered FALSE registered S3method
#> 4 Summary ordered TRUE base
13.4.3 Crear métodos
Hay dos arrugas a tener en cuenta cuando crea un nuevo método:
Primero, solo debe escribir un método si posee el genérico o la clase. R le permitirá definir un método incluso si no lo hace, pero es de muy mala educación. En su lugar, trabaje con el autor del genérico o de la clase para agregar el método en su código.
Un método debe tener los mismos argumentos que su genérico. Esto se aplica en los paquetes mediante
R CMD check
, pero es una buena práctica incluso si no está creando un paquete.Hay una excepción a esta regla: si el genérico tiene
...
, el método puede contener un superconjunto de argumentos. Esto permite que los métodos tomen argumentos adicionales arbitrarios. La desventaja de usar...
, sin embargo, es que cualquier argumento mal escrito se tragará silenciosamente 5, como se menciona en la Sección 6.6.
13.4.4 Ejercicios
Lea el código fuente de
t()
yt.test()
y confirme quet.test()
es un método genérico de S3 y no un método de S3. ¿Qué pasa si creas un objeto con la clasetest
y llamast()
con él? ¿Por qué?<- structure(1:10, class = "test") x t(x)
¿Para qué genéricos tiene métodos la clase
table
?¿Para qué genéricos tiene métodos la clase
ecdf
?¿Qué base genérica tiene el mayor número de métodos definidos?
Lea detenidamente la documentación de
UseMethod()
y explique por qué el siguiente código devuelve los resultados que devuelve. ¿Qué dos reglas usuales de evaluación de funciones violaUseMethod()
?<- function(x) { g <- 10 x <- 10 y UseMethod("g") }<- function(x) c(x = x, y = y) g.default <- 1 x <- 1 y g(x) #> x y #> 1 1
¿Cuáles son los argumentos para
[
? ¿Por qué es una pregunta difícil de responder?
13.5 Estilos de objeto
Hasta ahora me he centrado en clases de estilo vectorial como Date
y factor
. Estos tienen la propiedad clave de que length(x)
representa el número de observaciones en el vector. Hay tres variantes que no tienen esta propiedad:
Los objetos de estilo de registro utilizan una lista de vectores de igual longitud para representar componentes individuales del objeto. El mejor ejemplo de esto es
POSIXlt
, que debajo del capó es una lista de 11 componentes de fecha y hora como año, mes y día. Las clases de estilo de registro anulanlongitud()
y los métodos de creación de subconjuntos para ocultar este detalle de implementación.<- as.POSIXlt(ISOdatetime(2020, 1, 1, 0, 0, 1:3)) x x#> [1] "2020-01-01 00:00:01 UTC" "2020-01-01 00:00:02 UTC" #> [3] "2020-01-01 00:00:03 UTC" length(x) #> [1] 3 length(unclass(x)) #> [1] 11 1]] # the first date time x[[#> [1] "2020-01-01 00:00:01 UTC" unclass(x)[[1]] # the first component, the number of seconds #> [1] 1 2 3
Los marcos de datos son similares a los objetos de estilo de registro en que ambos usan listas de vectores de igual longitud. Sin embargo, los marcos de datos son conceptualmente bidimensionales y los componentes individuales se exponen fácilmente al usuario. El número de observaciones es el número de filas, no la longitud:
<- data.frame(x = 1:100, y = 1:100) x length(x) #> [1] 2 nrow(x) #> [1] 100
Los objetos escalares normalmente usan una lista para representar una sola cosa. Por ejemplo, un objeto
lm
es una lista de longitud 12 pero representa un modelo.<- lm(mpg ~ wt, data = mtcars) mod length(mod) #> [1] 12
Los objetos escalares también se pueden construir sobre funciones, llamadas y entornos6. En general, esto es menos útil, pero puede ver aplicaciones en
stats::ecdf()
, R6 (Capítulo 14) yrlang::quo()
(Capítulo 19) .
Desafortunadamente, describir el uso apropiado de cada uno de estos estilos de objeto está más allá del alcance de este libro. Sin embargo, puede obtener más información en la documentación del paquete vctrs (https://vctrs.r-lib.org); el paquete también proporciona constructores y ayudantes que facilitan la implementación de los diferentes estilos.
13.5.1 Ejercicios
Categorice los objetos devueltos por
lm()
,factor()
,table()
,as.Date()
,as.POSIXct()
ecdf()
,ordered()
,I()
en los estilos descritos anteriormente.¿Cómo sería una función constructora para objetos
lm
,new_lm()
? Use?lm
y experimente para descubrir los campos obligatorios y sus tipos.
13.6 Herencia
Las clases de S3 pueden compartir el comportamiento a través de un mecanismo llamado herencia. La herencia está impulsada por tres ideas:
La clase puede ser un carácter vector. Por ejemplo, las clases
ordered
yPOSIXct
tienen dos componentes en su clase:class(ordered("x")) #> [1] "ordered" "factor" class(Sys.time()) #> [1] "POSIXct" "POSIXt"
Si no se encuentra un método para la clase en el primer elemento del vector, R busca un método para la segunda clase (y así sucesivamente):
s3_dispatch(print(ordered("x"))) #> print.ordered #> => print.factor #> * print.default s3_dispatch(print(Sys.time())) #> => print.POSIXct #> print.POSIXt #> * print.default
Un método puede delegar trabajo llamando a
NextMethod()
. Volveremos a eso muy pronto; por ahora, tenga en cuenta ques3_dispatch()
informa delegación con->
.s3_dispatch(ordered("x")[1]) #> [.ordered #> => [.factor #> [.default #> -> [ (internal) s3_dispatch(Sys.time()[1]) #> => [.POSIXct #> [.POSIXt #> [.default #> -> [ (internal)
Antes de continuar, necesitamos un poco de vocabulario para describir la relación entre las clases que aparecen juntas en un vector de clase. Diremos que ordered
es una subclase de factor
porque siempre aparece antes que él en el vector de clase y, a la inversa, diremos que factor
es una superclase de ordered
.
S3 no impone restricciones en la relación entre subclases y superclases, pero su vida será más fácil si impone algunas. Le recomiendo que se adhiera a dos principios simples al crear una subclase:
El tipo base de la subclase debe ser el mismo que el de la superclase.
Los atributos de la subclase deben ser un superconjunto de los atributos de la superclase.
POSIXt
no se adhiere a estos principios porque POSIXct
tiene tipo doble y POSIXlt
tiene tipo lista. Esto significa que POSIXt
no es una superclase, e ilustra que es bastante posible usar el sistema de herencia S3 para implementar otros estilos de código compartido (aquí POSIXt
juega un papel más como una interfaz), pero necesitará descubra convenciones seguras usted mismo.
13.6.1 NextMethod()
NextMethod()
es la parte más difícil de entender de la herencia, por lo que comenzaremos con un ejemplo concreto para el caso de uso más común: [
. Comenzaremos creando una clase de juguete simple: una clase secreta
que oculta su salida cuando se imprime:
<- function(x = double()) {
new_secret stopifnot(is.double(x))
structure(x, class = "secret")
}
<- function(x, ...) {
print.secret print(strrep("x", nchar(x)))
invisible(x)
}
<- new_secret(c(15, 1, 456))
x
x#> [1] "xx" "x" "xxx"
Esto funciona, pero el método predeterminado [
no conserva la clase:
s3_dispatch(x[1])
#> [.secret
#> [.default
#> => [ (internal)
1]
x[#> [1] 15
Para arreglar esto, necesitamos proporcionar un método [.secret
. ¿Cómo podríamos implementar este método? El enfoque ingenuo no funcionará porque nos quedaremos atrapados en un bucle infinito:
`[.secret` <- function(x, i) {
new_secret(x[i])
}
En su lugar, necesitamos alguna forma de llamar al código [
subyacente, es decir, la implementación que sería llamada si no tuviéramos un método [.secret
. Un enfoque sería unclass()
el objeto:
`[.secret` <- function(x, i) {
<- unclass(x)
x new_secret(x[i])
}1]
x[#> [1] "xx"
Esto funciona, pero es ineficiente porque crea una copia de x
. Un mejor enfoque es usar NextMethod()
, que resuelve de manera concisa el problema de delegar al método que se habría llamado si [.secret
no existiera:
`[.secret` <- function(x, i) {
new_secret(NextMethod())
}1]
x[#> [1] "xx"
Podemos ver lo que está pasando con sloop::s3_dispatch()
:
s3_dispatch(x[1])
#> => [.secret
#> [.default
#> -> [ (internal)
El =>
indica que se llama a [.secret
, pero que NextMethod()
delega el trabajo al método interno subyacente [
, como se muestra en ->
.
Al igual que con UseMethod()
, la semántica precisa de NextMethod()
es compleja. En particular, realiza un seguimiento de la lista de posibles métodos siguientes con una variable especial, lo que significa que la modificación del objeto que se envía no tendrá ningún impacto en el método que se llamará a continuación.
13.6.2 Permitir subclases
Cuando crea una clase, debe decidir si desea permitir subclases, ya que requiere algunos cambios en el constructor y una reflexión cuidadosa en sus métodos.
Para permitir subclases, el constructor principal debe tener argumentos ...
y class
:
<- function(x, ..., class = character()) {
new_secret stopifnot(is.double(x))
structure(
x,
...,class = c(class, "secret")
) }
Luego, el constructor de la subclase puede simplemente llamar al constructor de la clase principal con argumentos adicionales según sea necesario. Por ejemplo, imagina que queremos crear una clase supersecreta que también oculta la cantidad de caracteres:
<- function(x) {
new_supersecret new_secret(x, class = "supersecret")
}
<- function(x, ...) {
print.supersecret print(rep("xxxxx", length(x)))
invisible(x)
}
<- new_supersecret(c(15, 1, 456))
x2
x2#> [1] "xxxxx" "xxxxx" "xxxxx"
Para permitir la herencia, también debe pensar detenidamente en sus métodos, ya que ya no puede usar el constructor. Si lo hace, el método siempre devolverá la misma clase, independientemente de la entrada. Esto obliga a quien hace una subclase a hacer mucho trabajo extra.
Concretamente, esto significa que debemos revisar el método [.secret
. Actualmente siempre devuelve un secret()
, incluso cuando se le da un supersecreto:
`[.secret` <- function(x, ...) {
new_secret(NextMethod())
}
1:3]
x2[#> [1] "xx" "x" "xxx"
Queremos asegurarnos de que [.secret
devuelva la misma clase que x
incluso si es una subclase. Por lo que puedo decir, no hay forma de resolver este problema usando solo la base R. En su lugar, deberá utilizar el paquete vctrs, que proporciona una solución en forma de vctrs::vec_restore()
genérico. Este genérico toma dos entradas: un objeto que ha perdido información de subclase y un objeto de plantilla para usar para la restauración.
Por lo general, los métodos vec_restore()
son bastante simples: simplemente llama al constructor con los argumentos apropiados:
<- function(x, to, ...) new_secret(x)
vec_restore.secret <- function(x, to, ...) new_supersecret(x) vec_restore.supersecret
(Si su clase tiene atributos, deberá pasarlos de to
al constructor).
Ahora podemos usar vec_restore()
en el método [.secret
:
`[.secret` <- function(x, ...) {
::vec_restore(NextMethod(), x)
vctrs
}1:3]
x2[#> [1] "xxxxx" "xxxxx" "xxxxx"
(Solo entendí completamente este problema recientemente, por lo que al momento de escribir no se usa en el tidyverse. Con suerte, para cuando estés leyendo esto, se habrá implementado, lo que hará que sea mucho más fácil (por ejemplo) subclasificar tibbles. )
Si construye su clase usando las herramientas provistas por el paquete vctrs, [
obtendrá este comportamiento automáticamente. Solo necesitará proporcionar su propio método [
si usa atributos que dependen de los datos o desea un comportamiento de subconjunto no estándar. Ver ?vctrs::new_vctr
para más detalles.
13.6.3 Ejercicios
¿Cómo admite subclases
[.Date
? ¿Cómo no admite subclases?R tiene dos clases para representar datos de fecha y hora,
POSIXct
yPOSIXlt
, que heredan ambas dePOSIXt
. ¿Qué genéricos tienen comportamientos diferentes para las dos clases? ¿Qué genéricos comparten el mismo comportamiento?¿Qué espera que devuelva este código? ¿Qué devuelve realmente? ¿Por qué?
<- function(x) UseMethod("generic2") generic2 <- function(x) "a1" generic2.a1 <- function(x) "a2" generic2.a2 <- function(x) { generic2.b class(x) <- "a1" NextMethod() } generic2(structure(list(), class = c("b", "a2")))
13.7 Detalles de envío
Este capítulo concluye con algunos detalles adicionales sobre el envío de métodos. Es seguro omitir estos detalles si es nuevo en S3.
13.7.1 S3 y tipos básicos
¿Qué sucede cuando llama a un genérico S3 con un objeto base, es decir, un objeto sin clase? Podrías pensar que enviaría lo que class()
devuelve:
class(matrix(1:5))
#> [1] "matrix" "array"
Pero, lamentablemente, el envío se produce en la clase implícita, que tiene tres componentes:
- La cadena “array” o “matrix” si el objeto tiene dimensiones
- El resultado de
typeof()
con algunos ajustes menores - La cadena “numeric” si el objeto es “integer” o “double”
No hay una función base que calcule la clase implícita, pero puede usar sloop::s3_class()
s3_class(matrix(1:5))
#> [1] "matrix" "integer" "numeric"
Esto es usado por s3_dispatch()
:
s3_dispatch(print(matrix(1:5)))
#> print.matrix
#> print.integer
#> print.numeric
#> => print.default
Esto significa que la clade, class()
, de un objeto no determina de forma única su envío:
<- 1:5
x1 class(x1)
#> [1] "integer"
s3_dispatch(mean(x1))
#> mean.integer
#> mean.numeric
#> => mean.default
<- structure(x1, class = "integer")
x2 class(x2)
#> [1] "integer"
s3_dispatch(mean(x2))
#> mean.integer
#> => mean.default
13.7.2 Genéricos internos
Algunas funciones básicas, como [
, sum()
y cbind()
, se denominan genéricos internos porque no llaman a UseMethod()
sino que llaman a las funciones de C DispatchGroup( )
o DispatchOrEval()
. s3_dispatch()
muestra genéricos internos al incluir el nombre del genérico seguido de (internal)
:
s3_dispatch(Sys.time()[1])
#> => [.POSIXct
#> [.POSIXt
#> [.default
#> -> [ (internal)
Por motivos de rendimiento, los genéricos internos no envían a los métodos a menos que se haya establecido el atributo de clase, lo que significa que los genéricos internos no utilizan la clase implícita. Nuevamente, si alguna vez se siente confundido acerca del envío de métodos, puede confiar en s3_dispatch()
.
13.7.3 Genéricos del grupo
Los genéricos de grupo son la parte más complicada del envío de métodos de S3 porque involucran tanto NextMethod()
como genéricos internos. Al igual que los genéricos internos, solo existen en la base R y no puede definir su propio grupo genérico.
Hay cuatro genéricos de grupo:
Matemáticas:
abs()
,sign()
,sqrt()
,floor()
,cos()
,sin()
,log()
, y más (ver?Math
para la lista completa).Operaciones:
+
,-
,*
,/
,^
,%%
,%/%
,&
,|
,!
,==
,!=
,<
,<=
,>=
, y>
.Resumen:
all()
,any()
,sum()
,prod()
,min()
,max()
, yrange()
.Complejo:
Arg()
,Conj()
,Im()
,Mod()
,Re()
.
La definición de un solo grupo genérico para su clase anula el comportamiento predeterminado para todos los miembros del grupo. Los métodos para genéricos grupales se buscan solo si los métodos para el genérico específico no existen:
s3_dispatch(sum(Sys.time()))
#> sum.POSIXct
#> sum.POSIXt
#> sum.default
#> => Summary.POSIXct
#> Summary.POSIXt
#> Summary.default
#> -> sum (internal)
La mayoría de los genéricos de grupo implican una llamada a NextMethod()
. Por ejemplo, tome los objetos difftime()
. Si observa el envío del método para abs()
, verá que hay un grupo genérico Math
definido.
<- as.difftime(10, units = "mins")
y s3_dispatch(abs(y))
#> abs.difftime
#> abs.default
#> => Math.difftime
#> Math.default
#> -> abs (internal)
Math.difftime
básicamente se ve así:
<- function(x, ...) {
Math.difftime new_difftime(NextMethod(), units = attr(x, "units"))
}
Despacha al siguiente método, aquí el valor predeterminado interno, para realizar el cálculo real y luego restaurar la clase y los atributos. (Para admitir mejor las subclases de difftime
, sería necesario llamar a vec_restore()
, como se describe en la Sección 13.6.2.)
Dentro de una función genérica de grupo, una variable especial .Generic
proporciona la función genérica real llamada. Esto puede ser útil cuando se producen mensajes de error y, a veces, puede ser útil si necesita recuperar manualmente el genérico con diferentes argumentos.
13.7.4 Despacho doble
Los genéricos del grupo Ops, que incluye la aritmética de dos argumentos y los operadores booleanos como -
y &
, implementan un tipo especial de envío de métodos. Despachan en el tipo de ambos argumentos, que se llama despacho doble. Esto es necesario para preservar la propiedad conmutativa de muchos operadores, es decir, a + b
debería ser igual a b + a
. Tome el siguiente ejemplo simple:
<- as.Date("2017-01-01")
date <- 1L
integer
+ integer
date #> [1] "2017-01-02"
+ date
integer #> [1] "2017-01-02"
Si +
se enviara solo en el primer argumento, devolvería valores diferentes para los dos casos. Para superar este problema, los genéricos del grupo Ops utilizan una estrategia ligeramente diferente a la habitual. En lugar de hacer un envío de un solo método, hacen dos, uno para cada entrada. Hay tres posibles resultados de esta búsqueda:
Los métodos son los mismos, por lo que no importa qué método se utilice.
Los métodos son diferentes y R recurre al método interno con una advertencia.
Un método es interno, en cuyo caso R llama al otro método.
Este enfoque es propenso a errores, por lo que si desea implementar un despacho doble robusto para operadores algebraicos, le recomiendo usar el paquete vctrs. Ver ?vctrs::vec_arith
para más detalles.
13.7.5 Ejercicios
Explique las diferencias en el envío a continuación:
<- function(x) 10 length.integer <- 1:5 x1 class(x1) #> [1] "integer" s3_dispatch(length(x1)) #> * length.integer #> length.numeric #> length.default #> => length (internal) <- structure(x1, class = "integer") x2 class(x2) #> [1] "integer" s3_dispatch(length(x2)) #> => length.integer #> length.default #> * length (internal)
¿Qué clases tienen un método para el grupo
Math
genérico en base R? Lee el código fuente. ¿Cómo funcionan los métodos?Math.difftime()
es más complicado de lo que describí. ¿Por qué?
Las excepciones son los métodos que se encuentran en el paquete base, como
t.data.frame
, y los métodos que ha creado.↩︎Las versiones recientes de R tienen constructores
.Date()
,.difftime()
,.POSIXct()
y.POSIXlt()
, pero son internos, no están bien documentados y no siguen los principios que Recomiendo.↩︎Este ayudante no es eficiente: en segundo plano
ISODatetime()
funciona pegando los componentes en una cadena y luego usandostrptime()
. Un equivalente más eficiente está disponible enlubridate::make_datetime()
.↩︎La excepción son los genéricos internos, que se implementan en C y son el tema de la Sección 13.7.2.↩︎
Consulte https://github.com/hadley/ellipsis para ver una forma experimental de advertir cuando los métodos no usan todos los argumentos en
...
, lo que proporciona una posible resolución de este problema.↩︎También puede construir un objeto encima de una lista de pares, pero todavía tengo que encontrar una buena razón para hacerlo.↩︎