Objektorientiertes Programmieren
Objektorientiertes Programmieren (OOP) basiert auf den beiden Konzepten "Klasse" und "Methode". Die Klasse eines Objekts definiert das Verhalten von Objekten. Bei der Auswahl einer Methode wird ebenfalls die Klasse berücksichtigt. Methoden können sich unterschiedlich verhalten je nach Klasse.
In R existieren mehrere Systeme zum OOP. Am bekanntesten sind die S3 und S4 Systeme. Wir werden uns auf das (einfachere) S3 System beschränken.
S3
S3 ist informell und erlaubt somit das schnelle Erzeugen von S3 Objekten. Die meisten Objekte sind S3 Objekte. In der Basisversion gibt es aber keine "einfache" Möglichkeit zur Überprüfung ob es sich um ein S3 Objekt oder z.B. ein Basisobjekt handelt. Daher bietet es sich an mit pryr::otype()
zu arbeiten.
> library(pryr)
> df <- data.frame(x = 1:5, y = letters[1:5])
> otype(df)
## [1] "S3"
> otype(df$x)
## [1] "base"
> is.object(df) & !isS4(df)
## [1] TRUE
Methoden
In S3 werden Methoden den jeweiligen Funktionen zugeordnet. Diese werden dann generische Funktionen genannt. Eine generische Funktion erkennt man an UseMethod()
im Source Code oder mit pryr::ftype()
.
> mean
## function (x, ...)
## UseMethod("mean")
## <bytecode: 0x0000000008500da0>
## <environment: namespace:base>
> ftype(mean)
## [1] "s3" "generic"
Für eine gegebene Klasse bestimmt die generische Funktion die passende S3 Methode. Deren Name ist von der Form generic.class()
, wie z.B. print.factor()
.
Bemerkung: Daher sollte man
.
nicht im Namen eigener Funktionen verwenden. Dies kann leicht zu Verwirrungen führen.
pryr::ftype()
hilft aber auch in solchen Fällen weiter.
> ftype(t.data.frame)
## [1] "s3" "method"
> ftype(t.test)
## [1] "s3" "generic"
Obwohl beide Funktionen aussehen wie eine Methode, ist t.test()
keine Methode der Funktion t()
für die (nicht vorhandene) test
Klasse. t.data.frame()
ist hingegen die Transponierungs-Methode für Data Frames.
Alle Methoden einer generischen Funktion erhält man mit methods()
.
> methods(mean)
## [1] mean.Date mean.default mean.difftime mean.POSIXct mean.POSIXlt
## see '?methods' for accessing help and source code
> methods(head)
## [1] head.data.frame* head.default* head.ftable* head.function*
## [5] head.matrix head.table*
## see '?methods' for accessing help and source code
Die Methoden von mean()
sind alle sichtbar. head()
hat hingegen verborgene Methoden. Diese sind mit einem * gekennzeichnet. Den Source Code einer "verborgenen" Funktion erhält man mit getS3method()
.
> head.default
## Error in eval(expr, envir, enclos): Objekt 'head.default' nicht gefunden
> getS3method("head", "default")
## function (x, n = 6L, ...)
## {
## stopifnot(length(n) == 1L)
## n <- if (n < 0L)
## max(length(x) + n, 0L)
## else min(n, length(x))
## x[seq_len(n)]
## }
## <bytecode: 0x00000000073d5510>
## <environment: namespace:utils>
Alle generischen Funktionen zu einer speziellen Klasse erhält man ebenfalls mit methods()
.
> methods(class = "factor")
## [1] [ [[ [[<- [<- all.equal
## [6] as.character as.data.frame as.Date as.list as.logical
## [11] as.POSIXlt as.vector coerce droplevels format
## [16] initialize is.na<- length<- levels<- Math
## [21] Ops plot print relevel relist
## [26] rep show slotsFromS3 summary Summary
## [31] xtfrm
## see '?methods' for accessing help and source code
Klasse definieren
Mit class()
kann die Klasse eines Objekts abgefragt und auch gesetzt werden. structure()
erlaubt die Definition eines Objekts mit Klassenangabe in einem Befehl.
> m_obj <- structure(list(x = 1:4, y = c(3,7)), class = "m_klasse")
> m_obj <- list(x = 1:4, y = c(3,7))
> class(m_obj) <- "m_klasse"
>
> class(m_obj)
## [1] "m_klasse"
m_obj
"erbt" nun Eigenschaften der Klasse m_klasse
> inherits(m_obj, "m_klasse")
## [1] TRUE
Für viele S3 Klassen existiert eine Funktion zur Konstruktion von Objekten dieser Klasse, wie z.B. factor(), data.frame(), ...
Daher wollen wir nun auch eine solche Funktion für die Klasse m_klasse
schreiben. Objekte aus dieser Klasse - wie zuvor gesehen - sind Listen mit numerischen Einträgen. Demzufolge könnte eine solche Funktion die nachfolgende Form haben.
> m_klasse <- function(x, y){
+ if(!is.numeric(x) | !is.numeric(y))
+ stop("x und y muessen numerisch sein")
+ structure(list(x, y), class = "m_klasse")
+ }
> m_obj <- m_klasse(1:3, "a")
## Error in m_klasse(1:3, "a"): x und y muessen numerisch sein
Da der Input nicht die passende Struktur hatte, liefert m_klasse()
- wie definiert - eine Fehlermeldung.
> m_obj <- m_klasse(x = 1:3, y = c(4,5))
> class(m_obj)
## [1] "m_klasse"
Neue Methoden und generische Funktionen
Zur Definition einer neuen generischen Funktion benutzt man UseMethod()
.
> m_generic <- function(x) UseMethod("m_generic")
Eine generische Funktion ist nutzlos ohne (mindestens) eine Methode. Daher definieren wir nun für die Klasse m_klasse
eine Methode zur generischen Funktion m_generic()
.
> m_generic.m_klasse <- function(x) x[1]
> ftype(m_generic.m_klasse)
## [1] "s3" "method"
Die Methode m_generic.m_klasse()
gibt also für ein Objekt der Klasse m_klasse
das erste Listenelement zurück.
> m_generic(m_obj)
## [[1]]
## [1] 1 2 3
Ebenso kann man eine neue Methode für eine bereits existierende generische Funktion definieren.
> mean.m_klasse <- function(x) mean(x[[1]])
Die Methode berechnet also den empirischen Mittelwert des ersten Listenelements.
> mean(m_obj)
## [1] 2
Methodenauswahl
UseMethod()
erzeugt einen Vektor von Funktionsnamen vom Typ
> paste0("generic",".",c("klasse","default"))
## [1] "generic.klasse" "generic.default"
und sucht nach den entsprechenden Funktionen. Für unsere generische Funktion m_generic()
liegen bisher ja folgende Methoden vor.
> methods(m_generic)
## [1] m_generic.m_klasse
## see '?methods' for accessing help and source code
Es ist also nur eine Methode vorgesehen für die Klasse m_klasse
. Daher ist klar, dass der Befehl
> m_generic(1:5)
## Error in UseMethod("m_generic"): nicht anwendbare Methode für 'm_generic' auf Objekt der Klasse "c('integer', 'numeric')" angewendet
zu einem Fehler führt. Es sollte daher immer eine Default Methode angegeben werden.
> m_generic.default <- function(x) "m_generic nicht definiert fuer diese Klasse"
> m_generic(structure(list(1, 2), class = c("d_klasse", "m_klasse")))
## [[1]]
## [1] 1
> m_generic(structure(list(), class = c("e_klasse")))
## [1] "m_generic nicht definiert fuer diese Klasse"
Die nachfolgende Tabelle enthät eine Übersicht wichtiger Funktionen in Bezug auf S3 Objekte.
Funktion | Beschreibung |
---|---|
attribute() |
Erfragen und Setzen aller Attribute eines Objekts |
attr() |
Erfragen und Setzen konkreter Attribute |
class() |
Erfragen und Setzen des Klassen-Attributs |
inherits() |
Von einer anderen Klasse Merkmale erben |
methods() |
Alle zu einer generischen Funktion gehörenden Methoden |
NextMethod() |
Verweis auf die nächste in Frage kommende Methode |
UseMethod() |
Verweis von der generischen Funktion an die Methode |
S4
Das S4 System arbeitet ähnlich wie S3. Es ist aber deutlich formeller gestaltet. Hauptunterschiede:
- eine formellere Klassen-Definition wird verwendet (Slots/Felder der Objekte müssen beschrieben werden)
- die Methodenauswahl kann sich auf mehr als ein Argument beziehen
- der Operator
@
erlaubt den Zugriff auf Slots/Felder eines S4 Objekts
Weitere Informationen zu S4 findet man in Advanced R: S4 und den dort angegebenen Referenzen.