Clases y Métodos
1 OOP en R
1.0.1 Programación Orientada a Objetos (OOP)
- Los objetos encapsulan información y control de su comportamiento (objects).
- Las clases describen propiedades de un grupo de objetos (class).
- Se pueden definir clases a partir de otras (inheritance).
- Una función genérica se comporta de forma diferente atendiendo a la clase de uno (o varios) de sus argumentos (polymorphism).
1.0.2 OOP en R
En R
coexisten dos implementaciones de la OOP:
S3
: elaboración informal con enfasis en las funciones genéricas y el polimorfismo.S4
: elaboración formal de clases y métodos.
1.0.3 OOP en R
2 Clases y métodos S3
2.1 Clases
2.1.1 Clases
Los objetos básicos en R
tienen una clase implícita definida en S3
. Es accesible con class
.
x <- rnorm(10)
class(x)
[1] "numeric"
Pero no tienen atributo…
attr(x, 'class')
NULL
…ni se consideran formalmente objetos
is.object(x)
[1] FALSE
2.1.2 Clases
Se puede redefinir la clase de un objecto S3
con class
class(x) <- 'myNumeric' class(x)
[1] "myNumeric"
Ahora sí es un objeto…
is.object(x)
[1] TRUE
y su atributo está definido
attr(x, 'class')
[1] "myNumeric"
Sin embargo, su modo de almacenamiento (clase intrínseca) no cambia:
mode(x)
[1] "numeric"
2.1.3 Definición de Clases
task1 <- list(what='Write an email', when=as.Date('2013-01-01'), priority='Low') class(task1) <- 'Task' task1
$what [1] "Write an email" $when [1] "2013-01-01" $priority [1] "Low" attr(,"class") [1] "Task"
task2 <- list(what='Find and fix bugs', when=as.Date('2013-03-15'), priority='High') class(task2) <- 'Task'
2.1.4 Definición de Clases
myToDo <- list(task1, task2) class(myToDo) <- c('ToDo3') myToDo
[[1]] $what [1] "Write an email" $when [1] "2013-01-01" $priority [1] "Low" attr(,"class") [1] "Task" [[2]] $what [1] "Find and fix bugs" $when [1] "2013-03-15" $priority [1] "High" attr(,"class") [1] "Task" attr(,"class") [1] "ToDo3"
2.1.5 Problemas de la sencillez de S3
notToDo <- list(task1, 2019) class(notToDo) <- c('ToDo3') notToDo
[[1]] $what [1] "Write an email" $when [1] "2013-01-01" $priority [1] "Low" attr(,"class") [1] "Task" [[2]] [1] 2019 attr(,"class") [1] "ToDo3"
2.2 Métodos
2.2.1 Métodos con S3
Son sencillos de usar e implementar pero poco robustos.
Se definen a partir de un método genérico…
summary
function (object, ...) UseMethod("summary") <bytecode: 0x5590c6b26c38> <environment: namespace:base>
…añadiendo a la función el nombre de la clase con un punto como separador.
summary.data.frame
function (object, maxsum = 7L, digits = max(3L, getOption("digits") - 3L), ...) { ncw <- function(x) { z <- nchar(x, type = "w") if (any(na <- is.na(z))) { z[na] <- nchar(encodeString(z[na]), "b") } z } z <- lapply(X = as.list(object), FUN = summary, maxsum = maxsum, digits = 12L, ...) nv <- length(object) nm <- names(object) lw <- numeric(nv) nr <- if (nv) max(vapply(z, function(x) NROW(x) + !is.null(attr(x, "NAs")), integer(1))) else 0 for (i in seq_len(nv)) { sms <- z[[i]] if (is.matrix(sms)) { cn <- paste(nm[i], gsub("^ +", "", colnames(sms), useBytes = TRUE), sep = ".") tmp <- format(sms) if (nrow(sms) < nr) tmp <- rbind(tmp, matrix("", nr - nrow(sms), ncol(sms))) sms <- apply(tmp, 1L, function(x) paste(x, collapse = " ")) wid <- sapply(tmp[1L, ], nchar, type = "w") blanks <- paste(character(max(wid)), collapse = " ") wcn <- ncw(cn) pad0 <- floor((wid - wcn)/2) pad1 <- wid - wcn - pad0 cn <- paste0(substring(blanks, 1L, pad0), cn, substring(blanks, 1L, pad1)) nm[i] <- paste(cn, collapse = " ") } else { sms <- format(sms, digits = digits) lbs <- format(names(sms)) sms <- paste0(lbs, ":", sms, " ") lw[i] <- ncw(lbs[1L]) length(sms) <- nr } z[[i]] <- sms } if (nv) { z <- unlist(z, use.names = TRUE) dim(z) <- c(nr, nv) if (anyNA(lw)) warning("probably wrong encoding in names(.) of column ", paste(which(is.na(lw)), collapse = ", ")) blanks <- paste(character(max(lw, na.rm = TRUE) + 2L), collapse = " ") pad <- floor(lw - ncw(nm)/2) nm <- paste0(substring(blanks, 1, pad), nm) dimnames(z) <- list(rep.int("", nr), nm) } else { z <- character() dim(z) <- c(nr, nv) } attr(z, "class") <- c("table") z } <bytecode: 0x5590c723a6b8> <environment: namespace:base>
2.2.2 Métodos con S3
Con methods
podemos averiguar los métodos que hay definidos para una función particular:
methods('summary')
[1] summary.aov summary.aovlist* [3] summary.aspell* summary.check_packages_in_dir* [5] summary.connection summary.data.frame [7] summary.Date summary.default [9] summary.ecdf* summary.factor [11] summary.glm summary.infl* [13] summary.lm summary.loess* [15] summary.manova summary.matrix [17] summary.mlm* summary.nls* [19] summary.packageStatus* summary.PDF_Dictionary* [21] summary.PDF_Stream* summary.POSIXct [23] summary.POSIXlt summary.ppr* [25] summary.prcomp* summary.princomp* [27] summary.proc_time summary.shingle* [29] summary.srcfile summary.srcref [31] summary.stepfun summary.stl* [33] summary.table summary.trellis* [35] summary.tukeysmooth* summary.warnings see '?methods' for accessing help and source code
2.2.3 Métodos con S3
Si no hay un método definido para la clase del objeto, UseMethod
ejecuta la función por defecto:
summary.default
function (object, ..., digits) { if (is.factor(object)) return(summary.factor(object, ...)) else if (is.matrix(object)) { if (missing(digits)) return(summary.matrix(object, ...)) else return(summary.matrix(object, digits = digits, ...)) } value <- if (is.logical(object)) c(Mode = "logical", { tb <- table(object, exclude = NULL, useNA = "ifany") if (!is.null(n <- dimnames(tb)[[1L]]) && any(iN <- is.na(n))) dimnames(tb)[[1L]][iN] <- "NA's" tb }) else if (is.numeric(object)) { nas <- is.na(object) object <- object[!nas] qq <- stats::quantile(object) qq <- c(qq[1L:3L], mean(object), qq[4L:5L]) if (!missing(digits)) qq <- signif(qq, digits) names(qq) <- c("Min.", "1st Qu.", "Median", "Mean", "3rd Qu.", "Max.") if (any(nas)) c(qq, `NA's` = sum(nas)) else qq } else if (is.recursive(object) && !is.language(object) && (n <- length(object))) { sumry <- array("", c(n, 3L), list(names(object), c("Length", "Class", "Mode"))) ll <- numeric(n) for (i in 1L:n) { ii <- object[[i]] ll[i] <- length(ii) cls <- oldClass(ii) sumry[i, 2L] <- if (length(cls)) cls[1L] else "-none-" sumry[i, 3L] <- mode(ii) } sumry[, 1L] <- format(as.integer(ll)) sumry } else c(Length = length(object), Class = class(object), Mode = mode(object)) class(value) <- c("summaryDefault", "table") value } <bytecode: 0x5590c6b296b8> <environment: namespace:base>
2.2.4 Ejemplo de definición de método genérico
En primer lugar, definimos la función con UseMethod
:
myFun <- function(x, ...)UseMethod('myFun')
… y la función por defecto.
myFun.default <- function(x, ...){ cat('Funcion genérica\n') print(x) }
2.2.5 Ejemplo de definición de método genérico
Dado que aún no hay métodos definidos, esta función ejecutará la función por defecto.
methods('myFun')
[1] myFun.default see '?methods' for accessing help and source code
x <- rnorm(10)
myFun(x)
Funcion genérica [1] -0.64490605 -0.76379262 0.57614789 0.12388062 1.80471202 1.64963775 [7] 0.15689557 -0.03204213 0.62892058 -0.11461364
myFun(task1)
Funcion genérica $what [1] "Write an email" $when [1] "2013-01-01" $priority [1] "Low" attr(,"class") [1] "Task"
2.2.6 Ejemplo de definición de método específico
myFun.Task <- function(x, number,...) { if (!missing(number)) cat('Task no.', number,':\n') cat('What: ', x$what, '- When:', as.character(x$when), '- Priority:', x$priority, '\n') }
methods(myFun)
[1] myFun.default myFun.Task see '?methods' for accessing help and source code
methods(class='Task')
[1] myFun see '?methods' for accessing help and source code
2.2.7 Método de Task
myFun(task1)
What: Write an email - When: 2013-01-01 - Priority: Low
myFun(task2)
What: Find and fix bugs - When: 2013-03-15 - Priority: High
myFun(myToDo)
Funcion genérica [[1]] $what [1] "Write an email" $when [1] "2013-01-01" $priority [1] "Low" attr(,"class") [1] "Task" [[2]] $what [1] "Find and fix bugs" $when [1] "2013-03-15" $priority [1] "High" attr(,"class") [1] "Task" attr(,"class") [1] "ToDo3"
2.2.8 Definición del método para ToDo3
2.2.9 Definición del método para ToDo3
myFun.ToDo3 <- function(x, ...){ cat('This is my ToDo list:\n') ## Cada uno de los elementos de un ## objeto ToDo3 son Task. Por tanto, ## x[[i]] es de clase Task y ## print(x[[i]]) ejecuta el metodo ## print.Task for (i in seq_along(x)) myFun(x[[i]], i) cat('--------------------\n') }
myFun(myToDo)
This is my ToDo list: Task no. 1 : What: Write an email - When: 2013-01-01 - Priority: Low Task no. 2 : What: Find and fix bugs - When: 2013-03-15 - Priority: High --------------------
3 Clases y métodos S4
3.1 Clases en S4
3.1.1 Clases en S4
Se construyen con setClass
, que acepta varios argumentos
Class
: nombre de la clase.slots
: una lista con las clases de cada componente. Los nombres de este vector corresponden a los nombres de los componentes (slot
).contains
: un vector con las clases que esta nueva clase extiende.prototype
: un objeto proporcionando el contenido por defecto para los componentes definidos enslots
.validity
: a función que comprueba la validez de la clase creada con la información suministrada.
3.1.2 Datos de ejemplo
3.1.3 Definición de una nueva clase
setClass('bird', slots = c( name = 'character', lat = 'numeric', lon = 'numeric', alt = 'numeric', speed = 'numeric', time = 'POSIXct') )
3.1.4 Funciones para obtener información de una clase
getClass('bird')
Class "bird" [in ".GlobalEnv"] Slots: Name: name lat lon alt speed time Class: character numeric numeric numeric numeric POSIXct
getSlots('bird')
name lat lon alt speed time "character" "numeric" "numeric" "numeric" "numeric" "POSIXct"
slotNames('bird')
[1] "name" "lat" "lon" "alt" "speed" "time"
3.1.5 Creación de un objeto con la clase definida
Una vez que la clase ha sido definida con setClass
, se puede crear un objeto nuevo con new
. Es habitual definir funciones que construyen y modifican objetos para evitar el uso directo de new
:
readBird <- function(name, path) { csvFile <- file.path(path, paste0(name, ".csv")) vals <- read.csv(csvFile) new('bird', name = name, lat = vals$latitude, lon = vals$longitude, alt = vals$altitude, speed = vals$speed_2d, time = as.POSIXct(vals$date_time) ) }
3.1.6 Creación de objetos con la clase definida
eric <- readBird("eric", "data") nico <- readBird("nico", "data") sanne <- readBird("sanne", "data")
3.1.7 Acceso a los slots
A diferencia de $
en listas y data.frame
, para extraer información de los slots hay que emplear @
(pero no es recomendable):
eric@name
[1] "eric"
summary(eric@speed)
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's 0.0000 0.3437 1.0031 2.3005 2.4792 63.4881 85
3.1.8 Clases S4
con slots tipo lista
setClass("flock", slots = c( name = "character", members = "list") )
notAFlock <- new("flock", name = "flock0", members = list(eric, 3, "hello")) sapply(notAFlock@members, class)
[1] "bird" "numeric" "character"
3.1.9 Función de validación
valida <- function (object) { if (any(sapply(object@members, function(x) !is(x, "bird")))) stop("only bird objects are accepted.") return(TRUE) } setClass("flock", slots = c( name = "character", members = "list"), validity = valida )
3.1.10 Ejemplo de objeto S4
con slot tipo list
newFlock <- function(name, ...){ birds <- list(...) new("flock", name = name, members = birds) }
notAFlock <- newFlock("flock0", eric, 2, "hello")
Error in validityMethod(object) : only bird objects are accepted.
myFlock <- newFlock("flock1", eric, nico, sanne)
3.2 Métodos en S4
3.2.1 Métodos en S4
: setMethod
- Normalmente se definen con
setMethod
suministrando:- la clase de los objetos para esta definición del
método (
signature
) - la función a ejecutar (
definition
).
- la clase de los objetos para esta definición del
método (
setMethod('show', signature = "bird", definition = function(object) { cat("Name: ", object@name, "\n") cat("Latitude: ", summary(object@lat), "\n") cat("Longitude: ", summary(object@lon), "\n") cat("Speed: ", summary(object@speed), "\n") })
eric
Name: eric Latitude: 30.17401 30.43032 30.4624 39.05512 50.11692 51.36129 Longitude: -9.928351 -9.643971 -9.630419 -4.409152 2.65808 3.601085 Speed: 0 0.3436568 1.00312 2.300545 2.479153 63.48807 85
3.2.2 Métodos en S4
: setMethod
setMethod('show', signature = "flock", definition = function(object) { cat("Flock Name: ", object@name, "\n") N <- length(object@members) lapply(seq_len(N), function(i) { cat("Bird #", i, "\n") print(object@members[[i]]) }) })
myFlock
Flock Name: flock1 Bird # 1 Name: eric Latitude: 30.17401 30.43032 30.4624 39.05512 50.11692 51.36129 Longitude: -9.928351 -9.643971 -9.630419 -4.409152 2.65808 3.601085 Speed: 0 0.3436568 1.00312 2.300545 2.479153 63.48807 85 Bird # 2 Name: nico Latitude: 12.35442 15.89667 23.82977 31.08407 50.21611 51.51845 Longitude: -17.62615 -16.75806 -15.94404 -8.311241 3.609543 4.857561 Speed: 0 0.4876218 1.541914 2.908726 3.717069 48.38151 113 Bird # 3 Name: sanne Latitude: 13.88409 14.09925 14.50451 21.04743 22.94247 51.37478 Longitude: -17.37979 -16.81003 -16.75907 -13.88172 -16.0166 3.389886 Speed: 0 0.4049691 1.1456 2.450434 2.869024 57.20175 245
3.2.3 Métodos en S4
: setGeneric
- Es necesario que exista un método genérico ya definido.
isGeneric("as.data.frame")
[1] FALSE
- Si no existe, se define con
setGeneric
(y quizásstandardGeneric
).
setGeneric("as.data.frame")
[1] "as.data.frame"
- La función
definition
debe respetar los argumentos de la función genérica y en el mismo orden.
getGeneric("as.data.frame")
standardGeneric for "as.data.frame" defined from package "base" function (x, row.names = NULL, optional = FALSE, ...) standardGeneric("as.data.frame") <environment: 0x5590c6356310> Methods may be defined for arguments: x, row.names, optional Use showMethods("as.data.frame") for currently available ones.
3.2.4 Métodos en S4
: ejemplo con as.data.frame
setMethod("as.data.frame", signature = "bird", definition = function(x, ...) { data.frame( name = x@name, lat = x@lat, lon = x@lon, alt = x@alt, speed = x@speed, time = x@time) })
ericDF <- as.data.frame(eric)
3.2.5 Métodos en S4
: ejemplo con as.data.frame
3.2.6 Métodos en S4
: ejemplo con as.data.frame
setMethod("as.data.frame", signature = "flock", definition = function(x, ...) { dfs <- lapply(x@members, as.data.frame) dfs <- do.call(rbind, dfs) dfs$flock_name <- x@name dfs })
flockDF <- as.data.frame(myFlock)
3.2.7 Métodos en S4
: ejemplo con xyplot
library(lattice) setGeneric("xyplot") setMethod('xyplot', signature = "bird", definition = function(x, data = NULL, ...) { df <- as.data.frame(x) xyplot(lat ~ lon, data = df, ...) })
[1] "xyplot"
xyplot(eric)
3.2.8 Métodos en S4
: ejemplo con xyplot
3.2.9 Métodos en S4
: ejemplo con xyplot
setMethod('xyplot', signature = "bird", definition = function(x, data = NULL, mode = "latlon", ...) { df <- as.data.frame(x) switch(mode, lontime = xyplot(lon ~ time, data = df, ...), lattime = xyplot(lat ~ time, data = df, ...), latlon = xyplot(lat ~ lon, data = df, ...), speed = xyplot(speed ~ time, data = df, ...) ) })
xyplot(eric, mode = "lontime")
3.2.10 Métodos en S4
: ejemplo con xyplot
3.2.11 Métodos en S4
: ejemplo con xyplot
setMethod('xyplot', signature = "flock", definition = function(x, data = NULL, ...) { df <- as.data.frame(x) xyplot(lon ~ lat, group = name, data = df, auto.key = list(space = "right")) })
xyplot(myFlock)
3.3 Clases S3
con clases y métodos S4
3.3.1 Clases S3
con clases y métodos S4
Para usar objetos de clase S3
en signatures
de métodos S4
o
como contenido de slots
de una clase S4
hay que registrarlos con
setOldClass
:
setOldClass('lm')
getClass('lm')
Virtual Class "lm" [package "methods"] Slots: Name: .S3Class Class: character Extends: "oldClass" Known Subclasses: Class "mlm", directly Class "aov", directly Class "glm", directly Class "maov", by class "mlm", distance 2 Class "glm.null", by class "glm", distance 2
3.3.2 Ejemplo con lm
y xyplot
Definimos un método genérico para xyplot
library(lattice) setGeneric('xyplot')
[1] "xyplot"
Definimos un método para la clase lm
usando xyplot
.
setMethod('xyplot', signature = c(x = 'lm', data = 'missing'), definition = function(x, data, ...) { fitted <- fitted(x) residuals <- residuals(x) xyplot(residuals ~ fitted,...) })
3.3.3 Ejemplo con lm
y xyplot
Recuperamos la regresión que empleamos en el apartado de Estadística:
lmFertEdu <- lm(Fertility ~ Education, data = swiss)
summary(lmFertEdu)
Call: lm(formula = Fertility ~ Education, data = swiss) Residuals: Min 1Q Median 3Q Max -17.036 -6.711 -1.011 9.526 19.689 Coefficients: Estimate Std. Error t value Pr(>|t|) (Intercept) 79.6101 2.1041 37.836 < 2e-16 *** Education -0.8624 0.1448 -5.954 3.66e-07 *** --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 Residual standard error: 9.446 on 45 degrees of freedom Multiple R-squared: 0.4406, Adjusted R-squared: 0.4282 F-statistic: 35.45 on 1 and 45 DF, p-value: 3.659e-07
3.3.4 Ejemplo con lm
y xyplot
xyplot(lmFertEdu, col='red', pch = 19, type = c('p', 'g'))