B Programação Orientada a Objetos
Como vimos anteriormente, o R é uma linguagem de programação orientada à objetos. Dois conceitos fundamentais desse tipo de linguagem são os de classe e método. Já vimos também que todo objeto no R possui uma classe (que define sua estrutura) e analisamos algumas delas. O que seria então um método? Para responder essa pergunta precisamos entender inicialmente os tipos de orientação a objetos que o R possui.
O R possui 3 sitemas de orientação a objetos: S3, S4, e RC:
- S3: implementa um estilo de programação orientada a objeto chamada de generic-function. Esse é o estilo mais básico de programação em R (e também o mais utilizado). A ideia é que existam funções genéricas que decidem qual método aplicar de acordo com a classe do objeto. Os métodos são definidos da mesma forma que qualquer função, mas chamados de maneira diferente. É um estilo de programação mais “informal”, mas possibilita uma grande liberdade para o programador.
- S4: é um estilo mais formal, no sentido que que as funções genéricas devem possuir uma classe formal definida. Além disso, é possível também fazer o despacho múltiplo de métodos, ao contrário da classe S3.
- RC: (Reference Classes, antes chamado de R5) é o sistema mais novo implementado no R. A principal diferença com os sistemas S3 e S4 é que métodos pertencem à objetos, não à funções. Isso faz com que objetos da classe RC se comportem mais como objetos da maioria das linguagens de programação, como Python, Java, e C#.
Nesta sessão vamos abordar como funcionam os métodos como definidos pelo sistema S3, por ser o mais utilizado na prática para se criar novas funções no R. Para saber mais sobre os outros métodos, consulte o livro Advanced R.
Vamos entender como uma função genérica pode ser criada através de um
exemplo. Usando a função methods()
, podemos verificar quais métodos
estão disponíveis para uma determinada função, por exemplo, para a
função mean()
:
methods(mean)
1] mean.Date mean.default mean.difftime mean.POSIXct
[5] mean.POSIXlt mean.quosure* mean.vctrs_vctr*
['?methods' for accessing help and source code see
O resultado são expressões do tipo mean.<classe>
, onde <classe>
é
uma classe de objeto como aquelas vistas anteriormente. Isso significa
que a função mean()
, quando aplicada à um objeto da classe Date
, por
exemplo, pode ter um comportamento diferente quando a mesma função for
aplicada à um objeto de outra classe (numérica).
Suponha que temos o seguinte vetor numérico:
set.seed(1)
<- rnorm(100)
vec class(vec)
1] "numeric" [
e queremos calcular sua média. Basta aplicar a função mean()
nesse
objeto para obtermos o resultado esperado
mean(vec)
1] 0.1088874 [
Mas isso só é possível porque existe um método definido espcificamente
para um vetor da classe numeric
, que nesse caso é a função
mean.default
. A função genérica nesse caso é a mean()
, e a função
método é a mean.default
. Veja que não precisamos escrever o nome
inteiro da função genérica para que ela seja utilizada, como por exemplo,
mean.default(vec)
1] 0.1088874 [
Uma vez passado um objeto para uma função, é a classe do objeto que irá
definir qual método utilizar, de acordo com os métodos disponíveis. Veja
o que acontece se forçarmos o uso da função mean.Date()
nesse vetor
mean.Date(vec)
1] "1970-01-01" [
O resultado não faz sentido pois ele é específico para um objeto da
classe Date
.
Pode-se ainda alterar a classe de um objeto, que passa a ser tratado pelo método associado a esta classe, se houver.
class(vec) <- "Date"
mean(vec)
1] "1970-01-01" [
Em S3 um objeto pode ter mais de uma classe e neste caso a procura por método segue a ordem de especificação. Procura-se, sequencialmente, métodos para todos as classes definidas. Veja esta sequência de três atribuições de classes.
class(vec) <- c("numeric", "Date")
mean(vec)
1] "1970-01-01"
[class(vec) <- c("Date", "numeric")
mean(vec)
1] "1970-01-01"
[class(vec) <- c("foo", "Date")
mean(vec)
1] "1970-01-01" [
Em todos casos foi processada o método mean.Date()
.
Na primeira e terceira porque não existe método específico para
classes numeric
ou foo
. Na segunda porque Date
era a primeira classe
do objeto para qual há um método específico.
Tudo isso acontece por causa de um mecanismo chamado de despacho de
métodos (method dispatch), que é responsável por identificar a
classe do objeto e utilizar (“despachar”) a função método correta para
aquela classe. Toda função genérica possui a mesma forma: uma chamada
para a função UseMethod()
, que especifica o nome genérico e o objeto a
ser despachado. Por exemplo, veja o código fonte da função mean()
meanfunction (x, ...)
UseMethod("mean")
<bytecode: 0x55832c9eb590>
<environment: namespace:base>
Agora veja o código fonte da função mean.default
, que é o método
específico para vetores numéricos
mean.defaultfunction (x, trim = 0, na.rm = FALSE, ...)
{if (!is.numeric(x) && !is.complex(x) && !is.logical(x)) {
warning("argument is not numeric or logical: returning NA")
return(NA_real_)
}if (isTRUE(na.rm))
<- x[!is.na(x)]
x if (!is.numeric(trim) || length(trim) != 1L)
stop("'trim' must be numeric of length one")
<- length(x)
n if (trim > 0 && n) {
if (is.complex(x))
stop("trimmed means are not defined for complex data")
if (anyNA(x))
return(NA_real_)
if (trim >= 0.5)
return(stats::median(x, na.rm = FALSE))
<- floor(n * trim) + 1
lo <- n + 1 - lo
hi <- sort.int(x, partial = unique(c(lo, hi)))[lo:hi]
x
}.Internal(mean(x))
}<bytecode: 0x55832cdb9058>
<environment: namespace:base>
Agora suponha que você ddeseja criar uma função que calcule a média para
um objeto de uma classe diferente daquelas previamente definidas. Por
exemplo, suponha que você quer que a função mean()
retorne a média das
linhas de uma matriz.
set.seed(1)
<- matrix(rnorm(50), nrow = 5)
mat mean(mat)
1] 0.1004483 [
O resultado é a média de todos os elementos, e não de cada linha. Nesse caso, podemos definir nossa própria função método para fazer o cálculo que precisamos. Por exemplo:
<- function(x, ...) rowMeans(x) mean.matrix
Uma função método é sempre definida dessa forma:
<funçãogenérica>.<classe>
. Agora podemos ver novamente os métodos
disponíveis para a função mean()
methods(mean)
1] mean.Date mean.default mean.difftime mean.matrix
[5] mean.POSIXct mean.POSIXlt mean.quosure* mean.vctrs_vctr*
['?methods' for accessing help and source code see
e simplesmente aplicar a função genérica mean()
à um objeto da classe
matrix
para obter o resultado que desejamos
class(mat)
1] "matrix" "array"
[mean(mat)
1] 0.09544402 0.12852087 0.06229588 -0.01993810 0.23591872 [
Esse exemplo ilustra como é simples criar funções método para
diferentes classes de objetos. Poderíamos fazer o mesmo para objetos das
classes data.frame
e list
<- function(x, ...) sapply(x, mean, ...)
mean.data.frame <- function(x, ...) lapply(x, mean) mean.list
Aplicando em objetos dessas classes específicas, obtemos:
## Data frame
set.seed(1)
<- data.frame(c1 = rnorm(10),
da c2 = runif(10))
class(da)
1] "data.frame"
[mean(da)
c1 c2 0.1322028 0.4183230
## Lista
set.seed(1)
<- list(rnorm(10), runif(50))
dl class(dl)
1] "list"
[mean(dl)
1]]
[[1] 0.1322028
[
2]]
[[1] 0.4946632 [
Obviamente esse processo todo é extremamente importante ao se criar
novas funções no R. Podemos tanto criar uma função genérica (como a
mean()
) e diversos métodos para ela usando classes de objetos
existentes, quanto (inclusive) criar novas classes e funções método para
elas. Essa é uma das grandes lberdades que o método S3 de orientação à
objetos permite, e possivelmente um dos motivos pelos quais é
relativamente simples criar pacotes inteiros no R.