FoRt: R & OOP Fabian Scheipl Institut für Statistik LMU München Objekt-Orientierte Programmierung in R OOP Systeme in R • Basistypen: interne (C) Datentypen • S3: OO mit generischen Funktionen. generics entscheiden welche Methode benutzt wird. Zwanglos, keine formelle Klassendefinition oder Objektvalidierung • S4: Formellere & mächtigere Variante von S3: – formelle Klassendefinition kontrolliert Repräsentation, Konstruktion, Vererbung – multiple dispatch: generische Funktionen wählen Methoden basierend auf der Klasse von mehreren Funktions-Argumenten, nicht nur der Klasse des ersten wie bei S3 – Standard für Bioconductor-Pakete. • Reference classes (RC): mehr wie Java oder Python: – kein copy-on-modify sondern mutable objects – Methoden gehören zu Objekten, nicht zu generischen Funktionen Basistypen Basistypen • • • • • jedes R Objekt wird als C Objekt (struct) angelegt dieses definiert wie das Objekt tatsächlich abgespeichert wird vector und list sind Basistypen (base types). nur R Core kann neue Basistypen definieren Abfrage mit typeof: typeof(t.test) ## [1] "closure" typeof(globalenv()) ## [1] "environment" typeof(pi) ## [1] "double" 1 Relevante Basistypen • • • • • • • • • • • Vektoren: logical, integer, double, complex, character list environment Funktionen: closure language-Objekte mit modes expression, name, call symbol promise expression S4 NULL ... Basistypen & OOP systems • S3 Objekte: typischerweise Listen aus verschiedenen Basistypen • S4 Objekte: spezieller Basistyp deren Felder Basistypen oder andere S4 Objekte enthalten • RC Objekte sind eine Kombination aus S4 und environments. S3 S3 • ältestes & einfachstes OO System in R • einziges OO System das in den base, graphics, utils, datasets und stats Paketen benutzt wird • informell und ad hoc, aber ausreichend für die meisten Projekte (IMO, YMMV) S3 Prinzip Methoden sind mit Funktionen – genauer: generics– assoziiert, nicht mit Objekten oder Klassen Grundidee: • Generische Funktionen definieren ein Label für eine ganze Sammlung von Funktionen (Methoden) • Wenn eine generische Funktion aufgerufen wird, gibt sie die Argumente weiter an eine spezialisierte Methode die für die Klasse des (üblicherweise) ersten Arguments definiert ist (method dispatch) S3 Beispiel: print() • print(x) muss sehr unterschiedliche Dinge tun je nachdem was x ist. . . • S3 nimmt es dem Nutzer ab selbst angeben zu müssen was für eine print-Funktion benutzt werden soll • die richtige Methode für das Ausgeben von x wird ermittelt indem die S3-Klasse von x abgefragt wird (method dispatch) 2 S3: Generische Funktionen • Quellcode einer generic macht üblicherweise nur einen Aufruf von UseMethod() • UseMethod() baldowert aus welche Methode die richtige ist und ruft diese auf (method dispatch) Beispiel: print ## ## ## ## function (x, ...) UseMethod("print") <bytecode: 0x13bbc48> <environment: namespace:base> predict ## ## ## ## function (object, ...) UseMethod("predict") <bytecode: 0x17bbf90> <environment: namespace:stats> S3: Methoden • S3-Methoden heißen immer <generic>.<class>() (ein Grund mehr bei der Programmierung nicht den Punkt als Trennzeichen für Namen zu verwenden) • benutze methods(<generic>) um zu sehen welche Methoden für eine generic definiert sind: methods(mean) ## [1] mean.Date mean.default mean.difftime mean.POSIXct ## see '?methods' for accessing help and source code mean.POSIXlt • S3: Methods • zeige alle generics die Methoden für eine gegebene Klasse haben: methods(class = "ts") ## ## ## ## ## ## [1] aggregate [6] diffinv [11] Math2 [16] plot [21] [<see '?methods' for as.data.frame cbind coerce diff initialize kernapply Math monthplot na.omit print show slotsFromS3 [ t window<accessing help and source code 3 cycle lines Ops time window methods(class = "lm") ## ## ## ## ## ## ## ## ## ## ## [1] add1 alias anova [5] coerce confint cooks.distance [9] dfbeta dfbetas drop1 [13] effects extractAIC family [17] hatvalues influence initialize [21] labels logLik model.frame [25] nobs plot predict [29] proj qr residuals [33] rstudent show simulate [37] summary variable.names vcov see '?methods' for accessing help and source code case.names deviance dummy.coef formula kappa model.matrix print rstandard slotsFromS3 S3: Methods • viele S3-Methoden sind nicht direkt einsehbar • benutze getS3method() um deren Quellcode zu betrachten: getS3method(f = "extractAIC", class = "lm") ## ## ## ## ## ## ## ## ## ## ## ## function (fit, scale = 0, k = 2, ...) { n <- length(fit$residuals) edf <- n - fit$df.residual RSS <- deviance.lm(fit) dev <- if (scale > 0) RSS/scale - n else n * log(RSS/n) c(edf, dev + k * edf) } <bytecode: 0x2b56568> <environment: namespace:stats> S3 Objekte S3-Klasse eines Objekts wird durch sein class-Attribut festgelegt: • class(x) gibt das class-Attribut zurück • class<-() setzt das class-Attribut eines Objekts und damit die S3-Klasse • inherits(x, "some_class") checkt ob x zur Klasse some_class gehört. that_guy <- "Josef Ackermann" class(that_guy) <- "top1percenter" attributes(that_guy) ## $class ## [1] "top1percenter" 4 class(that_guy) ## [1] "top1percenter" inherits(that_guy, "top1percenter") ## [1] TRUE S3: Klassendefinition & Objekterstellung Simpel, ad-hoc: • es gibt keine Klassendefinition: nur implizit durch bestehenden Code definiert • jedes dahergelaufene R-Objekt mit "whatev" %in% attribute(<object>, "class") ist ein Objekt der Klasse whatev (!!) S3 Objekte • class kann auch ein Vektor sein • geht von engster zu breitester/allgemeinster Klasse: (o <- ordered(1 : 3)) ## [1] 1 2 3 ## Levels: 1 < 2 < 3 class(o) ## [1] "ordered" "factor" • S3-Objekte sind meist base type list oder ein Vektor mit Attributen: attributes(o) ## ## ## ## ## $levels [1] "1" "2" "3" $class [1] "ordered" "factor" typeof(o) ## [1] "integer" 5 S3 Objekte • S3-Klassen haben üblicherweise eine Konstruktor-Funktion • immer benutzen falls vorhanden (z.B. factor(), data.frame()) • Konstruktor sollte genauso heißen wie die Klasse deren Objekte er erzeugt: top1percenter <- function(x, wealth) { if (wealth < 1e7) { stop(x, " is not filthy rich and not a member of the top 1%.") } attr(x, "wealth") <- wealth class(x) <- "top1percenter" x } that_guy <- top1percenter("Josef Ackermann", wealth = 238e6) top1percenter("F.S. aus M.", wealth = -1000) ## Error in top1percenter("F.S. aus M.", wealth = -1000): F.S. aus M. is not filthy rich and not a membe Neue generics definieren • • • • Funktion new_generic definieren die UseMethod("new_generic", "arg") aufruft arg ist das Argument dessen Klasse für den method dispatch abgefragt wird (default: erstes Argument) UseMethod() bekommt keinerlei andere Argumente der generic! generics sollten immer ...-Argument akzeptieren: gewährleistet Flexibilität für neue Methoden insult <- function(object, ...) UseMethod("insult") Neue Methoden Ganz normale Funktionsdefinition: • mit dem korrekten <generic>.<class> Namen • mit den Argumenten der generic: insult.top1percenter <- function(object, ...) { message(object, " ist ein Ausbeuter, Plutokrat & Feind der Arbeiterklasse.") } insult(that_guy) ## Josef Ackermann ist ein Ausbeuter, Plutokrat & Feind der Arbeiterklasse. Neue Methoden Ganz normale Funktionsdefinition: • mit dem korrekten <generic>.<class> name • mit den Argumenten der generic 6 insult.top1percenter <- function(object, ...) { message(object, " ist ein Ausbeuter, Plutokrat & Feind der Arbeiterklasse.") } insult(that_guy) ## Josef Ackermann ist ein Ausbeuter, Plutokrat & Feind der Arbeiterklasse. Neue Methoden Methoden für bestehende generics: print ## ## ## ## function (x, ...) UseMethod("print") <bytecode: 0x13bbc48> <environment: namespace:base> print.top1percenter <- function(x, ...) { cat(x, "\n Klasse: Top 1%\n Vermögen: ", formatC(attr(x, "wealth"), digits = 8), "€\n") } that_guy ## Josef Ackermann ## Klasse: Top 1% ## Vermögen: 2.38e+08 € • muss selbst sicherstellen dass neue Methode zu generic (und anderen Methoden) passt. Method Dispatch • UseMethod() sucht nach Funktionen mit Namen wie some_generic.<class(x)[1]>, some_generic.<class(x)[2]>, etc., und some_generic.default, in dieser Reihenfolge. • die erste die gefunden wird wird ausgewertet • auf default-Methode wird zurückgegriffen wenn keine Methode für die Klasse implementiert ist: insult.default <- function(x, ...) { insult <- sample(c("Tsibfigladscha", "Loamsiada", "Breznsoiza", "Haumdaucha"), 1) message(substitute(x), ", du ", insult, "!") } methods("insult") ## [1] insult.default insult.top1percenter ## see '?methods' for accessing help and source code 7 class(iris) ## [1] "data.frame" insult(iris) ## iris, du Breznsoiza! S4 S4 Ähnlich wie S3, aber formeller und strikter: • formelle Klassendefinition für – Felder der Klasse & Art ihrer Einträge – Vererbungsstruktur (Elternklassen) – optional: Validitätsprüfung, Prototypen-Instanz • multiple dispatch für Methoden basierend auf mehreren Argumenten einer generic Implementation im Paket methods. Sehr häufig verwendet in Bioconductor-Paketen. S4: Beispiel library(methods) library(stats4) y <- rpois(10, lambda = 5) negative_loglik <- function(lambda) -sum(dpois(y, lambda = lambda, log=TRUE)) ml_estimate <- mle(negative_loglik, start = list(lambda = 1), nobs = length(y)) ml_estimate ## ## ## ## ## ## ## Call: mle(minuslogl = negative_loglik, start = list(lambda = 1), nobs = length(y)) Coefficients: lambda 4.500008 Inspektion von S4 Objekten isS4(ml_estimate) ## [1] TRUE 8 is(ml_estimate) ## [1] "mle" is(ml_estimate, "mle" ) ## [1] TRUE str(ml_estimate, 2) ## Formal class 'mle' [package "stats4"] with 9 slots ## ..@ call : language mle(minuslogl = negative_loglik, start = list(lambda = 1), nobs = length(y) ## ..@ coef : Named num 4.5 ## .. ..- attr(*, "names")= chr "lambda" ## ..@ fullcoef : Named num 4.5 ## .. ..- attr(*, "names")= chr "lambda" ## ..@ vcov : num [1, 1] 0.45 ## .. ..- attr(*, "dimnames")=List of 2 ## ..@ min : num 18.6 ## ..@ details :List of 6 ## ..@ minuslogl:function (lambda) ## .. ..- attr(*, "srcref")=Class 'srcref' atomic [1:8] 4 20 4 77 20 77 4 4 ## .. .. .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x17cefe0> ## ..@ nobs : int 10 ## ..@ method : chr "BFGS" Orientierung in S4 • Klassendokumentation: class?mle oder ?"mle-class" • Welche Klassen und generische Funktionen sind im Paket definiert? getClasses("package:stats4") ## [1] "mle" "profile.mle" "summary.mle" getGenerics("package:stats4") ## ## ## ## ## ## ## An object of class "ObjectsWithPackage": Object: "AIC" "BIC" "coef" "confint" "logLik" "nobs" "plot" Package: "stats" "stats" "stats" "stats" "stats" "stats" "graphics" Object: "profile" "show" "summary" "update" "vcov" Package: "stats" "methods" "base" "stats" "stats" Orientierung in S4 Welche Methodensignaturen gibt es für eine generic? 9 showMethods("plot") ## Function: plot (package graphics) ## x="ANY", y="ANY" ## x="profile.mle", y="missing" showMethods("AIC") ## Function: AIC (package stats) ## object="ANY" Quellcode einer Methode: getMethod("plot", signature = c(x = "profile.mle", y = "missing")) ## Method Definition: ## ## function (x, y, ...) ## { ## .local <- function (x, levels, conf = c(99, 95, 90, 80, 50)/100, ## nseg = 50, absVal = TRUE, ...) ## { ## obj <- x@profile ## confstr <- NULL ## if (missing(levels)) { ## levels <- sqrt(qchisq(pmax(0, pmin(1, conf)), 1)) ## confstr <- paste0(format(100 * conf), "%") ## } ## if (any(levels <= 0)) { ## levels <- levels[levels > 0] ## warning("levels truncated to positive values only") ## } ## if (is.null(confstr)) { ## confstr <- paste0(format(100 * pchisq(levels^2, 1)), ## "%") ## } ## mlev <- max(levels) * 1.05 ## nm <- names(obj) ## opar <- par(mar = c(5, 4, 1, 1) + 0.1) ## if (absVal) { ## for (i in nm) { ## sp <- splines::interpSpline(obj[[i]]$par.vals[, ## i], obj[[i]]$z, na.action = na.omit) ## bsp <- splines::backSpline(sp) ## xlim <- predict(bsp, c(-mlev, mlev))$y ## if (is.na(xlim[1L])) ## xlim[1L] <- min(obj[[i]]$par.vals[, i]) ## if (is.na(xlim[2L])) ## xlim[2L] <- max(obj[[i]]$par.vals[, i]) ## dev.hold() ## plot(abs(z) ~ par.vals[, i], data = obj[[i]], ## xlab = i, ylim = c(0, mlev), xlim = xlim, ylab = expression(abs(z)), 10 ## type = "n") ## avals <- rbind(as.data.frame(predict(sp)), data.frame(x = obj[[i]]$par.vals[, ## i], y = obj[[i]]$z)) ## avals$y <- abs(avals$y) ## lines(avals[order(avals$x), ], col = 4) ## abline(v = predict(bsp, 0)$y, h = 0, col = 3, ## lty = 2) ## for (lev in levels) { ## pred <- predict(bsp, c(-lev, lev))$y ## lines(pred, rep(lev, 2), type = "h", col = 6, ## lty = 2) ## pred <- ifelse(is.na(pred), xlim, pred) ## lines(pred, rep(lev, 2), type = "l", col = 6, ## lty = 2) ## } ## dev.flush() ## } ## } ## else { ## for (i in nm) { ## sp <- splines::interpSpline(obj[[i]]$par.vals[, ## i], obj[[i]]$z, na.action = na.omit) ## bsp <- splines::backSpline(sp) ## xlim <- predict(bsp, c(-mlev, mlev))$y ## x0 <- predict(bsp, 0)$y ## if (is.na(xlim[1L])) ## xlim[1L] <- min(obj[[i]]$par.vals[, i]) ## if (is.na(xlim[2L])) ## xlim[2L] <- max(obj[[i]]$par.vals[, i]) ## dev.hold() ## plot(z ~ par.vals[, i], data = obj[[i]], xlab = i, ## ylim = c(-mlev, mlev), xlim = xlim, ylab = expression(z), ## type = "n") ## lines(predict(sp), col = 4) ## abline(h = 0, v = x0, col = 3, lty = 2) ## for (lev in levels) { ## pred <- predict(bsp, c(-lev, lev))$y ## lines(pred, c(-lev, lev), type = "h", col = 6, ## lty = 2) ## pred <- ifelse(is.na(pred), xlim, pred) ## lines(c(x0, pred[2L]), rep(lev, 2), type = "l", ## col = 6, lty = 2) ## lines(c(pred[1L], x0), rep(-lev, 2), type = "l", ## col = 6, lty = 2) ## } ## dev.flush() ## } ## } ## par(opar) ## } ## .local(x, ...) ## } ## <bytecode: 0x2cf60f0> ## <environment: namespace:stats4> 11 ## ## Signatures: ## x y ## target "profile.mle" "missing" ## defined "profile.mle" "missing" S4 Klassen sind definiert durch • Klassenname (oft “kleingeschriebenMitBinnenMajuskeln”). • slots: benamte Liste von Feldern definiert Feldnamen und erlaubte Einträge • contains: gibt die Elternklasse(n) an (optional) Zusätzlich: • prototype: Defaultwerte für slots • validity: Funktion die die Gültigkeit übergebener Feldwerte überprüft. – gibt TRUE zurück falls alle Feldwerte valide sind – sonst Fehlermeldung (character) S4 Klassen setClass("citizen", slots = list(name = "character", age = "numeric", job = "character"), prototype = list(name = "Anna Musterfrau", age=40, job="rocket scientist") ) setClass("top1Percenter", slots = list(butler = "citizen", illuminati="logical", wealth = "numeric"), contains = "citizen", prototype = list(name = "A. Usbeuter", age=70, job="heir", butler=new("citizen"), illuminati=TRUE, wealth=1e9), validity = function(object) { invalids <- character(0) wrong_job <- !(object@job %in% c("banker", "CEO", "heir")) poor <- object@wealth < 1e8 if (wrong_job) invalids <- "wrong <job> for a plutocrat." if (poor) invalids <- c(invalids, "too little <wealth> for a plutocrat.") if (length(invalids)) invalids else TRUE } ) getClass("citizen") ## ## ## ## ## ## ## ## Class "citizen" [in ".GlobalEnv"] Slots: Name: name Class: character age job numeric character Known Subclasses: "top1Percenter" 12 Anlegen von S4 Objekten albert <- new("citizen", name = "Albert", age = 42, job = "domestic servant") joseph_a <- new("top1Percenter", name = "J. Ackermann", job = "banker", age = 66, butler = albert, illuminati = TRUE) str(joseph_a, 2) ## Formal class 'top1Percenter' [package ".GlobalEnv"] with 6 slots ## ..@ butler :Formal class 'citizen' [package ".GlobalEnv"] with 3 slots ## ..@ illuminati: logi TRUE ## ..@ wealth : num 1e+09 ## ..@ name : chr "J. Ackermann" ## ..@ age : num 66 ## ..@ job : chr "banker" albert ## ## ## ## ## ## ## ## ## An object of class "citizen" Slot "name": [1] "Albert" Slot "age": [1] 42 Slot "job": [1] "domestic servant" Anlegen von S4 Objekten new("top1Percenter", name = "F.S.", job = "Akademisches Prekariat", age = 35, butler = joseph_a, illuminati = FALSE, wealth = 2e4) ## Error in validObject(.Object): invalid class "top1Percenter" object: 1: wrong <job> for a plutocrat. ## invalid class "top1Percenter" object: 2: too little <wealth> for a plutocrat. • besser als new(): benutze Konstruktorfunktionen für S4 Objekte falls vorhanden. Zugriff auf S4 Objekte albert@job ## [1] "domestic servant" 13 slot(joseph_a, "age") ## [1] 66 Definition von S4 generics & Methoden Prä-existierende generic: setGeneric("print") setMethod("print", signature = c(x = "citizen"), function(x) { paste0(x@name, " is a ", x@age, "-year old ", x@job, ".") } ) print ## ## ## ## ## ## ## standardGeneric for "print" defined from package "base" function (x, ...) standardGeneric("print") <environment: 0x2cc2780> Methods may be defined for arguments: x Use showMethods("print") for currently available ones. print(albert) ## [1] "Albert is a 42-year old domestic servant." print(joseph_a) ## [1] "J. Ackermann is a 66-year old banker." Definition von S4 generics & Methoden Neue generic: setClass("Couple", slots = list(person1 = "citizen", person2 = "citizen")) setGeneric("marry", function(lover1, lover2, ...) { standardGeneric("marry") }) ## [1] "marry" 14 setMethod("marry", c(lover1 = "citizen", lover2 = "citizen"), function(lover1, lover2) { message("how wonderful: ", lover1@name, " & ", lover2@name, " will tie the knot.") new("Couple", person1 = lover1, person2 = lover2) } ) ## [1] "marry" happy_couple <- marry(albert, joseph_a) ## how wonderful: Albert & J. Ackermann will tie the knot. str(happy_couple, 2) ## Formal class 'Couple' [package ".GlobalEnv"] with 2 slots ## ..@ person1:Formal class 'citizen' [package ".GlobalEnv"] with 3 slots ## ..@ person2:Formal class 'top1Percenter' [package ".GlobalEnv"] with 6 slots Definition von S4 generics & Methoden Spezielle Datentypen/Signaturen ANY, missing setMethod("marry", c(lover1 = "citizen", lover2 = "ANY"), function(lover1, lover2) { message(lover1@name, " can only get married to a <citizen>, not to some ", mode(lover2), ".") } ) ## [1] "marry" marry(albert, "Albert's teddybear") ## Albert can only get married to a <citizen>, not to some character. Definition von S4 generics & Methoden Spezielle Datentypen/Signaturen ANY, missing: setMethod("marry", c(lover1 = "citizen", lover2 = "missing"), function(lover1, lover2) { message(lover1@name, " needs to find somebody to love so they can marry.") } ) 15 ## [1] "marry" marry(albert) ## Albert needs to find somebody to love so they can marry. Methodenvererbung callNextMethod() ruft die Methode auf die aufgerufen worden wäre wenn die aktuelle Methode nicht existieren würde: setMethod("marry", c(lover1 = "top1Percenter", lover2 = "citizen"), function(lover1, lover2) { message("alert the tabloids -- draft a pre-nup -- put the Moet on ice!") callNextMethod() } ) ## [1] "marry" rich_couple <- marry(joseph_a, albert) ## alert the tabloids -- draft a pre-nup -- put the Moet on ice! ## how wonderful: J. Ackermann & Albert will tie the knot. Method dispatch get("AIC", pos = "package:stats4") ## ## ## ## ## ## ## standardGeneric for "AIC" defined from package "stats" function (object, ..., k = 2) standardGeneric("AIC") <environment: 0x2684798> Methods may be defined for arguments: object, k Use showMethods("AIC") for currently available ones. get("AIC", pos = "package:stats") ## ## ## ## function (object, ..., k = 2) UseMethod("AIC") <bytecode: 0x1c408b8> <environment: namespace:stats> ⇒ S3:UseMethod ≈ S4:standardGeneric 16 S4: . . . but wait, there’s more • virtuelle Klassen • setClassUnion() • multiple inheritance S3 vs. S4 Operation S3 S4 Klassendefinition implizit, am besten über Konstruktorfunktion setClass() Objekterzeugung Konstruktor oder Setzen von class-Attribut new() oder Konstruktor Feld/Attribut auslesen je nach base type des Objekts: $, [, [[, attribute() @, slot(), accessor-Funktionen Methode definieren <generic>.<class> schreiben setMethod() generic deklarieren Funktion die UseMethod() aufruft definieren setGeneric() Vererbung: Klassen vektorielles class-Attribut contains in setClass() Vererbung: Methoden NextMethod() callNextMethod() 17
© Copyright 2025 ExpyDoc