OOP v R

R je objektově orientovaný jazyk.

Objekty však nejsou vlastní součástí definice jazyka, ale jsou vytvořeny v jeho rámci.

R má několik různých systémů objektů: S3, S4, Reference classes a další.

Zde se podíváme na nejdůležitější základy systému S3, který je nejpoužívanější a nejjednodušší.

Další systémy jsou popsané v AdvR.

Základní pojmy (pro laiky)

Každý objekt má nějakou třídu.

Třída (class) popisuje

  • strukturu dat objektu
  • vztah k ostatním třídám objektů (v rámci dědičnosti)
  • jaké funkce (metody) se volají při práci s objektem

Základní pojmy: příklad

Objekt třídy "člověk"

  • obsahuje atributy: jméno, příjmení, výška, váha
  • má definovány funkce pro výpočet BMI

Objekt třídy "manažer" dědí od třídy "člověk"

  • obsahuje stejná data (může obsahovat i další, např. seznam podřízených)
  • může využívat funkce se stejnými jmény (které však mohou být implementovány odlišně)

Třída "manažer" je potomek třídy "člověk".

Základní pojmy: dědičnost

Dědičnost vytváří hierarchii: potomci dědí vlastnosti a chování předků.

Pokud potomek nemá implementovanou nějakou funkci, pak se použije funkce jeho předka.

Systém S3

Systém S3 používá "generic function OOP".

V normálních OO jazycích patří objektu nebo třídě jak data, tak metody (funkce).

V R však objektu patří jen data a jméno třídy; metody patří tzv. generické funkci.

Vytvoření třídy a objektu

S3 nemá formální definici tříd proměnných.

Objekt se vytvoří tak, že se vytvoří nějaký základní typ objektu (většinou seznam) a nastaví se mu atribut class:

foo <- list()        # vytvoří prázdný seznam
class(foo) <- "foo"  # přiřadí mu třídu "foo"

Alternativně naráz pomocí funkce structure():

foo <- structure(list(), class = "foo")

Nyní je proměnná foo objekt třídy "foo":

class(foo)
## [1] "foo"

Konstruktory

R nemá žádný mechanismus, který by kontroloval, zda je datová struktura objektu správná.

Již vytvořenému objektu je možné přidat nebo ubrat datové sloty i změnit jeho třídu (nedělejte to, pokud opravdu dobře nevíte, co děláte).

Aby se zajistilo, že je struktura objektu správná, je vhodné vytvořit konstruktor – funkci, která vytváří objekt:

human <- function(name, height, weight) 
    structure(list(name = name, height = height, weight = weight),
              class = "human")
adam <- human("Adam", 173, 63)

Potomci lidí

Dědičnost: atribut class je vektorem jmen několik tříd – zleva doprava od potomků k předkům:

manager <- function(name, height, weight, rank)
    structure(list(name = name, height = height, weight = weight, rank = rank),
              class = c("manager", "human"))
eva <- manager("Eve", 169, 52, "CEO")
class(eva)
## [1] "manager" "human"
c(manager = inherits(eva, "manager"), human = inherits(eva, "human"))
## manager   human 
##    TRUE    TRUE

Získání dat z objektu

Protože je většina objektů v S3 postavená pomocí seznamů, můžete získat hodnotu jednotlivých slotů objektů pomocí $:

str(eva)
## List of 4
##  $ name  : chr "Eve"
##  $ height: num 169
##  $ weight: num 52
##  $ rank  : chr "CEO"
##  - attr(*, "class")= chr [1:2] "manager" "human"
eva$rank
## [1] "CEO"

Nová metoda (1)

Stejně pojmenovaná (generická) funkce volá pro každý typ objektu jiný kód – uzpůsobený danému typu dat.

Příklad: print() – zatím se tiskne jako seznam:

print(adam)
## $name
## [1] "Adam"
## 
## $height
## [1] 173
## 
## $weight
## [1] 63
## 
## attr(,"class")
## [1] "human"

Nová metoda (2)

Funkce print() pro třídu "human":

print.human <- function(x)
    cat("*** Human ***",
        paste("Name:", x$name),
        paste("Height:", x$height),
        paste("Weight:", x$weight),
        sep = "\n")
print(adam)
## *** Human ***
## Name: Adam
## Height: 173
## Weight: 63

Nová metoda (3)

Protože třída "manager" je potomkem třídy "human", dědí její chování. To znamená, že pokud tato třída nemá definovanou svou metodu print(), pak zavolá metodu svého předka, třídy "human":

eva  # implicitně se volá funkce print.human
## *** Human ***
## Name: Eve
## Height: 169
## Weight: 52

Nová generická funkce (1)

Novou generickou funkci jde vytvořit takto:

bmi <- function(x)          # generická funkce
    UseMethod("bmi")
bmi.human <- function(x)    # metoda pro třídu "human"
    x$weight / (x$height / 100) ^ 2
bmi.manager <- function(x)  # metoda pro třídu "manager"
    "classified"

Nová generická funkce (2)

Nyní můžeme zjistit, jaký mají Adam a Eva BMI:

bmi(adam)
## [1] 21.04982
bmi(eva)
## [1] "classified"

(Bohužel, informace o BMI manažerů je tajná.)

Nová generická funkce (3)

Generická funkce může mít i implicitní metodu, která se použije v případě, že pro daný typ není žádná metoda k dispozici. Tato metoda se jmenuje default:

bmi.default <- function(x)
    "Unknown class"
bmi(1)
## [1] "Unknown class"

(Bez definování implicitní metody by předchozí řádek skončil chybou.)

Jak poznat objekt S3

Poznat, že je něco objekt v systému S3 není úplně snadné. Jsou tři možnosti:

is.object(eva) & !isS4(eva)
## [1] TRUE
pryr::otype(eva)  # funkce z balíku pryr
## [1] "S3"

Nebo se samozřejmě podíváte do dokumentace funkce, kterou používáte.

Struktura objektu

Většina objektů systému S3 je postavená nad seznamy, takže jejich strukturu zjistíte funkcí str() a složky získáte pomocí $:

str(eva)
## List of 4
##  $ name  : chr "Eve"
##  $ height: num 169
##  $ weight: num 52
##  $ rank  : chr "CEO"
##  - attr(*, "class")= chr [1:2] "manager" "human"
eva$height
## [1] 169

Které metody jsou k dispozici

Metody, které patří dané generické funkci:

head(methods("print"))  # jen prvních 6 metod (zajistí funkce head())
## [1] "print.acf"     "print.AES"     "print.anova"   "print.aov"    
## [5] "print.aovlist" "print.ar"

Metody, které jsou k dispozici pro nějakou třídu:

methods(class = "human")
## [1] bmi   print
## see '?methods' for accessing help and source code

Nebo: apropos() s regulárním výrazem:

apropos(".*\\.human")
## [1] "bmi.human"   "print.human"

Nápověda k metodám

Nápověda ke generické funkci nemusí říct, jak se funkce používá pro danou třídu objektu – hledejte dokumentaci ke konkrétní metodě.

Příklad: odhad lineárního modelu je objekt třídy "lm".

help(plot)  # dokumentace ke generické funkci
help(plot.lm)  # dokumentace k metodě pro třídu lm

Více informací

Detailnější informace o objektových systémech S3, S4 a ReferenceClass najdete v AdvR, kap. 7.