Wie verwende ich die Ellipsenfunktion von R, wenn ich meine eigene Funktion schreibe?

185

Die R-Sprache verfügt über eine nützliche Funktion zum Definieren von Funktionen, die eine variable Anzahl von Argumenten annehmen können. Beispielsweise akzeptiert die Funktion data.frameeine beliebige Anzahl von Argumenten, und jedes Argument wird zu den Daten für eine Spalte in der resultierenden Datentabelle. Anwendungsbeispiel:

> data.frame(letters=c("a", "b", "c"), numbers=c(1,2,3), notes=c("do", "re", "mi"))
  letters numbers notes
1       a       1    do
2       b       2    re
3       c       3    mi

Die Signatur der Funktion enthält folgende Auslassungspunkte:

function (..., row.names = NULL, check.rows = FALSE, check.names = TRUE, 
    stringsAsFactors = default.stringsAsFactors()) 
{
    [FUNCTION DEFINITION HERE]
}

Ich möchte eine Funktion schreiben, die etwas Ähnliches tut, mehrere Werte nimmt und sie zu einem einzigen Rückgabewert zusammenfasst (sowie eine andere Verarbeitung durchführt). Dazu muss ich herausfinden, wie ...die Argumente der Funktion innerhalb der Funktion "entpackt" werden. Ich weiß nicht, wie ich das machen soll. Die relevante Zeile in der Funktionsdefinition von data.frameist object <- as.list(substitute(list(...)))[-1L], was ich nicht verstehen kann.

Wie kann ich also die Auslassungspunkte aus der Signatur der Funktion in beispielsweise eine Liste konvertieren?

Um genauer zu sein, wie kann ich get_list_from_ellipsisin den folgenden Code schreiben ?

my_ellipsis_function(...) {
    input_list <- get_list_from_ellipsis(...)
    output_list <- lapply(X=input_list, FUN=do_something_interesting)
    return(output_list)
}

my_ellipsis_function(a=1:10,b=11:20,c=21:30)

Bearbeiten

Es scheint zwei Möglichkeiten zu geben, dies zu tun. Sie sind as.list(substitute(list(...)))[-1L]und list(...). Diese beiden machen jedoch nicht genau dasselbe. (Unterschiede finden Sie in den Beispielen in den Antworten.) Kann mir jemand sagen, was der praktische Unterschied zwischen ihnen ist und welchen ich verwenden sollte?

Ryan C. Thompson
quelle

Antworten:

113

Ich lese Antworten und Kommentare und sehe, dass einige Dinge nicht erwähnt wurden:

  1. data.frameverwendet die list(...)Version. Fragment des Codes:

    object <- as.list(substitute(list(...)))[-1L]
    mrn <- is.null(row.names)
    x <- list(...)

    objectwird verwendet, um etwas mit Spaltennamen zu zaubern, wird aber xverwendet, um final zu erstellen data.frame.
    Informationen zur Verwendung nicht bewerteter ...Argumente finden Sie in dem write.csvCode, in dem sie match.callverwendet werden.

  2. Während Sie in Kommentar schreiben, ist Dirk Antwort keine Liste von Listen. Ist eine Liste der Länge 4, welche Elemente vom languageTyp sind. Das erste Objekt ist ein symbol- list, das zweite ist Ausdruck 1:10und so weiter. Das erklärt, warum dies [-1L]erforderlich ist: Es entfernt erwartete erwartete symbolArgumente in ...(weil es immer eine Liste ist).
    Wie Dirk substitutefeststellt, wird "Analysebaum der nicht bewertete Ausdruck" zurückgegeben.
    Wenn Sie rufen my_ellipsis_function(a=1:10,b=11:20,c=21:30)dann ...„erzeugt“ eine Liste von Argumenten: list(a=1:10,b=11:20,c=21:30)und substitutees eine Liste der vier Elemente machen:

    List of 4
    $  : symbol list
    $ a: language 1:10
    $ b: language 11:20
    $ c: language 21:30

    Das erste Element hat keinen Namen und dies ist [[1]]in Dirk Antwort. Ich erreiche diese Ergebnisse mit:

    my_ellipsis_function <- function(...) {
      input_list <- as.list(substitute(list(...)))
      str(input_list)
      NULL
    }
    my_ellipsis_function(a=1:10,b=11:20,c=21:30)
  3. Wie oben können wir strüberprüfen, welche Objekte sich in einer Funktion befinden.

    my_ellipsis_function <- function(...) {
        input_list <- list(...)
        output_list <- lapply(X=input_list, function(x) {str(x);summary(x)})
        return(output_list)
    }
    my_ellipsis_function(a=1:10,b=11:20,c=21:30)
     int [1:10] 1 2 3 4 5 6 7 8 9 10
     int [1:10] 11 12 13 14 15 16 17 18 19 20
     int [1:10] 21 22 23 24 25 26 27 28 29 30
    $a
       Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
       1.00    3.25    5.50    5.50    7.75   10.00 
    $b
       Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
       11.0    13.2    15.5    15.5    17.8    20.0 
    $c
       Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
       21.0    23.2    25.5    25.5    27.8    30.0 

    Es ist in Ordnung. Mal sehen substituteVersion:

       my_ellipsis_function <- function(...) {
           input_list <- as.list(substitute(list(...)))
           output_list <- lapply(X=input_list, function(x) {str(x);summary(x)})
           return(output_list)
       }
       my_ellipsis_function(a=1:10,b=11:20,c=21:30)
        symbol list
        language 1:10
        language 11:20
        language 21:30
       [[1]]
       Length  Class   Mode 
            1   name   name 
       $a
       Length  Class   Mode 
            3   call   call 
       $b
       Length  Class   Mode 
            3   call   call 
       $c
       Length  Class   Mode 
            3   call   call 

    Ist nicht das, was wir brauchten. Sie benötigen zusätzliche Tricks, um mit solchen Objekten umzugehen (wie in write.csv).

Wenn Sie verwenden möchten ..., sollten Sie es wie in Shane Antwort von verwenden list(...).

Marek
quelle
38

Sie können die Auslassungspunkte in eine Liste mit konvertieren list()und dann Ihre Operationen daran ausführen:

> test.func <- function(...) { lapply(list(...), class) }
> test.func(a="b", b=1)
$a
[1] "character"

$b
[1] "numeric"

Ihre get_list_from_ellipsisFunktion ist also nichts weiter als list.

Ein gültiger Anwendungsfall hierfür ist in Fällen, in denen Sie eine unbekannte Anzahl von Objekten für den Betrieb übergeben möchten (wie in Ihrem Beispiel von c()oder data.frame()). Es ist jedoch keine gute Idee, die zu verwenden, ...wenn Sie jeden Parameter im Voraus kennen, da dies der Argumentzeichenfolge einige Mehrdeutigkeiten und weitere Komplikationen hinzufügt (und die Funktionssignatur für jeden anderen Benutzer unklar macht). Die Argumentliste ist eine wichtige Dokumentation für Funktionsbenutzer.

Andernfalls ist es auch nützlich, wenn Sie Parameter an eine Unterfunktion übergeben möchten, ohne sie alle in Ihren eigenen Funktionsargumenten verfügbar zu machen. Dies ist in der Funktionsdokumentation vermerkt.

Shane
quelle
Ich weiß, wie man die Ellipse als Durchgang für Argumente zu Unterfunktionen verwendet, aber es ist auch unter R-Primitiven üblich, die Ellipse so zu verwenden, wie ich es beschrieben habe. Tatsächlich funktionieren sowohl die listals auch die cFunktionen auf diese Weise, aber beide sind Grundelemente, sodass ich ihren Quellcode nicht einfach überprüfen kann, um zu verstehen, wie sie funktionieren.
Ryan C. Thompson
rbind.data.framebenutze diesen Weg.
Marek
4
Wenn dies list(...)ausreicht, warum verwenden R-Builds stattdessen data.framedie längere Form as.list(substitute(list(...)))[-1L]?
Ryan C. Thompson
1
Da ich nicht selbst erstellt hat data.frame, weiß ich nicht , die Antwort auf diese Frage (was gesagt, ich bin sicher , dass es ist ein guter Grund dafür). Ich verwende list()zu diesem Zweck in meinen eigenen Paketen und habe noch kein Problem damit.
Shane
33

Nur um die Antworten von Shane und Dirk zu ergänzen: Es ist interessant zu vergleichen

get_list_from_ellipsis1 <- function(...)
{
  list(...)
}
get_list_from_ellipsis1(a = 1:10, b = 2:20) # returns a list of integer vectors

$a
 [1]  1  2  3  4  5  6  7  8  9 10

$b
 [1]  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20

mit

get_list_from_ellipsis2 <- function(...)
{
  as.list(substitute(list(...)))[-1L]
}
get_list_from_ellipsis2(a = 1:10, b = 2:20) # returns a list of calls

$a
1:10

$b
2:20

So wie es aussieht, scheint jede Version für Ihre Zwecke geeignet zu sein my_ellipsis_function, obwohl die erste deutlich einfacher ist.

Richie Cotton
quelle
15

Du hast schon die halbe Antwort gegeben. Erwägen

R> my_ellipsis_function <- function(...) {
+   input_list <- as.list(substitute(list(...)))
+ }
R> print(my_ellipsis_function(a=1:10, b=2:20))
[[1]]
list

$a
1:10

$b
11:20

R> 

Dies nahm also zwei Argumente aund baus dem Aufruf und konvertierte es in eine Liste. War es nicht das, wonach du gefragt hast?

Dirk Eddelbuettel
quelle
2
Nicht ganz das, was ich will. Das scheint tatsächlich eine Liste von Listen zurückzugeben. Beachten Sie die [[1]]. Außerdem würde ich gerne wissen, wie die magische Beschwörung as.list(substitute(list(...)))funktioniert.
Ryan C. Thompson
2
Das Innere list(...)erstellt ein listObjekt basierend auf den Argumenten. Dann substitute()erstellt der Parse - Baum für den unevaluierten Ausdruck; Siehe die Hilfe zu dieser Funktion. Sowie ein guter fortgeschrittener Text zu R (oder S). Das ist kein triviales Zeug.
Dirk Eddelbuettel
Ok, was ist mit dem [[-1L]]Teil (aus meiner Frage)? Sollte es nicht sein [[1]]?
Ryan C. Thompson
3
Sie müssen sich über die Indizierung informieren. Das Minus bedeutet "ausschließen", dh es print(c(1:3)[-1])werden nur 2 und 3 gedruckt. Das List eine neumodische Art und Weise es als eine ganze Zahl endet , um sicherzustellen, das viel in den R - Quellen durchgeführt wird.
Dirk Eddelbuettel
7
Ich muss mich nicht über die Indizierung informieren, aber ich muss der Ausgabe der von Ihnen angezeigten Befehle mehr Aufmerksamkeit schenken. Der Unterschied zwischen dem [[1]]und dem $aIndex ließ mich denken, dass verschachtelte Listen beteiligt waren. Aber jetzt sehe ich, dass Sie tatsächlich die Liste erhalten, die ich möchte, aber mit einem zusätzlichen Element vorne. Dann [-1L]macht das also Sinn. Woher kommt dieses zusätzliche erste Element überhaupt? Und gibt es einen Grund, warum ich dies anstelle von einfach verwenden sollte list(...)?
Ryan C. Thompson
6

Dies funktioniert wie erwartet. Das Folgende ist eine interaktive Sitzung:

> talk <- function(func, msg, ...){
+     func(msg, ...);
+ }
> talk(cat, c("this", "is", "a","message."), sep=":")
this:is:a:message.
> 

Gleich, außer mit einem Standardargument:

> talk <- function(func, msg=c("Hello","World!"), ...){
+     func(msg, ...);
+ }
> talk(cat,sep=":")
Hello:World!
> talk(cat,sep=",", fill=1)
Hello,
World!
>

Wie Sie sehen, können Sie damit 'zusätzliche' Argumente an eine Funktion in Ihrer Funktion übergeben, wenn die Standardeinstellungen in einem bestimmten Fall nicht Ihren Wünschen entsprechen.

Overloaded_Operator
quelle