#### Introdução a orientação a objetos no R (OO-S3) #### Links úteis: - [Package R.oo](http://cran.r-project.org/web/packages/R.oo/index.html) - [Advanced R](https://adv-r.hadley.nz/oo.html) - [Programming in R](http://manuals.bioinformatics.ucr.edu/home/programming-in-r) - [Vincent Zoonekynd](http://zoonek2.free.fr/UNIX/48_R/02.html#4) #### Introdução Observe as diferenças nos resultados das funções **plot** e **summary** abaixo: a) **plot**: ```{r setup, echo=FALSE} #opts_chunk$set(comment='') ``` ```{r plot} plot(rivers) plot(iris[, -5], col=c('red', 'green', 'blue')[unclass(iris$Species)], pch=19) plot(Titanic) ``` b) **summary**: ```{r summary} summary(rivers) summary(iris) summary(Titanic) ``` Os resultados são bem diferentes, não? Se não consegue entender o motivo dessas diferenças, estude o que está abaixo. Isso deverá ajudar a entender alguns dos detalhes "técnicos" mais importantes do R (e de profunda influência em seu uso). O uso eficiente (acima do básico) do R pressupõe conhecimentos (básicos) de Orientação a Objetos (OO)! OO é um paradigma da Ciência da Computação (não uma exclusividade do R) para a resolução de problemas computacionais. Na realidade, quase todas as linguagens de programação modernas (o R é uma delas) adotaram OO. Ex: C++, C#, Object Pascal, Python, Ruby, etc. OO é fundamentado na análise, projeto e programação de sistemas de software baseado na composição e interação entre as diversas unidades de software chamadas de objetos. Na OO, um problema real complexo qualquer é abstraido (ou seja, simplificado) em suas partes mais elementares e importantes na resolução de um problema. Abstrair, significa uma simplificação da realidade complexa em seus elementos mínimos relevantes. Surgem então os conceitos de CLASSE, INSTÂNCIA DA CLASSE e OBJETO (além de HERANÇA, ENCAPSULAMENTO e POLIMORFISMO DE MÉTODO). Na realidade o R é um grande conjunto de objetos. Tudo no R é objeto! Adicionalmente, esses objetos estão organizados numa estrutura hierárquica de classes. Mas o que são classes? As classes, são os projetos, as definições formais dessas partes simples (objetos) que irão fazer parte da resolução de um problema computacional! Cada classe define, para cada objeto, minimamente: - PROPRIEDADES: atributos, qualidades. Ou seja, como o objeto se apresenta na resolução do problema; - MÉTODOS: ação, comportamento. Ou seja, como o objeto age ou se comporta na resolução do problema. *Na OO, resolver um problema computacional qualquer significa essencialmente fazer uma modelagem de classes.* Uma classe não existe, ela não é nada mais que uma definição formal de algo que virá a existir. A classe passa a existir quando for instanciada, em outras palavras, quando uma instância da classe for efetivamente criada (o objeto real da abstração). Uma vez criadas essas instâncias (objetos) elas irão atuar na resolução do problema para o qual foram pensadas (isso é feito na modelagem de classes). ```{r class} class(rivers) class(iris) class(Titanic) ``` A resposta simples para as diferenças encontradas nas funções genéricas **plot** e **summary** no início desse tópico é: OS OBJETOS PERTENCEM A CLASSES DISTINTAS. Portanto, possuem PROPRIEDADES E MÉTODOS DISTINTOS. Vejamos um problema real que consiste em armazenar dados (de diferentes naturezas) de *n* indivíduos observados em *p* variáveis em escalas distintas. A estrutura de dados (objeto) adequada para isso é um *data.frame*: ```{r data.frame} n <- 10 p <- 5 set.seed(13) dad <- data.frame(Y1=floor(runif(n, 1, 10)), # Renda (s. mínimo) Y2=rnorm(n, 1.7, .2), # Altura Y3=rnorm(n, 60, .3), # Peso Y4=rnorm(n, 30, .4), # Idade Y5=sample(c('M', 'F'), n, rep=T)) # Sexo rownames(dad) <- paste('id', 1:nrow(dad), sep='-') dad ``` Vamos verificar as propriedades (atributos) do objeto *dad*: ```{r dad} attributes(dad) ``` E os métodos associados à instância da classe, ou seja, do objeto *dad*: onde estão? No caso do R, diferentemente da maioria das outras linguagens, os objetos de armazenamento de dados (*vetores*, *matrizes*, *arrays*, *data.frames*, *listas*, *tabelas*, *etc*) não possuem métodos. O que existem são funções genéricas (**print**, **summary**, **plot**, **mean**, *etc*) que provêem métodos para os objetos que armazenam dados: ```{r methods} methods(summary) methods(plot) methods(mean) ``` ```{r plot.dad} summary(dad) plot(dad[-5], col='red', pch=19) ``` Cada um desses métodos (genéricos) necessita saber a que classe o objeto passado como argumento para a função genérica pertence para usar um método apropriado (segundo as propriedades do objeto) na intenção de apresentar as informações (numéricas no caso de **summary** e gráficas no caso de **plot**) contidas nos objetos que armazenam dados. Esse mecanismo no R é denominado **dispatch** (despachar, expedir). Todos os métodos genéricos possuem necessariamente um método padrão (*default*). ```{r default} str(summary.default) str(plot.default) str(mean.default) ``` Todos os métodos do R são genéricos? Não! Existem métodos específicos para determinados tipos de objetos. Os métodos genéricos são apenas para as ações mais corriqueiras, comuns e usuais a vários objetos. Os métodos *default* sempre tentam retirar o máximo possível de informação de um objeto, INDEPENDENTE DE SUA CLASSE. NA AUSÊNCIA DE UM MÉTODO ESPECÍFICO PARA DETERMINADO TIPO DE OBJETO (CLASSE) É SEMPRE O MÉTODO SELECIONADO/EXECUTADO. Exemplificando. Quando digitamos: > summary(dad) Acontece o seguinte: - O interpretador R pergunta ao objeto *dad* a que classe ele pertence: ```{r} class(dad) ``` - Seleciona na lista da função genérica *summary* um método adequado (a função *grep* ajuda a localizar a função): ```{r grep} grep('data.frame', methods(summary), value=TRUE) ``` - Usa esse método (ação) *summary.data.frame* para retirar algumas informações úteis do objeto *dad*, segundo as instruções R que estão no corpo da função *summary.data.frame*: ```{r} summary(dad) ``` A classe de um objeto é uma informação que o próprio objeto armazena, como um atributo: ```{r attributes} attributes(dad) ``` Esse atributo pode ser livremente alterado pelo usuário ```{r} old.class <- class(dad) # Armazenando a classe original class(dad) <- 'test' # Atribuindo uma nova classe attributes(dad) summary(dad) # Observar a diferença em relação ao 'summary' anterior grep('^summary.test', methods(summary), value=TRUE) # Observar que não existe o método 'summary.test'. ``` Qual método foi aplicado? *summary.default* Que embora tente, não retira todas as informações de interesse do objeto *dad*, pois não é específico para essa finalidade. Retornando a classe original do objeto *dad*: ```{r} class(dad) <- old.class class(dad) summary(dad) # Novamente foi executado o método 'summary.data.frame' ``` Nesse ponto as coisas (e o poder da OO) devem estar começando a ficar um pouco mais claras para você. Adicionalmente, este mecanismo **dispatch** (despachar, expedir) do R escolher um método específico de acordo com a classe do objeto pode ser considerado POLIMORFISMO DE CLASSE. ##### Brincando com as classes (segredos da OO) ```{r} class(dad) <- c('test', 'jcfaria', 'data.frame') attributes(dad) ``` OPA..., o objeto pertence a mais de uma classe? Sim, um objeto no R (segundo a implementação OO S3) pode pertencer a mais de uma classe! Como assim S3? É que S3 foi a primeira implementação de OO no R, depois (o mundo não para mesmo) os desenvolvedores acharam que poderia ser melhor e criaram (estão criando) a S4. Mas relaxe, S3 e S4 convivem em harmonia! Estima-se que aproximadamente 80-85% do R está em S3 e 15-20% em S4. Detalhes de diferenciação entre S3 e S4 estão fora do escopo dessa introdução. Tá bom..., um objeto pode pertencer a mais de uma classe! (Isso se relaciona com HERANÇA, como será visto um pouco mais adiante) Nesses casos, como o R seleciona o método adequado via mecanismo dispatch? O retorno da função *class*, como visto, é um vetor de caracteres. O R SEMPRE VERIFICA ESSE VETOR DA ESQUERDA PARA A DIREITA. **SEMPRE**! Tente guardar (e não esquecer) isso em alguma parte de sua mente. Ou seja, no sentido do mais novo para o mais velho (HERANÇA). A primeira ocorrência encontrada é a que será executada (selecionada pelo mecanismo dispatch). No nosso caso, **summary(dad)**: 1. procura pelo método **summary.test** (como não encontra, passa par o segunda posição) 2. procura pelo método **summary.jcfaria** (idem) 3. procura por **summary.data.frame** (BINGO!) Se também não tivesse encontrado **summary.data.frame** o método **summary.default** seria eleito/selecionado. Fazendo uma alusão, isso pode ser entendido assim: - você se forma em Bacharelado; - quando faz mestrado (especializou-se, terá mais informação que quando era Bacharel e deve se chamado de MS - Mestre); - quando faz doutorado (especializou-se ainda mais, terá mais informação que quando era Mestre e deve ser chamado de DS - Doutor); - apenas se lembre que você vai sempre encontrar um Bacharel em um MS ou DS. Se você verificar sua formação acadêmica (sua classe) ela seria assim: DS, MS e Bacharel. Em poucas palavras, você foi se especializando com o tempo (assim como muitas classes do R vão também se especializando). ##### Brincando (um pouco mais) com OO ```{r OO1} summary.jcfaria <- function(object, clm=1, ...) summary(object[, clm], ...) methods(summary) # Observe a presença de 'summary.jcfaria'. grep('summary.jcf', # (se acha difícil localizar a função 'grep' pode ajudar) methods(summary), value=TRUE) summary(dad) summary(dad, clm=2) summary(dad, clm=3, digits=2) ``` Observe que na última chamada foi adicionado um argumento *digits=2* que não consta na declaração do método *summary.jcfaria*. Esse é o significado de '...' na função **summary.jcfaria**. Ou seja, pode-se passar qualquer argumento (no caso **digits**) para outras funções internas da função (método) **summary.jcfaria** sem ter que se preocupar em declará-los ou tratá-los. ##### Outro exemplo ```{r OO2} plot.jcfaria <- function(x, ...) boxplot(x, ...) methods(plot) # Observe a presença de 'plot.jcfaria' grep('plot.jcf', methods(plot), value=TRUE) plot(dad[3]) plot(dad[3], ylim=c(59.5, 60.6), xlab='Peso', col='red', main='plot.jcfaria') ``` Bom, as coisas relativas a OO no R devem estar um pouco mais claras. Adicionalmente, foi percebida e importância disso no uso, não? #### Um exemplo completo de OO em S3: desde a criação de uma função genérica, implementação de métodos e testes ##### Criando uma nova função genérica: fruta Vimos que o R tem várias funções genéricas (**summary**, **plot**, etc) e que elas realizam tarefas diferentes de acordo com a classe do objeto que é passado como argumento: **summary(objeto)**. Iremos a seguir criar uma função genérica denominada **fruta**. ```{r} fruta <- function(x, ...) UseMethod('fruta') ``` ##### Criando métodos para a nova função genérica (fruta) ```{r} fruta.default <- function(x, ...) print('Não sei qual fruta!') fruta.Abacaxi <- function(x, ...) print('Abacaxi!') fruta.Carambola <- function(x, ...) print('Carambola!') fruta.Pitanga <- function(x, ...) print(paste(x, '-', 'Pitanga!')) ``` O objeto '...' você já sabe para que serve, não? Se não, veja acima em: Brincando (um pouco mais) com OO ##### Criando outros métodos para as genéricas (default) **summary** e **plot** Podemos criar métodos adicionais para as genéricas já existentes no R: ```{r add_summary} summary.Frutas <- function(object, ...) paste('Você gosta de ', tolower(class(object)), '?', sep='', ...) summary.Abacaxi <- function(object, ...) paste(class(object), ' deve ser servido em rodelas!', sep='', ...) ``` ```{r add_plot} plot.Frutas <- function(x, ...) { plot(1, type='n', ...) text(x=1, y=1, paste(class(x), 'não são para se plotar, \n mas para comer!', sep=' ', ...), col='red', ...) } plot.Abacaxi <- function(x, ...) { plot(1, type='n', ...) text(x=1, y=1, paste(class(x), 'não é para se plotar, \n mas para comer', '\n (servido em rodelas)!', sep=' ', ...), col='red', ...) } ``` ##### Testando a nova função genérica **fruta** e seus métodos ```{r fruta} a <- 1:3 class(a) fruta(a) # 'fruta.default' class(a) <- 'Carambola' a fruta(a) # 'a' pertence a classe 'Carambola'! class(a) <- c('Jaboticaba', 'Maçã', 'Abacaxi') # 'a' irá pertencer a mais de uma classe! a fruta(a) # Busca no vetor classe para dispatch -> 'Abacaxi' class(a) <- c('Mamão', 'Pitanga', 'Abacaxi') a fruta(a) # Busca no vetor classe para dispatch -> 'Pitanga' class(a) <- 'Framboesa' fruta(a) # Irá usar o método fruta.default summary(a) # Usa o método 'summary.default' plot(a, pch=19, cex=2) ``` ##### Testando os métodos feitos para as genéricas default: **summary** e **plot** ```{r frutas} class(a) <- 'Frutas' summary(a) # Usa o método 'summary.Frutas' plot(a) # Idem class(a) <- 'Abacaxi' summary(a) # Usa o método 'summary.Abacaxi' plot(a) # Idem a <- unclass(a) attributes(a) summary(a) # Usa o método 'summary.default' plot(a) # Usa o método 'plot.default' vogais <- letters[c(1, 5, 9, 15, 21)] b <- paste('a', vogais[1:4], sep='') b class(b) ``` ##### Alterando os atributos dos objetos de dados. Note que o atributo **dim** pode alterar a classe de um objeto (conversão de classe)! ```{r} attr(b, 'dim') <- c(2, 2) b class(b) attributes(b) fruta(b) ``` Pode-se acrescentar novos atributos aos objetos! Isso deve ser feito para acrescentar alguma informação adicional útil ao objeto. Seu principal uso é na contrução de métodos. ```{r attr} attr(b, 'JCFaria') <- 'Professor da UESC/Ilhéus/Bahia' attributes(b) attributes(b)$JCFaria fruta(b) # Usa o método 'fruta.default' summary(b) # Usa o método 'summary.matrix' class(b) <- 'teste' b rownames(b) <- paste('l', 1:2, sep='') colnames(b) <- paste('c', 1:2, sep='') ``` Adicionando um novo método a função genérica **summary**: ```{r} summary.teste <- function(object, ...) { print(object[,]) cat('\n') cat(attributes(object)$JCFaria) cat('\n') } summary(b) ``` #### Encapsulamento Encapsulamento é uma opção dos desenvolvedores de um determinado objeto, via de regra funções. Basicamente consiste em ocultar detalhes da implementação do objeto para o usuário final. Como exemplo peguemos um motor. Não é necessário saber o funcionamento dele, apenas temos que saber que o método **ligar** da nossa classe *motor* irá fazer com que o nosso motor entre em funcionamento. Ou seja, do ponto de vista do criador do objeto motor, não é necessário o usuário saber quais componentes o método irá acessar, modificar ou criar para que o motor entre em funcionamento. O que o método **ligar** faz, não diz respeito a quem usa, apenas se usa e sabe-se que irá funcionar. No conceito de orientação a objetos pode-se proteger aquilo que não se quer que sofra intervenção externa: métodos e atributos. Os objetos protegidos só podem ser alterados a partir dos métodos em que foram declarados ou dentro do objeto a que eles pertencem. ```{r} methods(summary) methods(plot) ``` Observe que vários métodos das funções genéricas (**summary**, **plot**) aparecem com um asterisco (`*`). Se você digitar o nome de um desses objetos (no caso funções) encapsuladas (ou mais tecnicamente do ponto de vista do R - **não exportadas**), por exemplo: ```{r, error=TRUE} summary.aovlist # Pacote 'stats' summary.prcomp # Pacote 'stats' ## ou plot.data.frame # Pacote 'graphics' plot.prcomp # Pacote 'stats' ``` não terá acesso diretamente ao código fonte do objeto da classe função, como é comum no R. Do ponto de vista dos desenvolvedores desses objetos (funções) o usuário não tem motivos para ter acesso ao código, dai optou por os encapsular, não por maldade! Eles fazem o que fazem e ponto final. :( Particularmente, não vejo muito sentido em encapsular objetos num ambiente tão aberto e democrático quanto o R. Contudo, não fique triste. Não será necessário pegar o código fonte do R para ter acesso ao código fonte dos objetos encapsulados. Existe uma alternativa: **pacote:::objeto-encapsulado** ```{r} stats:::summary.prcomp # Pacote 'stats' graphics:::plot.data.frame # Pacote 'graphics' stats:::plot.prcomp # Pacote 'stats' ``` Ou seja, o objeto da classe operador ***`:::`*** é a chave da capsula! Considere ainda a existência de algumas funções que facilitam a vida nessas situações de métodos encapsulados ou não exportados: ```{r} argsAnywhere(plot.prcomp) getAnywhere(summary.prcomp) ``` Bom, espero que seu uso do R seja mais eficiente de agora em diante! Repasse esse script sempre que tiver dúvidas, talvez você não entenda tudo da primeira vez. Querendo contribuir com sua abrangência e qualidade: - Altere - Melhore - Mande um email (preferencialmente com script.Rmd em anexo) para: joseclaudio.faria@gmail.com