OOP en el lenguaje R (parte 1): clases S3

R es un lenguaje orientado a objetos. En él, absolutamente todo es un objeto, comenzando por funciones y terminando con tablas.


A su vez, cada objeto en R pertenece a una clase. De hecho, en el mundo que nos rodea, la situación es casi la misma. Estamos rodeados de objetos, y cada objeto puede atribuirse a una clase. Una clase determina el conjunto de propiedades y acciones que se pueden realizar con este objeto.


imagen


Por ejemplo, en cualquier cocina hay una mesa y una estufa. Y la mesa de la cocina y la estufa se pueden llamar equipos de cocina. Las propiedades de la tabla, como regla, están limitadas por sus dimensiones, color y material del que está hecha. La estufa tiene una gama más amplia de propiedades, al menos potencia, será obligatorio el número de quemadores y el tipo de estufa (eléctrica o de gas).


Las acciones que se pueden realizar en los objetos se denominan sus métodos. Para la tabla y el plato, respectivamente, el conjunto de métodos también será diferente. Puede cenar en la mesa, puede cocinar en ella, pero es imposible tratar los alimentos con calor, para lo cual generalmente se usa una estufa.
imagen


Contenido



Propiedades de clase


En R, cada objeto también pertenece a una clase. Dependiendo de la clase, tiene un cierto conjunto de propiedades y métodos. En términos de programación orientada a objetos (OOP), la posibilidad de combinar similares en un conjunto de propiedades y métodos de objetos en grupos (clases) se denomina encapsulación .


Un vector es la clase más simple de objetos en R; tiene la propiedad de longitud. Por ejemplo, tomaremos las letras vectoriales incorporadas.


length(letters) 

 [1] 26 

Usando la función de length , obtuvimos la longitud del vector de letras . Ahora intentemos aplicar la misma función al marco de fecha incorporado del iris .


 length(iris) 

 [1] 5 

La función de length , aplicable a las tablas, devuelve el número de columnas.


Las tablas también tienen otra propiedad, dimensión.


 dim(iris) 

 [1] 150 5 

La función de dim en el ejemplo anterior muestra información de que hay 150 filas y 5 columnas en la tabla de iris .


A su vez, el vector no tiene dimensión.


 dim(letters) 

 NULL 

Por lo tanto, nos aseguramos de que los objetos de diferentes clases tengan un conjunto diferente de propiedades.


Funciones generalizadas


R tiene muchas funciones genéricas: print , plot , summary , etc. Estas funciones funcionan de manera diferente con objetos de diferentes clases.


Tomemos, por ejemplo, la función de plot . Ejecútelo pasando la tabla del iris como argumento principal.


plot(iris)


Resultado:


El resultado de la función de trazado


Ahora intentemos pasar a la función de plot un vector de 100 números aleatorios que tengan una distribución normal.


plot(rnorm(100, 50, 30))


Resultado:


El resultado de la función de trazado


Obtuvimos diferentes gráficos, en el primer caso, la matriz de correlación, en el segundo, el diagrama de dispersión, en el que se muestra el índice de observación a lo largo del eje xy su valor a lo largo del eje y.


Por lo tanto, la función de plot puede adaptarse para trabajar con diferentes clases. Si volvemos a la terminología de OOP, la capacidad de determinar la clase de un objeto entrante y realizar diversas acciones con objetos de diferentes clases se llama polimorfismo . Esto se debe al hecho de que esta función es solo un contenedor para una variedad de métodos escritos para trabajar con diferentes clases. Puede verificar esto con el siguiente comando:


 body(plot) 

 UseMethod("plot") 

El comando body imprime el cuerpo de la función en la consola R. Como puede ver, el cuerpo de la función cuerpo consta de un solo UseMethod("plot") .


Es decir la función de plot , solo inicia uno de los muchos métodos escritos en él, dependiendo de la clase del objeto que se le pasa. Vea una lista de todos sus métodos de la siguiente manera.


 methods(plot) 

  [1] plot.acf* plot.data.frame* plot.decomposed.ts* [4] plot.default plot.dendrogram* plot.density* [7] plot.ecdf plot.factor* plot.formula* [10] plot.function plot.hclust* plot.histogram* [13] plot.HoltWinters* plot.isoreg* plot.lm* [16] plot.medpolish* plot.mlm* plot.ppr* [19] plot.prcomp* plot.princomp* plot.profile.nls* [22] plot.raster* plot.spec* plot.stepfun [25] plot.stl* plot.table* plot.ts [28] plot.tskernel* plot.TukeyHSD* 

El resultado indica que la función de trazado tiene 29 métodos, entre los cuales hay plot.default , que funciona de manera predeterminada si la función recibe un objeto de una clase desconocida en la entrada.


Usando la función de methods , también puede obtener un conjunto de todas las funciones generalizadas que tienen un método escrito para cualquier clase.


 methods(, "data.frame") 

  [1] $<- [ [[ [[<- [5] [<- aggregate anyDuplicated as.data.frame [9] as.list as.matrix by cbind [13] coerce dim dimnames dimnames<- [17] droplevels duplicated edit format [21] formula head initialize is.na [25] Math merge na.exclude na.omit [29] Ops plot print prompt [33] rbind row.names row.names<- rowsum [37] show slotsFromS3 split split<- [41] stack str subset summary [45] Summary t tail transform [49] type.convert unique unstack within 

¿Qué es una clase S3 y cómo crear tu propia clase?


Hay varias clases en R que puedes crear tú mismo. Uno de los más populares es S3.


Esta clase es una lista en la que se almacenan varias propiedades de la clase que creó. Para crear su propia clase, simplemente cree una lista y asígnele un nombre de clase.


El libro "The Art of Programming in R" da un ejemplo de la clase de empleado , que almacena información sobre el empleado. Como ejemplo de este artículo, también decidí tomar un objeto para almacenar información sobre los empleados. Pero lo hizo más complejo y funcional.


 #    employee1 <- list(name = "Oleg", surname = "Petrov", salary = 1500, salary_datetime = Sys.Date(), previous_sallary = NULL, update = Sys.time()) #    class(employee1) <- "emp" 

Por lo tanto, creamos nuestra propia clase, que almacena los siguientes datos en su estructura:


  • Nombre del empleado
  • Apellido del empleado
  • Salario
  • El momento en que se estableció el salario.
  • Salario anterior
  • Fecha y hora de la última actualización de información.

Después de eso, con el comando class(employee1) <- "emp" asignamos la clase emp al objeto.


Para la conveniencia de crear objetos de clase emp, puede escribir una función.


Código de función para crear objetos de clase emp
 #     create_employee <- function(name, surname, salary, salary_datetime = Sys.Date(), update = Sys.time()) { out <- list(name = name, surname = surname, salary = salary, salary_datetime = salary_datetime, previous_sallary = NULL, update = update) class(out) <- "emp" return(out) } #    emp    create_employee employee1 <- create_employee("Oleg", "Petrov", 1500) #     class(employee1) 

 [1] "emp" 

Funciones de asignación a clases S3 personalizadas


Entonces, creamos nuestro propio emprendimiento de clase, pero hasta ahora esto no nos ha dado nada. Veamos por qué creamos nuestra propia clase y qué podemos hacer con ella.


En primer lugar, puede escribir funciones de asignación para la clase creada.


Función de asignación para [
 "[<-.emp" <- function(x, i, value) { if ( i == "salary" || i == 3 ) { cat(x$name, x$surname, "has changed salary from", x$salary, "to", value) x$previous_sallary <- x$salary x$salary <- value x$salary_datetime <- Sys.Date() x$update <- Sys.time() } else { cat( "You can`t change anything except salary" ) } return(x) } 

Función de asignación para [[
 "[[<-.emp" <- function(x, i, value) { if ( i == "salary" || i == 3 ) { cat(x$name, x$surname, "has changed salary from", x$salary, "to", value) x$previous_sallary <- x$salary x$salary <- value x$salary_datetime <- Sys.Date() x$update <- Sys.time() } else { cat( "You can`t change anything except salary" ) } return(x) } 

Las funciones de asignación en la creación siempre se indican entre comillas, y se ven así: "[<-. " / "[[<-. " . Y tienen 3 argumentos requeridos.


  • x : el objeto al que se asignará el valor;
  • i - Nombre / índice del elemento del objeto (nombre, apellido, salario, salario-fecha-hora, previo_sallar, actualizar);
  • valor : el valor asignado.

Además, en el cuerpo de la función, escribe cómo deben cambiar los elementos de su clase. En mi caso, quiero que el usuario pueda cambiar solo el salario (elemento de salario , cuyo índice es 3) . Por lo tanto, dentro de la función, escribo un cheque if ( i == "salary" || i == 3 ) . Si el usuario intenta editar otras propiedades, recibe el mensaje "You can't change anything except salary" .


Cuando se cambia el elemento de salario, se muestra un mensaje que contiene el nombre y el apellido del empleado, su nivel de salario actual y nuevo. El salario actual se pasa a la propiedad anterior_sallary y se le asigna un nuevo valor. Los valores de las propiedades salary_datetime y update también se actualizan.


Ahora puedes intentar cambiar el salario.


 employee1["salary"] <- 1750 

 Oleg Petrov has changed salary from 1500 to 1750 

Desarrollo de métodos personalizados para funciones genéricas.


Anteriormente, ya aprendió que en R hay funciones generalizadas que cambian su comportamiento dependiendo de la clase recibida en la entrada del objeto.


Puede agregar sus métodos a funciones generalizadas existentes e incluso crear sus propias funciones generalizadas.


Una de las funciones genéricas más utilizadas es print . Esta función se activa cada vez que llama a un objeto por su nombre. Ahora la salida de impresión del objeto de clase emp que creamos se ve así:


 $name [1] "Oleg" $surname [1] "Petrov" $salary [1] 1750 $salary_datetime [1] "2019-05-29" $previous_sallary [1] 1500 $update [1] "2019-05-29 11:13:25 EEST" 

Escribamos nuestro método para la función de impresión.


 print.emp <- function(x) { cat("Name:", x$name, x$surname, "\n", "Current salary:", x$salary, "\n", "Days from last udpate:", Sys.Date() - x$salary_datetime, "\n", "Previous salary:", x$previous_sallary) } 

Ahora la función de impresión puede imprimir objetos de nuestra clase emp . Simplemente ingrese el nombre del objeto en la consola y obtenga el siguiente resultado.


 employee1 

 Name: Oleg Petrov Current salary: 1750 Days from last udpate: 0 Previous salary: 1500 

Crear funciones y métodos genéricos


La mayoría de las funciones genéricas en el interior se ven iguales y solo usan la función UseMethod .


 #   get_salary <- function(x, ...) { UseMethod("get_salary") } 

Ahora escribiremos dos métodos para él, uno para trabajar con objetos de la clase emp , el segundo método se lanzará por defecto para objetos de todas las otras clases, para trabajar con los cuales nuestra función generalizada no tiene un método escrito por separado.


 #      emp get_salary.emp <- function(x) x$salary #      get_salary.default <- function(x) cat("Work only with emp class objects") 

El nombre del método consiste en el nombre de la función y la clase de objetos que procesará este método. El método predeterminado se ejecutará cada vez que pase un objeto de clase en el que el método no está escrito.


 get_salary(employee1) 

 [1] 1750 

 get_salary(iris) 

 Work only with emp class objects 

Herencia


Otro término que encontrará cuando aprenda la programación orientada a objetos.


imagen


Todo lo que se muestra en la imagen se puede clasificar como una clase de transporte . De hecho, todos estos objetos tienen un método común: movimiento y propiedades comunes, por ejemplo, la velocidad. Sin embargo, los 6 objetos se pueden dividir en tres subclases: tierra, agua y aire. En este caso, la subclase heredará las propiedades de la clase principal, pero también tendrá propiedades y métodos adicionales. Una propiedad similar en el marco de la programación orientada a objetos se llama herencia .


En nuestro ejemplo, podemos asignar trabajadores remotos a una subclase separada de remote_emp . Dichos empleados tendrán una propiedad adicional: ciudad de residencia.


 #    employee2 <- list(name = "Ivan", surname = "Ivanov", salary = 500, salary_datetime = Sys.Date(), previous_sallary = NULL, update = Sys.time(), city = "Moscow") #    remote_emp class(employee2) <- c("remote_emp", "emp") #    class(employee2) 

 [1] "remote_emp" "emp" 

Cuando asignamos una clase, creamos una subclase, usamos un vector en el que el primer elemento es el nombre de la subclase, seguido del nombre de la clase padre.


En el caso de la herencia, todas las funciones y métodos generalizados escritos para trabajar con la clase padre funcionarán correctamente con sus subclases.


 #    remote_emp   employee2 

 Name: Ivan Ivanov Current salary: 500 Days from last udpate: 0 Previous salary: 

 #   salary   remote_emp get_salary(employee2) 

 [1] 500 

Pero puede desarrollar métodos por separado para cada subclase.


 #     salary   remote_emp get_salary.remote_emp <- function(x) { cat(x$surname, "remote from", x$city, "\n") return(x$salary) } 

 #   salary   remote_emp get_salary(employee2) 

 Ivanov remote from Moscow [1] 500 

Funciona de la siguiente manera. Primero, la función genérica busca un método escrito para la subclase remote_emp , si no lo encuentra, va más allá y busca un método escrito para la clase padre emp .


Cuando puedes usar tus propias clases


Es poco probable que la funcionalidad de crear sus propias clases S3 sea útil para aquellos que recién comienzan su viaje en el dominio del lenguaje R.


Personalmente, fueron útiles para desarrollar el paquete rfacebookstat . El hecho es que en la API de Facebook, el parámetro action_breakdowns existe para cargar eventos y responder a publicaciones publicitarias en varios grupos.


Al usar tales agrupaciones, obtiene una respuesta en forma de estructura JSON en el siguiente formato:


 { "action_name": "like", "action_type": "post_reaction", "value": 6 } { "action_type": "comment", "value": 4 } 

El número y el nombre de los elementos para diferentes action_breakdowns es diferente, por lo que para cada uno debe escribir su propio analizador. Para resolver este problema, utilicé la funcionalidad para crear clases S3 personalizadas y una función generalizada con un conjunto de métodos.


Al solicitar estadísticas sobre eventos con agrupaciones, dependiendo de los valores de los argumentos, se definió una clase que se asignó a la respuesta recibida de la API. La respuesta se pasó a una función genérica y, según la clase especificada anteriormente, se determinó un método que analizaba el resultado. A quién le importa profundizar en los detalles de la implementación, aquí puede encontrar el código para crear una función y métodos generalizados, y aquí está su uso.


En mi caso, utilicé clases y métodos para procesarlos exclusivamente dentro del paquete. Si generalmente necesita proporcionar al usuario del paquete una interfaz para trabajar con las clases que creó, entonces todos los métodos deben incluirse como directiva S3method en el archivo S3method , de la siguiente manera.


 S3method(_,) S3method("[<-",emp) S3method("[[<-",emp) S3method("print",emp) 

Conclusión


Como queda claro en el título del artículo, esta es solo la primera parte, porque en R, además de las clases S3 , hay otros: S4 , R5 ( RC ), R6 . En el futuro intentaré escribir sobre cada una de estas implementaciones de OOP. Sin embargo, cualquier persona con un nivel de inglés que les permita leer libros libremente, entonces Headley Wickham es bastante sucinto, y con ejemplos cubrieron este tema en su libro "Advanced R" .


Si de repente en un artículo me perdí alguna información importante sobre las clases S3, te agradeceré que escribas sobre esto en los comentarios.

Source: https://habr.com/ru/post/453964/


All Articles