9.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 se nějaké základní datové struktuře (většinou seznamu) nastaví atribut class
na hodnotu jména třídy:
Alternativně to jde provést naráz pomocí funkce structure()
, která nastavuje danému objektu atributy:
Nyní je proměnná foo
objekt třídy 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:
human <- function(name, height, weight) # konstruktor
structure(list(name = name, height = height, weight = weight),
class = "human")
adam <- human("Adam", 173, 63) # tvorba objektu pomocí konstruktoru human()
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:
manager <- function(name, height, weight, rank) # konstruktor
structure(list(name = name, height = height, weight = weight, rank = rank),
class = c("manager", "human"))
eva <- manager("Eve", 169, 52, "CEO") # objekt třídy manager
Eva nyní dědí vlastnosti člověka:
## [1] "manager" "human"
## [1] TRUE
## [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 $
:
## List of 4
## $ name : chr "Eve"
## $ height: num 169
## $ weight: num 52
## $ rank : chr "CEO"
## - attr(*, "class")= chr [1:2] "manager" "human"
## [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:
## $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 má tvar jmeno_genericke_funkce.jmeno_tridy
, tj. jméno generické funkce oddělené tečkou od názvu třídy objektu. 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:
## *** 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á):
## [1] 21.04982
## [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
:
## [1] "Unknown class"
(Bez definování implicitní metody by předchozí řádek skončil chybou.)