2010-03-22 23 views
7

Asumir una estructura resumen anidada %old_hash ..¿Cómo puedo convertir limpiamente un hash Perl anidado en uno no anidado?

my %old_hash; 
$old_hash{"foo"}{"bar"}{"zonk"} = "hello"; 

.. lo que queremos "aplanar" (lo siento si eso es la terminología equivocada!) A un hash no anidada utilizando el sub &flatten(...) para que ..

my %h = &flatten(\%old_hash); 
die unless($h{"zonk"} eq "hello"); 

la siguiente definición de &flatten(...) hace el truco:

sub flatten { 
    my $hashref = shift; 
    my %hash; 
    my %i = %{$hashref}; 
    foreach my $ii (keys(%i)) { 
    my %j = %{$i{$ii}}; 
    foreach my $jj (keys(%j)) { 
     my %k = %{$j{$jj}}; 
     foreach my $kk (keys(%k)) { 
     my $value = $k{$kk}; 
     $hash{$kk} = $value; 
     } 
    } 
    } 
    return %hash; 
} 

Mientras que el código g solo funciona, no es muy legible o limpio.

Mi pregunta es doble:

  • ¿De qué manera el código dado que no se correspondan a las mejores prácticas modernas de Perl? Se duro! :-)
  • ¿Cómo lo limpiarías?
+2

Por favor, dé un ejemplo de lo que quiere decir con 'aplanar'. No entiendo qué intenta hacer tu código. –

+0

Sinan: Gracias por su comentario. He agregado un ejemplo de uso al principio de la pregunta. Espero que aclaren las cosas! – knorv

+0

Tu ejemplo ayuda a un ** pequeño **: ¿Qué sucede si el hash es '(a => {b => 1}, c => {b => 2})'? –

Respuesta

10

Su método no es una buena práctica porque no tiene escala. ¿Qué pasa si el hash anidado es seis, diez niveles de profundidad? La repetición debería indicarle que una rutina recursiva es probablemente lo que necesita.

sub flatten { 
    my ($in, $out) = @_; 
    for my $key (keys %$in) { 
     my $value = $in->{$key}; 
     if (defined $value && ref $value eq 'HASH') { 
      flatten($value, $out); 
     } 
     else { 
      $out->{$key} = $value; 
     } 
    } 
} 

Alternativamente, el buen estilo moderno de Perl es usar CPAN siempre que sea posible. Data::Traverse haría lo que necesita:

use Data::Traverse; 
sub flatten { 
    my %hash = @_; 
    my %flattened; 
    traverse { $flattened{$a} = $b } \%hash; 
    return %flattened; 
} 

Como nota final, por lo general es más eficiente que pasar por referencia hashes para evitar que se expanden hacia fuera en las listas y luego se convirtieron en valores hash de nuevo.

+1

Soy un gran admirador de Data :: Traverse. – friedo

+0

Ninguna sorpresa aquí, usted es su autor principal;) – willert

+0

-1 su primer bloque de código contiene una cantidad de errores tipográficos, '$ values' nunca se declara y ha mezclado el hash y la referencia a la sintaxis hash –

3

En primer lugar, utilizaría perl -c para asegurarme de que compila limpiamente, lo que no ocurre. Entonces, agregaría un } posterior para compilarlo.

Luego, me gustaría ejecutarlo a través de perltidy para mejorar el diseño del código (sangría, etc.).

Luego, corría perlcritic (en modo "severo") para decirme automáticamente lo que cree que son malas prácticas. Se queja de que:

subrutina no termina con el "retorno"

Actualización: el OP cambia esencialmente cada línea de código después de que publiqué mi respuesta anterior, pero creo que todavía se aplica. No es fácil disparar a un objetivo móvil :)

1

¿Es su intención terminar con una copia del hash original o simplemente un resultado reordenado?

Su código comienza con un hash (el hash original que se utiliza como referencia) y hace dos copias %i y %hash.

La declaración my %i=%{hashref} no es necesaria. Está copiando todo el hash a un nuevo hash. En cualquier caso (si desea una copia de no) puede usar referencias al hash original.

También está perdiendo datos si su hash en el hash tiene el mismo valor que el hash principal. ¿Esto es intencionado?

2

Hay algunos problemas con su enfoque que debe resolver. En primer lugar, ¿qué ocurre si hay dos nodos de hoja con la misma clave? ¿El segundo clobber es el primero, es el segundo ignorado, debería el resultado contener una lista de ellos? Aquí hay un enfoque. En primer lugar se construye una lista plana de pares de valores clave utilizando una función recursiva para hacer frente a otras profundidades de hash:

my %data = (
    foo => {bar => {baz => 'hello'}}, 
    fizz => {buzz => {bing => 'world'}}, 
    fad => {bad => {baz => 'clobber'}}, 
); 


sub flatten { 
    my $hash = shift; 
    map { 
     my $value = $$hash{$_}; 
     ref $value eq 'HASH' 
      ? flatten($value) 
      : ($_ => $value) 
    } keys %$hash 
} 

print join(", " => flatten \%data), "\n"; 
# baz, clobber, bing, world, baz, hello 

my %flat = flatten \%data; 

print join(", " => %flat), "\n"; 
# baz, hello, bing, world   # lost (baz => clobber) 

Una solución podría ser algo como esto, lo que creará un hash de árbitros matriz que contiene todos los valores:

sub merge { 
    my %out; 
    while (@_) { 
     my ($key, $value) = splice @_, 0, 2; 
     push @{ $out{$key} }, $value 
    } 
    %out 
} 

my %better_flat = merge flatten \%data; 

En el código de producción, sería más rápido pasar referencias entre las funciones, pero lo he omitido aquí para mayor claridad.