¿Cómo asignar desde una función que devuelve más de un valor?

223

Aún tratando de entrar en la lógica R ... ¿cuál es la "mejor" forma de desempaquetar (en LHS) los resultados de una función que devuelve múltiples valores?

No puedo hacer esto aparentemente:

R> functionReturningTwoValues <- function() { return(c(1, 2)) }
R> functionReturningTwoValues()
[1] 1 2
R> a, b <- functionReturningTwoValues()
Error: unexpected ',' in "a,"
R> c(a, b) <- functionReturningTwoValues()
Error in c(a, b) <- functionReturningTwoValues() : object 'a' not found

¿Realmente debo hacer lo siguiente?

R> r <- functionReturningTwoValues()
R> a <- r[1]; b <- r[2]

o el programador R escribiría algo más como esto:

R> functionReturningTwoValues <- function() {return(list(first=1, second=2))}
R> r <- functionReturningTwoValues()
R> r$first
[1] 1
R> r$second
[1] 2

--- editado para responder las preguntas de Shane ---

Realmente no necesito dar nombres a las partes del valor del resultado. Estoy aplicando una función agregada al primer componente y otra al segundo componente ( miny maxsi fuera la misma función para ambos componentes, no necesitaría dividirlos).

mariotomo
fuente
77
Para su información, otra forma de devolver múltiples valores es establecer un attrvalor de retorno.
Jonathan Chang el
Este es el equivalente del desempaque de tuplas de Python.
smci

Respuestas:

186

(1) list [...] <- Lo publiqué hace más de una década en r-help . Desde entonces se ha agregado al paquete gsubfn. No requiere un operador especial, pero sí requiere que el lado izquierdo se escriba de la list[...]siguiente manera:

library(gsubfn)  # need 0.7-0 or later
list[a, b] <- functionReturningTwoValues()

Si solo necesita el primer o segundo componente, estos también funcionan:

list[a] <- functionReturningTwoValues()
list[a, ] <- functionReturningTwoValues()
list[, b] <- functionReturningTwoValues()

(Por supuesto, si solo necesitara un valor, entonces functionReturningTwoValues()[[1]]o functionReturningTwoValues()[[2]]sería suficiente).

Vea el hilo de r-help citado para más ejemplos.

(2) con Si la intención es simplemente combinar los valores múltiples posteriormente y se nombran los valores de retorno, entonces se usa una alternativa simple with:

myfun <- function() list(a = 1, b = 2)

list[a, b] <- myfun()
a + b

# same
with(myfun(), a + b)

(3) adjuntar Otra alternativa es adjuntar:

attach(myfun())
a + b

AGREGADO: withyattach

G. Grothendieck
fuente
25
Acepté su respuesta debido al "con", pero no puedo reproducir lo que describe para el uso del lado izquierdo de "lista", todo lo que obtengo es "objeto 'a' no encontrado"
mariotomo
44
Esto funciona para mi. ¿Qué intentaste? ¿Leíste la publicación vinculada y la seguiste? ¿Definiste listy [<-.resultcomo se muestra allí?
G. Grothendieck
12
@ G.Grothendieck, ¿le importaría si pongo el contenido de su enlace en su respuesta? Creo que sería más fácil para las personas usarlo.
merlin2011
12
Estoy de acuerdo con @ merlin2011; tal como está escrito parece que esta sintaxis está incrustada en R base.
knowah
66
@ G.Grothendieck Estoy de acuerdo con merlin2011 y knowah: sería mejor si el código real que es importante aquí (el código referenciado en el enlace) está en la respuesta. Puede que no sea una mala idea mencionar que el objeto resultante no necesita ser nombrado lista. Eso me confundió por un momento antes de leer su código real. Como se mencionó, la respuesta dice que debe ejecutar el código en el enlace, pero la mayoría de la gente no va a leer ese código de inmediato a menos que esté en la respuesta directamente; esto da la impresión de que esta sintaxis está en la base R.
Dason
68

De alguna manera me topé con este truco inteligente en Internet ... No estoy seguro de si es desagradable o hermoso, pero te permite crear un operador "mágico" que te permite descomprimir múltiples valores de retorno en su propia variable. La :=función se define aquí y se incluye a continuación para la posteridad:

':=' <- function(lhs, rhs) {
  frame <- parent.frame()
  lhs <- as.list(substitute(lhs))
  if (length(lhs) > 1)
    lhs <- lhs[-1]
  if (length(lhs) == 1) {
    do.call(`=`, list(lhs[[1]], rhs), envir=frame)
    return(invisible(NULL)) 
  }
  if (is.function(rhs) || is(rhs, 'formula'))
    rhs <- list(rhs)
  if (length(lhs) > length(rhs))
    rhs <- c(rhs, rep(list(NULL), length(lhs) - length(rhs)))
  for (i in 1:length(lhs))
    do.call(`=`, list(lhs[[i]], rhs[[i]]), envir=frame)
  return(invisible(NULL)) 
}

Con eso en la mano, puedes hacer lo que buscas:

functionReturningTwoValues <- function() {
  return(list(1, matrix(0, 2, 2)))
}
c(a, b) := functionReturningTwoValues()
a
#[1] 1
b
#     [,1] [,2]
# [1,]    0    0
# [2,]    0    0

No sé cómo me siento al respecto. Quizás le resulte útil en su espacio de trabajo interactivo. Usarlo para construir bibliotecas (reutilizables) (para consumo masivo) podría no ser la mejor idea, pero supongo que eso depende de usted.

... sabes lo que dicen sobre responsabilidad y poder ...

Steve Lianoglou
fuente
12
También lo desaconsejaría mucho más ahora que cuando publiqué originalmente esta respuesta, ya que el paquete data.table usa :=mucho al operador de una manera mucho más práctica :-)
Steve Lianoglou
47

Por lo general, envuelvo el resultado en una lista, que es muy flexible (puede tener cualquier combinación de números, cadenas, vectores, matrices, matrices, listas, objetos en el resultado)

asi como:

func2<-function(input) {
   a<-input+1
   b<-input+2
   output<-list(a,b)
   return(output)
}

output<-func2(5)

for (i in output) {
   print(i)
}

[1] 6
[1] 7
Federico Giorgi
fuente
¿Qué sucede si en lugar de la salida <-func2 (5) quiero tener el resultado en dos objetos? He intentado con la lista ("a", "b") <-func2 (5) pero no funciona.
skan
13
functionReturningTwoValues <- function() { 
  results <- list()
  results$first <- 1
  results$second <-2
  return(results) 
}
a <- functionReturningTwoValues()

Creo que esto funciona.

Jojo
fuente
11

Creé un paquete R zeallot para abordar este problema. zeallot incluye una asignación múltiple o operador de asignación de desembalaje, %<-%. El LHS del operador es cualquier número de variables para asignar, construido usando llamadas ac() . El RHS del operador es un vector, una lista, un marco de datos, un objeto de fecha o cualquier objeto personalizado con un destructuremétodo implementado (ver ?zeallot::destructure).

Aquí hay algunos ejemplos basados ​​en la publicación original,

library(zeallot)

functionReturningTwoValues <- function() { 
  return(c(1, 2)) 
}

c(a, b) %<-% functionReturningTwoValues()
a  # 1
b  # 2

functionReturningListOfValues <- function() {
  return(list(1, 2, 3))
}

c(d, e, f) %<-% functionReturningListOfValues()
d  # 1
e  # 2
f  # 3

functionReturningNestedList <- function() {
  return(list(1, list(2, 3)))
}

c(f, c(g, h)) %<-% functionReturningNestedList()
f  # 1
g  # 2
h  # 3

functionReturningTooManyValues <- function() {
  return(as.list(1:20))
}

c(i, j, ...rest) %<-% functionReturningTooManyValues()
i     # 1
j     # 2
rest  # list(3, 4, 5, ..)

Consulte la viñeta del paquete para obtener más información y ejemplos.

nteetor
fuente
¿Existe una sintaxis especial para almacenar varias parcelas como salidas utilizando este método?
mrpargeter
2
No se requiere una sintaxis especial, puede asignar una lista de objetos de trazado como lo haría con una lista de números.
nteetor
10

No hay una respuesta correcta a esta pregunta. Realmente depende de lo que estés haciendo con los datos. En el simple ejemplo anterior, sugeriría encarecidamente:

  1. Mantenga las cosas lo más simple posible.
  2. Siempre que sea posible, es una buena práctica mantener sus funciones vectorizadas. Eso proporciona la mayor cantidad de flexibilidad y velocidad a largo plazo.

¿Es importante que los valores 1 y 2 anteriores tengan nombres? En otras palabras, ¿por qué es importante en este ejemplo que 1 y 2 se denominen a y b, en lugar de solo r [1] y r [2]? Una cosa importante para entender en este contexto es que a y b también son vectores de longitud 1. Por lo tanto, en realidad no está cambiando nada en el proceso de hacer esa asignación, aparte de tener 2 vectores nuevos que no necesitan subíndices para ser referenciado:

> r <- c(1,2)
> a <- r[1]
> b <- r[2]
> class(r)
[1] "numeric"
> class(a)
[1] "numeric"
> a
[1] 1
> a[1]
[1] 1

También puede asignar los nombres al vector original si prefiere hacer referencia a la letra que al índice:

> names(r) <- c("a","b")
> names(r)
[1] "a" "b"
> r["a"]
a 
1 

[Editar] Dado que aplicará min y max a cada vector por separado, sugeriría usar una matriz (si a y b tendrán la misma longitud y el mismo tipo de datos) o un marco de datos (si a y b serán la misma longitud pero pueden ser diferentes tipos de datos) o use una lista como en su último ejemplo (si pueden ser de diferentes longitudes y tipos de datos).

> r <- data.frame(a=1:4, b=5:8)
> r
  a b
1 1 5
2 2 6
3 3 7
4 4 8
> min(r$a)
[1] 1
> max(r$b)
[1] 8
Shane
fuente
editó la pregunta para incluir sus comentarios. Gracias. dar nombres a cosas como r[1]puede ayudar a aclarar las cosas (está bien, no si los nombres como aaparecen en su lugar).
mariotomo 01 de
5

Las listas parecen perfectas para este propósito. Por ejemplo, dentro de la función que tendría

x = desired_return_value_1 # (vector, matrix, etc)

y = desired_return_value_2 # (vector, matrix, etc)

returnlist = list(x,y...)

}  # end of function

programa principal

x = returnlist[[1]]

y = returnlist[[2]]
Arnold
fuente
44
¿Cómo puede asignar ambas variables en un solo comando, como list ("x", "y") <-returnlist ()? Lo digo porque si tiene muchos elementos en la lista, necesitaría ejecutar la función completa varias veces y eso cuesta un tiempo.
skan
4

Sí a su segunda y tercera pregunta: eso es lo que debe hacer, ya que no puede tener múltiples "valores" a la izquierda de una tarea.

Dirk Eddelbuettel
fuente
3

¿Qué hay de usar asignar?

functionReturningTwoValues <- function(a, b) {
  assign(a, 1, pos=1)
  assign(b, 2, pos=1)
}

Puede pasar los nombres de la variable que desea pasar por referencia.

> functionReturningTwoValues('a', 'b')
> a
[1] 1
> b
[1] 2

Si necesita acceder a los valores existentes, el inverso de assignes get.

Steve Pitchers
fuente
... pero esto requiere que sepas los nombres de las variables receptoras en ese entorno
smci
@smci Sí. Es por eso que el método de "lista nombrada" en la pregunta es generalmente mejor: r <- function() { return(list(first=1, second=2)) }y haga referencia a los resultados usando r$firsty r$second.
Steve Pitchers
2
Una vez que tenga su función, ¿cómo puede asignar ambas variables en un solo comando, como list ("x", "y") <- functionReturningTwoValues ​​('a', 'b')? Digo eso porque si tiene muchos elementos en la lista, necesitaría ejecutar la función completa varias veces y eso cuesta un tiempo
skan
3

Si desea devolver la salida de su función al entorno global, puede usar list2env, como en este ejemplo:

myfun <- function(x) { a <- 1:x
                       b <- 5:x
                       df <- data.frame(a=a, b=b)

                       newList <- list("my_obj1" = a, "my_obj2" = b, "myDF"=df)
                       list2env(newList ,.GlobalEnv)
                       }
    myfun(3)

Esta función creará tres objetos en su entorno global:

> my_obj1
  [1] 1 2 3

> my_obj2
  [1] 5 4 3

> myDF
    a b
  1 1 5
  2 2 4
  3 3 3
rafa.pereira
fuente
1

[A] Si cada uno de foo y bar es un número único, entonces no hay nada de malo en c (foo, bar); y también puede nombrar los componentes: c (Foo = foo, Bar = bar). Para que pueda acceder a los componentes del resultado 'res' como res [1], res [2]; o, en el caso mencionado, como res ["Foo"], res ["BAR"].

[B] Si foo y bar son vectores del mismo tipo y longitud, entonces nuevamente no hay nada de malo en devolver cbind (foo, bar) o rbind (foo, bar); igualmente nombrable. En el caso 'cbind', accedería a foo y bar como res [, 1], res [, 2] o como res [, "Foo"], res [, "Bar"]. También puede preferir devolver un marco de datos en lugar de una matriz:

data.frame(Foo=foo,Bar=bar)

y accede a ellos como res $ Foo, res $ Bar. Esto también funcionaría bien si foo y bar fueran de la misma longitud pero no del mismo tipo (por ejemplo, foo es un vector de números, bar un vector de cadenas de caracteres).

[C] Si foo y bar son lo suficientemente diferentes como para no combinarlos convenientemente como anteriormente, entonces definitivamente deberías devolver una lista.

Por ejemplo, su función podría ajustarse a un modelo lineal y también calcular valores pronosticados, por lo que podría tener

LM<-lm(....) ; foo<-summary(LM); bar<-LM$fit

y luego accederías return list(Foo=foo,Bar=bar)al resumen como res $ Foo, los valores pronosticados como res $ Bar

fuente: http://r.789695.n4.nabble.com/How-to-return-multiple-values-in-a-function-td858528.html

Prakhar Agrawal
fuente
-1

Para obtener múltiples salidas de una función y mantenerlas en el formato deseado, puede guardar las salidas en su disco duro (en el directorio de trabajo) desde dentro de la función y luego cargarlas desde fuera de la función:

myfun <- function(x) {
                      df1 <- ...
                      df2 <- ...
                      save(df1, file = "myfile1")
                      save(df2, file = "myfile2")
}
load("myfile1")
load("myfile2")
Andrew Eaves
fuente
-1

Con R 3.6.1, puedo hacer lo siguiente

fr2v <- function() { c(5,3) }
a_b <- fr2v()
(a_b[[1]]) # prints "5"
(a_b[[2]]) # prints "3"
radumanolescu
fuente