¿Cuál es la forma más segura de iterar a través de las claves de un hash de Perl?

107

Si tengo un hash de Perl con un montón de pares (clave, valor), ¿cuál es el método preferido para recorrer todas las claves? He oído que el uso eachpuede de alguna manera tener efectos secundarios no deseados. Entonces, ¿es eso cierto? ¿Es mejor uno de los dos métodos siguientes, o hay una mejor manera?

# Method 1
while (my ($key, $value) = each(%hash)) {
    # Something
}

# Method 2
foreach my $key (keys(%hash)) {
    # Something
}
Rudd Zwolinski
fuente

Respuestas:

199

La regla general es utilizar la función que mejor se adapte a sus necesidades.

Si solo quiere las claves y no planea leer ninguno de los valores, use keys ():

foreach my $key (keys %hash) { ... }

Si solo desea los valores, use values ​​():

foreach my $val (values %hash) { ... }

Si necesita las claves y los valores, use cada ():

keys %hash; # reset the internal iterator so a prior each() doesn't affect the loop
while(my($k, $v) = each %hash) { ... }

Si planea cambiar las claves del hash de alguna manera, excepto para eliminar la clave actual durante la iteración, entonces no debe usar cada (). Por ejemplo, este código para crear un nuevo conjunto de teclas en mayúsculas con valores duplicados funciona bien usando keys ():

%h = (a => 1, b => 2);

foreach my $k (keys %h)
{
  $h{uc $k} = $h{$k} * 2;
}

produciendo el hash resultante esperado:

(a => 1, A => 2, b => 2, B => 4)

Pero usando cada () para hacer lo mismo:

%h = (a => 1, b => 2);

keys %h;
while(my($k, $v) = each %h)
{
  $h{uc $k} = $h{$k} * 2; # BAD IDEA!
}

produce resultados incorrectos de formas difíciles de predecir. Por ejemplo:

(a => 1, A => 2, b => 2, B => 8)

Sin embargo, esto es seguro:

keys %h;
while(my($k, $v) = each %h)
{
  if(...)
  {
    delete $h{$k}; # This is safe
  }
}

Todo esto se describe en la documentación de perl:

% perldoc -f keys
% perldoc -f each
Juan Siracusa
fuente
6
Agregue una clave de contexto vacío% h; antes de cada bucle para mostrar de forma segura utilizando el iterador.
ysth
5
Hay otra salvedad con cada uno. El iterador está vinculado al hash, no al contexto, lo que significa que no es reentrante. Por ejemplo, si recorre un hash e imprime el hash, perl reiniciará internamente el iterador, haciendo que este código se repita sin fin: my% hash = (a => 1, b => 2, c => 3,); while (mi ($ k, $ v) = cada% hash) {imprimir% hash; } Lea más en blogs.perl.org/users/rurban/2014/04/do-not-use-each.html
Rawler
28

Una cosa que debe tener en cuenta al usar eaches que tiene el efecto secundario de agregar "estado" a su hash (el hash tiene que recordar cuál es la clave "siguiente"). Cuando se usa código como los fragmentos publicados anteriormente, que recorren todo el hash de una sola vez, esto no suele ser un problema. Sin embargo, se encontrará con problemas difíciles de rastrear (hablo por experiencia;), cuando use eachjunto con declaraciones como lasto returnpara salir del while ... eachciclo antes de haber procesado todas las claves.

En este caso, el hash recordará qué claves ya ha devuelto, y cuando lo use eachla próxima vez (tal vez en un fragmento de código totalmente no relacionado), continuará en esta posición.

Ejemplo:

my %hash = ( foo => 1, bar => 2, baz => 3, quux => 4 );

# find key 'baz'
while ( my ($k, $v) = each %hash ) {
    print "found key $k\n";
    last if $k eq 'baz'; # found it!
}

# later ...

print "the hash contains:\n";

# iterate over all keys:
while ( my ($k, $v) = each %hash ) {
    print "$k => $v\n";
}

Esto imprime:

found key bar
found key baz
the hash contains:
quux => 4
foo => 1

¿Qué pasó con las teclas "bar" y baz "? Todavía están allí, pero la segunda eachcomienza donde se quedó la primera y se detiene cuando llega al final del hash, por lo que nunca las vemos en el segundo ciclo.

8jean
fuente
22

El lugar donde eachpuede causarle problemas es que es un iterador verdadero, sin alcance. A modo de ejemplo:

while ( my ($key,$val) = each %a_hash ) {
    print "$key => $val\n";
    last if $val; #exits loop when $val is true
}

# but "each" hasn't reset!!
while ( my ($key,$val) = each %a_hash ) {
    # continues where the last loop left off
    print "$key => $val\n";
}

Si necesita asegurarse de que eachobtenga todas las claves y valores, debe asegurarse de usar keyso valuesprimero (ya que eso restablece el iterador). Consulte la documentación de cada uno .

Darren Meyer
fuente
14

El uso de cada sintaxis evitará que se genere todo el conjunto de claves a la vez. Esto puede ser importante si está utilizando un hash vinculado a una base de datos con millones de filas. No desea generar la lista completa de claves de una sola vez y agotar su memoria física. En este caso, cada uno sirve como iterador, mientras que las claves realmente generan la matriz completa antes de que comience el ciclo.

Por tanto, el único lugar en el que "cada uno" es de uso real es cuando el hash es muy grande (en comparación con la memoria disponible). Es probable que eso solo suceda cuando el hash en sí no vive en la memoria, a menos que esté programando un dispositivo de recolección de datos portátil o algo con poca memoria.

Si la memoria no es un problema, normalmente el paradigma del mapa o de las claves es el paradigma más preventivo y más fácil de leer.


fuente
6

Algunas reflexiones diversas sobre este tema:

  1. No hay nada inseguro en ninguno de los iteradores hash. Lo que no es seguro es modificar las claves de un hash mientras lo repites. (Es perfectamente seguro modificar los valores). El único efecto secundario potencial que puedo pensar es que valuesdevuelve alias, lo que significa que modificarlos modificará el contenido del hash. Esto es por diseño, pero puede que no sea lo que desea en algunas circunstancias.
  2. La respuesta aceptada de John es buena con una excepción: la documentación es clara que no es seguro agregar claves mientras se itera sobre un hash. Puede funcionar para algunos conjuntos de datos, pero fallará en otros, según el orden de hash.
  3. Como ya se señaló, es seguro eliminar la última clave devuelta por each. Esto no es cierto keysya que eaches un iterador mientras keysdevuelve una lista.
Michael Carman
fuente
2
Re "no es cierto para las claves", más bien: no es aplicable a las claves y cualquier eliminación es segura. La redacción que usa implica que nunca es seguro eliminar nada al usar claves.
ysth
2
Re: "nada inseguro sobre ninguno de los iteradores hash", el otro peligro es asumir que el iterador está al principio antes de comenzar cada ciclo, como otros mencionan.
ysth
3

También utilizo siempre el método 2. El único beneficio de usar cada uno es que si solo está leyendo (en lugar de reasignar) el valor de la entrada de hash, no está desreferenciando constantemente el hash.

Jaredg
fuente
3

Puede que me muerda, pero creo que es una preferencia personal. No puedo encontrar ninguna referencia en los documentos a que cada () sea diferente de las claves () o los valores () (aparte de la respuesta obvia de "devuelven cosas diferentes". De hecho, los documentos indican que usan el mismo iterador y todos devuelve valores de lista reales en lugar de copias de ellos, y que modificar el hash mientras se itera sobre él usando cualquier llamada es malo.

Dicho todo esto, casi siempre uso keys () porque para mí es más autodocumentado acceder al valor de la clave a través del propio hash. De vez en cuando utilizo values ​​() cuando el valor es una referencia a una estructura grande y la clave del hash ya estaba almacenada en la estructura, momento en el que la clave es redundante y no la necesito. Creo que he usado cada () 2 veces en 10 años de programación Perl y probablemente fue la elección incorrecta en ambas ocasiones =)

jj33
fuente
2

Usualmente uso keysy no puedo pensar en la última vez que usé o leí un uso de each.

¡No te olvides de map, dependiendo de lo que estés haciendo en el bucle!

map { print "$_ => $hash{$_}\n" } keys %hash;
Gary Richardson
fuente
6
no use el mapa a menos que desee el valor de retorno
ko-dos
-1

Yo diría:

  1. Use lo que sea más fácil de leer / comprender para la mayoría de las personas (por lo que, por lo general, diría que las claves)
  2. Utilice lo que decida de forma coherente en todo el código base.

Esto le da 2 ventajas principales:

  1. Es más fácil detectar el código "común" para que pueda volver a factorizarlo en funciones / métodos.
  2. Es más fácil de mantener para los futuros desarrolladores.

No creo que sea más costoso usar claves sobre cada una, por lo que no es necesario dos construcciones diferentes para lo mismo en su código.

Hogsmill
fuente
1
Con el keysuso de memoria aumenta en hash-size * avg-key-size. Dado que el tamaño de la clave solo está limitado por la memoria (ya que son solo elementos de matriz como "sus" valores correspondientes bajo el capó), en algunas situaciones puede ser prohibitivamente más costoso tanto en el uso de memoria como en el tiempo necesario para hacer la copia.
Adrian Günter