8  Objekty

R je ve své podstatě objektově orientovaný jazyk: vše, co žije v prostředí R, jsou objekty. R je však velmi zvláštní objektově orientovaný jazyk. Na rozdíl od jiných objektových jazyků nejsou objekty (v užším smyslu slova) součástí definice vlastního jazyka, ale jsou vytvořeny v rámci tohoto jazyka. V důsledku toho (a dlouhého vývoje R a jeho předchůdce, jazyka S) dnes v R existuje několik různých systémů objektů. Z nichž nejvýznamnější je systém S3, který je nejpoužívanější a zároveň nejjednodušší. Zde se zaměříme právě na tento systém. (Vysvětlení ostatních systémů objektově orientovaného programování, OOP, v R najdete v Wickham (2019) v kapitolách 12–16; kniha je dostupná i na adrese https://adv-r.hadley.nz/.)

V této kapitole se naučíte

Cílem zde mimořádně není to, abyste vytvářeli vlastní objekty – to není při běžné datové analýze většinou potřeba; cílem je zajistit, že budete mít povšechné povědomí o tom, jak se s objekty v R zachází. K tomu je užitečné vědět, jak jsou objekty a metody zhruba implementované.

8.1 Základní pojmy OOP (pro laiky)

Základním pojmem objektově orientovaného programování je objekt. Objekt je datová struktura s jasně definovanými metodami, jak s těmito daty zacházet. Každý objekt má určitou třídu. Třída (class) popisuje strukturu dat objektu, vztah k ostatním třídám objektů (v rámci dědičnosti) a to, jaké funkce (metody) se volají při práci s objektem. Dědičnost umožňuje vytvářet mezi objekty hierarchii předků a potomků, kde potomci dědí datové struktury a metody od svých předků, mohou je však modifikovat a přidávat k nim. Dědičnost vytváří hierarchii: potomci dědí vlastnosti a chování předků. Pokud potomek nemá implementovanou nějakou metodu, pak se použije metoda jeho předka.

Např. objekt třídy člověk může obsahovat datové položky jako jméno, příjmení, výšku, váhu apod. Pro třídu člověk mohou být definovány nějaké funkce (metody), např. funkce, která spočítá BMI. Objekt třídy manažer může být logicky svázán s objektem třídy člověk – obsahuje všechna data třídy člověk a může obsahovat i další data, např. seznam podřízených. Stejně tak může třída manažer využívat metody se stejnými jmény jako třída člověk, může je však implementovat odlišně. Logickou vazbu tříd člověk a manažer popisuje dědičnost: třída manažer je potomek třídy člověk.

8.2 Systém S3

Systém S3 používá poměrně neobvyklý přístup k objektovému programování, tzv. generic function OOP. Tento přístup je jednoduchý (až primitivní), ale funguje velmi dobře a používá jej drtivá většina balíků v R. V normálních OOP 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. Nejjednodušší způsob, jak pochopit strukturu objektu, je nějaký objekt vytvořit.

S3 nemá formální definici tříd. Objekt se vytvoří tak, že nějaké základní datové struktuře (většinou seznamu) nastavíme atribut class na hodnotu jména třídy:

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

Alternativně to jde provést naráz pomocí funkce structure(), která nastavuje danému objektu atributy:

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

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

class(foo)
[1] "foo"

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 (technicky obvykle prvky seznamu) nebo změnit jeho třídu. Někdy se to hodí; nikdy to však nedělejte, pokud opravdu dobře nevíte, co děláte, jinak vznikne velmi těžko předvídatelné chování a těžko dohledatelné chyby.

Aby se zajistilo, že je struktura objektu správná, je vhodné vytvořit konstruktor, tj. funkci, která vytváří objekt. (Funkce jako numeric() jsou také konstruktory.) Ukažme si to na příkladu. Budeme chtít mít objekty třídy human, které budou obsahovat datové položky name, height a weight. Nejdříve vytvoříme konstruktor, a pak jeden objekt:

# konstruktor
human <- function(name, height, weight)
    structure(list(name = name, height = height, weight = weight),
              class = "human")
# tvorba objektu pomocí konstruktoru human()
adam <- human("Adam", 173, 63)

Dědičnost se zajistí tak, že atribut class je vektorem jmen několik tříd – zleva doprava od potomků k předkům. Budeme např. chtít mít objekty třídy manager, které jsou potomky třídy human. Opět pro ně vytvoříme konstruktor a jeden objekt:

# konstruktor
manager <- function(name, height, weight, rank)
    structure(list(name = name, height = height, weight = weight, rank = rank),
              class = c("manager", "human"))
# objekt třídy manager
eva <- manager("Eve", 169, 52, "CEO")

Eva nyní dědí vlastnosti člověka:

class(eva)
[1] "manager" "human"  
inherits(eva, "manager")
[1] TRUE
inherits(eva, "human")
[1] TRUE

Protože je většina objektů v S3 postavená pomocí seznamů, můžete zjistit strukturu objektu pomocí funkce str(). Hodnotu jednotlivých slotů objektů typu S3 můžete získat pomocí operátoru $:

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"

Hlavní pointa systému S3 spočívá v tom, že stejně pojmenovaná funkce volá pro každou třídu objektu jinou metodu (jinak implementovanou funkci) přesně uzpůsobenou dané třídě dat. Funkcím, které to dokážou, se říká generické funkce. Příkladem generické funkce je funkce print(), která tiskne různé informace v závislosti na tom, jaká je třída objektu, kterou chceme vytisknout. Třída human zatím nemá definovanou žádnou metodu pro generickou funkci print(), proto zavolá metodu pro seznam:

print(adam)
$name
[1] "Adam"

$height
[1] 173

$weight
[1] 63

attr(,"class")
[1] "human"

Když vytvoříte novou třídu objektu, můžete vytvořit i novou metodu ke generické funkci tak, že vytvoříte funkci, jejíž jméno se skládá ze jména generické funkce a třídy objektu, které jsou oddělené tečkou, tj. jmeno_genericke_funkce.jmeno_tridy. Metodu pro tisk objektů třídy human vytvoříme např. takto:

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

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 ***
Name: Eve
Height: 169
Weight: 52

Novou generickou funkci vytvoříme tak, že vytvoříme funkci, která bude obsahovat jediný řádek UseMethod("xxx"), kde xxx je jméno nové generické funkce. Řekněme, že chceme vytvořit novou generickou funkci, která vrátí pro objekty třídy human jejich body mass index. Samozřejmě musíme vytvořit i příslušné metody. Řekněme, že budeme chtít mít různou metodu bmi() pro objekt třídy human a objekt třídy manager:

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"

Nyní můžeme zjistit, jaký mají Adam a Eva BMI (bohužel je informace o BMI manažerů tajná):

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

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

8.3 Práce s objekty

Ve většině případů nebudete vytvářet vlastní objekty – stačí, když budete schopní zacházet s objekty, které vrátí funkce, které budete používat.

Nejdříve ze všeho potřebujete poznat, zda je nějaká proměnná objekt v systému S3. Jsou tři možnosti, jak to můžete udělat: 1) můžete se podívat, zda je proměnná objekt pomocí funkce is.object(), a že se nejedná o objekt v systému S4, 2) můžete použít funkci otype() z balíku sloop nebo 3) se samozřejmě můžete podívat do dokumentace:

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

Často potřebujete zjistit, jakou má daný objekt strukturu a jak se jmenují jeho jednotlivé položky. Pokud je objekt v systému S3 postaven nad seznamem, což je nejčastější případ, můžete to udělat pomocí funkce str() nebo funkce tree() z balíku lobstr. Když znáte jméno dané položky, můžete získat její obsah pomocí operátoru $:

str(eva)
List of 4
 $ name  : chr "Eve"
 $ height: num 169
 $ weight: num 52
 $ rank  : chr "CEO"
 - attr(*, "class")= chr [1:2] "manager" "human"
lobstr::tree(eva)
S3<manager/human>
├─name: "Eve"
├─height: 169
├─weight: 52
└─rank: "CEO"
eva$height
[1] 169

Někdy se hodí zjistit, jaké metody má daná třída k dispozici. K tomu slouží funkce methods(). Můžete ji použít dvě různými způsoby: 1) ke zjištění metod, které patří dané generické funkci, a 2) ke zjištění metod, které jsou k dispozici pro danou třídu:

head(methods("print"))  # metody generické funkce print() -- jen prvních 6 metod
[1] "print.acf"               "print.activeConcordance"
[3] "print.AES"               "print.anova"            
[5] "print.aov"               "print.aovlist"          
methods(class = "human")  # metody dostupné pro třídu human
[1] bmi   print
see '?methods' for accessing help and source code

Důležité je také najít dokumentaci k dané metodě. Z dokumentace ke generické funkci se totiž nemusíte se dozvědět vše, co potřebujete. Pokud chcete nápovědu k tomu, jak se generická funkce chová pro daný objekt, hledejte dokumentaci k jeho metodě (bohužel ne všechny balíky dokumentují všechny metody, které implementují):

# dokumentace k metodě pro tisk summary(m), kde m je objekt
# vráceny z\ lineární regrese
?print.summary.lm