POO na linguagem R (parte 1): classes S3

R é uma linguagem orientada a objetos. Nele, absolutamente tudo é um objeto, começando pelas funções e terminando com as tabelas.


Por sua vez, todo objeto em R pertence a uma classe. De fato, no mundo à nossa volta, a situação é a mesma. Estamos cercados por objetos, e cada objeto pode ser atribuído a uma classe. Uma classe determina o conjunto de propriedades e ações que podem ser executadas com este objeto.


imagem


Por exemplo, em qualquer cozinha, há uma mesa e fogão. E a mesa e o fogão da cozinha podem ser chamados de equipamentos de cozinha. As propriedades da tabela, em regra, são limitadas por suas dimensões, cor e material a partir do qual é feita. O fogão possui uma gama mais ampla de propriedades, pelo menos energia, o número de queimadores e o tipo de fogão (elétrico ou a gás) serão obrigatórios.


Ações que podem ser executadas em objetos são chamadas de métodos. Para a mesa e o prato, respectivamente, o conjunto de métodos também será diferente. Você pode jantar à mesa, cozinhar, mas é impossível tratar os alimentos com calor, para os quais geralmente é usado um fogão.
imagem


Conteúdo



Propriedades da classe


Em R, cada objeto também pertence a uma classe. Dependendo da classe, ele possui um certo conjunto de propriedades e métodos. Em termos de programação orientada a objetos (OOP), a possibilidade de combinar similar em um conjunto de propriedades e métodos de objetos em grupos (classes) é chamada de encapsulamento .


Um vetor é a classe mais simples de objetos em R; possui a propriedade length. Por exemplo, pegaremos as letras vetoriais incorporadas.


length(letters) 

 [1] 26 

Usando a função length , obtivemos o comprimento do vetor de letras . Agora vamos tentar aplicar a mesma função ao quadro de data interno da íris .


 length(iris) 

 [1] 5 

A função de length , aplicável às tabelas, retorna o número de colunas.


As tabelas também têm outra propriedade, dimensão.


 dim(iris) 

 [1] 150 5 

A função dim no exemplo acima exibe informações de que existem 150 linhas e 5 colunas na tabela de íris .


Por sua vez, o vetor não tem dimensão.


 dim(letters) 

 NULL 

Assim, garantimos que objetos de diferentes classes tenham um conjunto diferente de propriedades.


Funções Generalizadas


R possui muitas funções genéricas: print , plot , summary etc. Essas funções funcionam de maneira diferente com objetos de diferentes classes.


Pegue, por exemplo, a função plot . Vamos executá-lo passando a tabela iris como argumento principal.


plot(iris)


Resultado:


O resultado da função plot


Agora vamos tentar passar para a função plot um vetor de 100 números aleatórios que têm uma distribuição normal.


plot(rnorm(100, 50, 30))


Resultado:


O resultado da função plot


Obtivemos gráficos diferentes, no primeiro caso, a matriz de correlação, no segundo, o gráfico de dispersão, no qual o índice de observação é exibido ao longo do eixo x e seu valor ao longo do eixo y.


Assim, a função plot pode se adaptar ao trabalho com diferentes classes. Se retornarmos à terminologia OOP, a capacidade de determinar a classe de um objeto recebido e executar várias ações com objetos de diferentes classes é chamada polimorfismo . Isso se deve ao fato de essa função ser apenas um invólucro para uma variedade de métodos escritos para trabalhar com diferentes classes. Você pode verificar isso com o seguinte comando:


 body(plot) 

 UseMethod("plot") 

O comando body imprime o corpo da função no console R. Como você pode ver, o corpo da função body consiste em apenas um UseMethod("plot") .


I.e. a função plot , apenas inicia um dos muitos métodos gravados nele, dependendo da classe do objeto passado para ele. Veja uma lista de todos os seus métodos da seguinte maneira.


 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* 

O resultado indica que a função plot possui 29 métodos, entre os quais existe plot.default , que funciona por padrão se a função receber um objeto de uma classe desconhecida na entrada.


Usando a função de methods , você também pode obter um conjunto de todas as funções generalizadas que possuem um método escrito para qualquer classe.


 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 

O que é uma classe S3 e como criar sua própria classe


Existem várias classes no R que você pode criar por conta própria. Um dos mais populares é o S3.


Esta classe é uma lista na qual várias propriedades da classe que você criou são armazenadas. Para criar sua própria classe, basta criar uma lista e dar um nome à classe.


O livro "A arte de programar em R" fornece um exemplo da classe de funcionários , que armazena informações sobre o funcionário. Como exemplo para este artigo, também decidi usar um objeto para armazenar informações sobre funcionários. Mas tornou mais complexo e funcional.


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

Assim, criamos nossa própria classe, que armazena os seguintes dados em sua estrutura:


  • Nome do Funcionário
  • Sobrenome do funcionário
  • Salário
  • A hora em que o salário foi estabelecido
  • Salário anterior
  • Data e hora da última atualização de informações

Depois disso, com o comando class(employee1) <- "emp" atribuímos a classe emp ao objeto.


Para a conveniência de criar objetos da classe emp, você pode escrever uma função.


Código de função para criar objetos da classe 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" 

Funções de atribuição a classes S3 personalizadas


Então, criamos nossa própria classe emp , mas até agora isso não nos deu nada. Vamos ver por que criamos nossa própria classe e o que podemos fazer com ela.


Primeiro, você pode escrever funções de atribuição para a turma criada.


Função de atribuição 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) } 

Função de atribuição 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) } 

Ao criar, as funções de atribuição são sempre citadas e têm a seguinte aparência: "[<-. " / "[[<-. " . E eles têm 3 argumentos obrigatórios.


  • x - O objeto ao qual o valor será atribuído;
  • i - Nome / índice do elemento do objeto (nome, sobrenome, salário, salário_data, data_alta, atualização);
  • valor - o valor atribuído.

Mais adiante, no corpo da função, você escreve como os elementos da sua classe devem mudar. No meu caso, quero que o usuário possa alterar apenas o salário (elemento salário , cujo índice é 3) . Portanto, dentro da função, escrevo um if ( i == "salary" || i == 3 ) check if ( i == "salary" || i == 3 ) . Se o usuário tentar editar outras propriedades, ele receberá a mensagem "You can't change anything except salary" .


Quando o elemento do salário é alterado, é exibida uma mensagem contendo o nome e o sobrenome do empregado, seu atual e o novo nível de salário. O salário atual é passado para a propriedade previous_sallary e o salário recebe um novo valor. Os valores das propriedades employee_datetime e update também são atualizados.


Agora você pode tentar alterar o salário.


 employee1["salary"] <- 1750 

 Oleg Petrov has changed salary from 1500 to 1750 

Desenvolvendo Métodos Customizados para Funções Genéricas


Anteriormente, você já aprendeu que em R existem funções generalizadas que alteram seu comportamento, dependendo da classe recebida na entrada do objeto.


Você pode adicionar seus métodos a funções generalizadas existentes e até criar suas próprias funções generalizadas.


Uma das funções genéricas mais usadas é a print . Essa função é acionada toda vez que você chama um objeto por seu nome. Agora, a saída de impressão do objeto emp class que criamos se parece com isso:


 $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" 

Vamos escrever nosso método para a função de impressão.


 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) } 

Agora, a função de impressão pode imprimir objetos da nossa classe emp . Basta digitar o nome do objeto no console e obter a seguinte saída.


 employee1 

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

Criando Funções e Métodos Genéricos


A maioria das funções genéricas dentro tem a mesma aparência e apenas usa a função UseMethod .


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

Agora, escreveremos dois métodos para ele, um para trabalhar com objetos da classe emp , o segundo método será iniciado por padrão para objetos de todas as outras classes, para trabalhar com os quais nossa função generalizada não possui um método escrito separadamente.


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

O nome do método consiste no nome da função e na classe de objetos que esse método processará. O método padrão será executado sempre que você passar um objeto de classe no qual o método não está gravado.


 get_salary(employee1) 

 [1] 1750 

 get_salary(iris) 

 Work only with emp class objects 

Herança


Outro termo que você encontrará ao aprender programação orientada a objetos.


imagem


Tudo o que é mostrado na imagem pode ser classificado como uma classe de transporte . De fato, todos esses objetos têm um método comum - movimento e propriedades comuns, por exemplo, velocidade. No entanto, todos os 6 objetos podem ser divididos em três subclasses: terra, água e ar. Nesse caso, a subclasse herdará as propriedades da classe pai, mas também terá propriedades e métodos adicionais. Uma propriedade semelhante na estrutura da programação orientada a objetos é chamada herança .


Em nosso exemplo, podemos alocar trabalhadores remotos para uma subclasse separada de remote_emp . Esses funcionários terão uma propriedade adicional: cidade de residência.


 #    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" 

Ao atribuir uma classe, criar uma subclasse, usamos um vetor no qual o primeiro elemento é o nome da subclasse, seguido pelo nome da classe pai.


No caso de herança, todas as funções e métodos generalizados escritos para trabalhar com a classe pai funcionarão corretamente com suas subclasses.


 #    remote_emp   employee2 

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

 #   salary   remote_emp get_salary(employee2) 

 [1] 500 

Mas você pode desenvolver métodos separadamente para cada subclasse.


 #     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 da seguinte maneira. Primeiro, a função generalizada procura por um método escrito para a subclasse remote_emp ; se não o encontrar, vai mais longe e procura um método escrito para a classe pai emp .


Quando você pode usar suas próprias classes


É improvável que a funcionalidade de criar suas próprias classes S3 seja útil para aqueles que estão apenas começando sua jornada no domínio da linguagem R.


Pessoalmente, eles foram úteis no desenvolvimento do pacote rfacebookstat . O fato é que, na API do Facebook, o parâmetro action_breakdowns existe para carregar eventos e responder a publicações de publicidade em vários grupos.


Ao usar esses agrupamentos, você obtém uma resposta na forma de uma estrutura JSON no seguinte formato:


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

O número e o nome dos elementos para diferentes action_breakdowns são diferentes, portanto, para cada um, você precisa escrever seu próprio analisador. Para resolver esse problema, usei a funcionalidade para criar classes S3 personalizadas e uma função generalizada com um conjunto de métodos.


Ao solicitar estatísticas sobre eventos com agrupamentos, dependendo dos valores dos argumentos, foi definida uma classe que foi atribuída à resposta recebida da API. A resposta foi passada para uma função genérica e, dependendo da classe especificada anteriormente, foi determinado um método que analisou o resultado. Qualquer pessoa interessada em se aprofundar nos detalhes da implementação pode encontrar o código para criar uma função e métodos generalizados, e aqui está o seu uso.


No meu caso, usei classes e métodos para processá-los exclusivamente dentro do pacote. Se você geralmente fornecer ao usuário do pacote uma interface para trabalhar com as classes que você criou, todos os métodos deverão ser incluídos como uma diretiva S3method no arquivo S3method , conforme a seguir.


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

Conclusão


Como fica claro no título do artigo, esta é apenas a primeira parte, porque em R, além das classes S3 , existem outras: S4 , R5 ( RC ), R6 . No futuro, tentarei escrever sobre cada uma dessas implementações OOP. No entanto, qualquer pessoa com um nível de inglês que lhes permita ler livremente livros, Headley Wickham é bastante sucinta e, com exemplos, abordou esse tópico em seu livro "Advanced R" .


Se de repente, em um artigo, perdi algumas informações importantes sobre as classes S3, ficarei grato se você escrever sobre isso nos comentários.

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


All Articles