¿Cómo probar si existe un elemento de lista?

113

Problema

Me gustaría probar si existe un elemento de una lista, aquí hay un ejemplo

foo <- list(a=1)
exists('foo') 
TRUE   #foo does exist
exists('foo$a') 
FALSE  #suggests that foo$a does not exist
foo$a
[1] 1  #but it does exist

En este ejemplo, sé que foo$aexiste, pero la prueba regresa FALSE.

Busqué ?existsy encontré que with(foo, exists('a')regresa TRUE, pero no entiendo por qué exists('foo$a')regresa FALSE.

Preguntas

  • ¿Por qué exists('foo$a')vuelve FALSE?
  • ¿Se utiliza with(...)el enfoque preferido?
David LeBauer
fuente
1
tal vez !is.null(foo$a)(o !is.null(foo[["a"]])para estar seguro)? (o exists("a",where=foo))
Ben Bolker
1
@BenBolker gracias - sería una buena respuesta; ¿Por qué se prefiere la última opción?
David LeBauer
3
@David coincidencia parcial ... intente lo anterior confoo <- list(a1=1)
baptiste

Respuestas:

151

En realidad, esto es un poco más complicado de lo que parece. Dado que una lista en realidad (con algo de esfuerzo) puede contener elementos NULL, puede que no sea suficiente para verificar is.null(foo$a). Una prueba más estricta podría ser verificar que el nombre esté realmente definido en la lista:

foo <- list(a=42, b=NULL)
foo

is.null(foo[["a"]]) # FALSE
is.null(foo[["b"]]) # TRUE, but the element "exists"...
is.null(foo[["c"]]) # TRUE

"a" %in% names(foo) # TRUE
"b" %in% names(foo) # TRUE
"c" %in% names(foo) # FALSE

... y foo[["a"]]es más seguro que foo$a, ya que este último usa una coincidencia parcial y, por lo tanto, también puede coincidir con un nombre más largo:

x <- list(abc=4)
x$a  # 4, since it partially matches abc
x[["a"]] # NULL, no match

[ACTUALIZAR] Entonces, volvamos a la pregunta de por qué exists('foo$a')no funciona. La existsfunción solo comprueba si existe una variable en un entorno, no si existen partes de un objeto. La cadena "foo$a"se interpreta literariamente: ¿Existe una variable llamada "foo $ a"? ... y la respuesta es FALSE...

foo <- list(a=42, b=NULL) # variable "foo" with element "a"
"bar$a" <- 42   # A variable actually called "bar$a"...
ls() # will include "foo" and "bar$a" 
exists("foo$a") # FALSE 
exists("bar$a") # TRUE
Tommy
fuente
2
todavía no está claro, ¿hay alguna razón para ello exists('foo$a') == FALSE?
David LeBauer
¡Esto sugiere que en general no existe una buena solución para esto en R! Uno podría querer cosas más complejas (como probar si $mylist[[12]]$out$mcerrorestá definido) que actualmente serían muy complicadas.
TMS
¿Estaba al tanto del whereargumento que se existsseñaló en la respuesta de @ Jim ?
David LeBauer
"bar$a" <- 42Realmente desearía que esto fuera una sintaxis inválida y existiera ("foo $ a") funcionara en el sentido ingenuo.
Andy V
44

La mejor manera de verificar los elementos con nombre es usar exist(), sin embargo, las respuestas anteriores no usan la función correctamente. Necesita usar el whereargumento para verificar la variable dentro de la lista.

foo <- list(a=42, b=NULL)

exists('a', where=foo) #TRUE
exists('b', where=foo) #TRUE
exists('c', where=foo) #FALSE
Jim
fuente
8
Usar exists()en una lista funciona, pero creo que R lo coacciona internamente a un entorno antes de buscar un objeto con ese nombre, lo cual es ineficiente y puede resultar en errores si hay elementos sin nombre. Por ejemplo, si se ejecuta exists('a', list(a=1, 2)), se le dará un error: Error in list2env(list(a = 1, 2), NULL, <environment>) : attempt to use zero-length variable name. La conversión ocurre aquí: github.com/wch/r-source/blob/…
wch
5

Aquí hay una comparación de rendimiento de los métodos propuestos en otras respuestas.

> foo <- sapply(letters, function(x){runif(5)}, simplify = FALSE)
> microbenchmark::microbenchmark('k' %in% names(foo), 
                                 is.null(foo[['k']]), 
                                 exists('k', where = foo))
Unit: nanoseconds
                     expr  min   lq    mean median   uq   max neval cld
      "k" %in% names(foo)  467  933 1064.31    934  934 10730   100  a 
      is.null(foo[["k"]])    0    0  168.50      1  467  3266   100  a 
 exists("k", where = foo) 6532 6998 7940.78   7232 7465 56917   100   b

Si planea usar la lista como un diccionario rápido al que se accede muchas veces, entonces el is.nullenfoque podría ser la única opción viable. Supongo que es O (1), mientras que el %in%enfoque es O (n).

Davor Josipovic
fuente
4

Se puede utilizar una versión ligeramente modificada de @ salient.salamander, si se quiere comprobar la ruta completa.

Element_Exists_Check = function( full_index_path ){
  tryCatch({
    len_element = length(full_index_path)
    exists_indicator = ifelse(len_element > 0, T, F)
      return(exists_indicator)
  }, error = function(e) {
    return(F)
  })
}
Soumya Boral
fuente
3

Una solución que aún no ha aparecido es el uso de length, que maneja correctamente NULL. Por lo que puedo decir, todos los valores excepto NULL tienen una longitud mayor que 0.

x <- list(4, -1, NULL, NA, Inf, -Inf, NaN, T, x = 0, y = "", z = c(1,2,3))
lapply(x, function(el) print(length(el)))
[1] 1
[1] 1
[1] 0
[1] 1
[1] 1
[1] 1
[1] 1
[1] 1
[1] 1
[1] 1
[1] 3

Por lo tanto, podríamos hacer una función simple que funcione con índices numerados y con nombre:

element.exists <- function(var, element)
{
  tryCatch({
    if(length(var[[element]]) > -1)
      return(T)
  }, error = function(e) {
    return(F)
  })
}

Si el elemento no existe, provoca una condición fuera de límites capturada por el bloque tryCatch.

salient.salamander
fuente
3

rlang::has_name() también puede hacer esto:

foo = list(a = 1, bb = NULL)
rlang::has_name(foo, "a")  # TRUE
rlang::has_name(foo, "b")  # FALSE. No partial matching
rlang::has_name(foo, "bb")  # TRUE. Handles NULL correctly
rlang::has_name(foo, "c")  # FALSE

Como puede ver, inherentemente maneja todos los casos que @Tommy mostró cómo manejar usando la base R y funciona para listas con elementos sin nombre. Todavía recomendaría exists("bb", where = foo)como se propone en otra respuesta para mejorar la legibilidad, pero has_namees una alternativa si tiene elementos sin nombre.

Jonas Lindeløv
fuente
0

Úselo purrr::has_elementpara verificar el valor de un elemento de la lista:

> x <- list(c(1, 2), c(3, 4))
> purrr::has_element(x, c(3, 4))
[1] TRUE
> purrr::has_element(x, c(3, 5))
[1] FALSE
Dmitry Zotikov
fuente
¿Funciona si el elemento está anidado / en cualquier nivel de anidación?
Revisé
@DavidLeBauer, no. En ese caso, usaría rapply(algo así como any(rapply(x, function(v) identical(v, c(3, 4)), how = 'unlist')))
Dmitry Zotikov