perldsc - Perl Data Structures Cookbook (estruturas de dados complexas)
A mais importante funcionalidade que infelizmente faltava em Perl, até a versão 5.0, era a capacidade de lidar com estrutura de dados complexas. Mesmo sem suporte direto na linguagem, alguns bravos programadores conseguiram emular seu funcionamento, mas era um trabalho árduo e não era para os de coração fraco.
Você poderia ocasionalmente se virar com a notação $m{$AoA,$b}, emprestada de awk, e, que as chaves eram a string concatenada "$AoA$b", mas transversar e ordenar era difícil.
$m{$AoA,$b}
"$AoA$b"
Programadores mais desesperados chegaram a hackear a tabela de símbolos de Perl diretamente, uma estratégia que se mostrava difícil de desenvolver e manter -- para dizer o mínimo.
A versão 5.0 de Perl nos permite ter estruturas de dados complexas. Você agora pode escrever alguma coisa como isso, e, do nada, ter um array de três dimensões!
for $x (1 .. 10) { for $y (1 .. 10) { for $z (1 .. 10) { $AoA[$x][$y][$z] = $x ** $y + $z; } } }
[N.T.: O nome da variável AoA vem do inglês "Array of Arrays", e foi mantido conforme o original. O mesmo acontecerá com variáveis chamadas AoH ("Array of Hashes"), HoA ("Hash of Arrays") e HoH ("Hash of Hashes")]
Por mais simples que isso possa parecer, existem detalhes escondidos muito mais elaborados do que vêem os olhos!
Como você imprime isso? Por que você não pode simplesmente dizer print @AoA? Como você ordena isso? Como você pode passar isso para uma função, ou obter um desses de uma função? Isso é um objeto? Você pode salvá-lo no disco para obtê-lo de volta mais tarde? Como você acessa as linhas e colunas dessa matriz? Todos os valores precisam ser numéricos?
print @AoA
Como você pode ver, é fácil ficar confuso. Embora uma pequena parte da dificuldade pudesse ser atribuída à implementação baseada em referências, era muito mais devido a ausência de documentação com exemplos escritos para o iniciante.
Este documento tem como objetivo ser um tratamento detalhado porém compreensível de muitos tipos de estruturas de dados que você poderá precisar. Ele também serve como um "livro de receitas", ou de exemplos. Dessa forma, quando você precisar criar uma dessas estruturas complexas, você pode simplesmente selecionar um dos exemplos disponíveis aqui.
Vamos ver cada uma dessas possíveis estruturas em detalhe. Temos seções separadas para cada um desses itens:
arrays de arrays
hashes de arrays
arrays de hashes
hashes de hashes
estruturas mais elaboradas
Mas, por enquanto, vamos dar uma olhada em questões mais gerais, comuns a todos estes tipos de estruturas de dados.
A coisa mais importante para compreender todas as estruturas de dados em Perl -- incluindo as arrays multidimensionais -- é que, embora possa não parecer, os @ARRAYs e %HASHes em Perl são sempre unidimensionais; eles só podem guardar valores escalares (strings, números ou referências). Eles não podem conter outras arrays ou hashes, mas, ao invés disso, podem conter referências para outros arrays e hashes.
@ARRAY
%HASH
Você não pode usar uma referência para um array ou um hash exatamente da mesma maneira que você usaria um array ou hash diretamente. Para programadores de C ou C++ isso pode parecer confuso. Se for o caso, pense que existe uma diferença entre a estrutura e o ponteiro para a estrutura. (N.T.: É como aquele ditado zen que diz: "O dedo aponta para a lua, mas ai daquele que confundir o dedo com a lua.")
Você pode (e deve) ler mais sobre referências em perlref. Basicamente, referências são como ponteiros, pois elas sabem para quem estão apontando. (Objetos também são um tipo de referência, mas nós não iremos precisar deles por enquanto.)
Isso significa que quando você tem algo que parece conter uma estrutura de duas ou mais dimensões, o que você tem, na verdade, é simplesmente uma estrutura unidimensional contendo referências para o próximo nível. Na prática, é como se você pudesse usá-la como se fosse multidimensional. (É assim que a maioria das arrays multidimensionais em C também funcionam.)
$array[7][12] # array of arrays $array[7]{string} # array of hashes $hash{string}[7] # hash of arrays $hash{string}{'another string'} # hash of hashes
Agora, como o nível superior contém apenas referências, se você tentar imprimir sua array com uma função print(), você vai obter algo não muito bonito, como:
@AoA = ( [2, 3], [4, 5, 7], [0] ); print $AoA[1][2]; 7 print @AoA; ARRAY(0x83c38)ARRAY(0x8b194)ARRAY(0x8b1d0)
Isso acontece porque Perl nunca dereferencia implicitamente suas variáveis. Se você quer a coisa para a qual a referência está apontando, então você precisa indicar isso explicitamente, com "prefix typing indicators" como ${$blah}, @{$blah}, @{$blah[$i]}, ou "postfix pointer arrows", como $a->[3], $h->{fred}, ou mesmo $ob->method()->[3].
${$blah}
@{$blah}
@{$blah[$i]}
$a->[3]
$h->{fred}
$ob->method()->[3]
Os dois erros mais comuns ao construir algo como uma array de arrays são a) acidentalmente contar o número de elementos ou b) obter uma referência para a mesmo alocação de memória repetidamente.
Aqui está o exemplo em que você obtêm uma contagem ao invés de uma lista aninhada:
for $i (1..10) { @array = somefunc($i); $AoA[$i] = @array; # ERRADO! }
Isto é simplesmente o mesmo que que atribuir um array a um escalar e obter sua contagem de elementos. Se fosse isso que você quisesse fazer, você poderia muito bem ser um pouquinho mais explícito, assim:
for $i (1..10) { @array = somefunc($i); $counts[$i] = scalar @array; }
Aqui está o outro exemplo, de obter uma referência para a mesma alocação de memória, de novo e de novo:
for $i (1..10) { @array = somefunc($i); $AoA[$i] = \@array; # ERRADO! }
Mas, qual é o problema com isso? Parece certo, não parece? Afinal, eu acabei de dizer que você precisaria de um array de referências, então, por deus, você me deu uma!
Infelizmente, embora isso seja verdade, ainda não funciona. Todas as referências em @AoA se referem a exatamente o mesmo lugar, e por isso irá conter o que quer que fique em @array no final. É similar ao problema demonstrado nesse programa em C:
#include <pwd.h> main() { struct passwd *getpwnam(), *rp, *dp; rp = getpwnam("root"); dp = getpwnam("daemon"); printf("daemon name is %s\nroot name is %s\n", dp->pw_name, rp->pw_name); }
Que irá imprimir
daemon name is daemon root name is daemon
O problema é que ambos rp e dp são ponteiros para a mesma locação da memória! Em C, você teria que lembrar de malloc() alguma memória nova. Em Perl, você usa o construtor de array [] ou o construtor de hash {}.
rp
dp
[]
{}
Esta é a maneira correta de se fazer o código anterior funcionar:
for $i (1..10) { @array = somefunc($i); $AoA[$i] = [ @array ]; }
Os colchetes fazem uma referência para uma nova array com uma cópia do que está em @array no momento da atribuição. É isso o que você quer.
[N.T.: O trecho a seguir está confuso.]
Note que isto irá produzir um resultado similar, embora seja bem mais difícil de ler:
for $i (1..10) { @array = 0 .. $i; @{$AoA[$i]} = @array; }
É a mesma coisa? Talvez sim, talvez não. A diferença sutil é que quando você atribui alguma coisa em colchetes, você tem certeza de que é uma nova referência com uma cópia dos dados. Algo completamente diferente pode acontecer com a dereferência do lado esquerdo da atribuição (@{$AoA[$i]}}). Tudo dependeria se $AoA[$i] estivesse indefinido, para começar, ou se já contivesse uma referência. Se @AoA já estivesse populada, como em
@{$AoA[$i]}}
$AoA[$i]
$AoA[3] = \@another_array;
Nesse caso a atribuição com a dereferência do lado esquerdo usaria a referência que já estava lá:
@{$AoA[3]} = @array;
É claro que isso teria o efeito "interessante" de sobrescrever ("clobbering") @another_array. (Você já reparou que quando um programador diz que alguma coisa é "interessante", ao invés de significar "intrigante", é muito mais provável que signifique "perturbador", "difícil", ou os dois? :-))
Então, lembre-se apenas de sempre usar os construtores de array e de hash com [] ou {}, e você estará bem, mesmo que não seja optimamente eficiente.
Surpreendentemente, a seguinte construção de aparência perigosa vai funcionar muito bem:
for $i (1..10) { my @array = somefunc($i); $AoA[$i] = \@array; }
Isso acontece por que my() é mais uma declaração de tempo de execução do que de tempo de compilação por si. Isso significa que a variável em my() é renovada constantemente, a cada volta do loop. Assim, embora parecesse que você estava guardando a mesma variável cada vez, na verdade não era isso que você estava fazendo! Esta distinção sutil pode produzir código mais eficiente, sob o risco de enganar todos exceto os programadores mais experientes. Por isso, eu normalmente recomendo não ensinar isso para iniciantes. De fato, a não ser quando vou passar argumentos para uma função, eu nunca gosto de ver o operador "me-dá-uma-referência" (a contrabarra) por todo o código. Ao invés disso, eu recomendo aos iniciantes que eles (e a maioria de nós) tentem usar os construtores muito mais legíveis [] e {}, ao invés de esperar que o escopo léxico (ou dinâmico) e a contagem de referências façam a coisa certa por trás das cenas.
Em resumo:
$AoA[$i] = [ @array ]; # usualmente melhor $AoA[$i] = \@array; # perilous; exatamente como my() estava naquele array? @{ $AoA[$i] } = @array; # modo bastante complicado para muitos programadores
Por falar em coisas como @{$AoA[$i]}, os dois exemplos a seguir são, na verdade, a mesma coisa:
@{$AoA[$i]}
$aref->[2][2] # claro $$aref[2][2] # confuso
Isso acontece pois as regras de precedência do Perl em relação aos seus cinco dereferenciadores (que parecem xingamento: $ @ * % &) fazem com que eles sejam avaliados antes dos índices entre colchetes ou chaves! Isso certamente virá como um grande choque para programadores C ou C++, familiarizados com *a[i] significando aquilo que é apontado pelo i-ésimo elemento de a. Isto é, eles primeiro pegam o índice e só então dereferenciam a coisa naquela posição. É assim em C, mas não estamos falando de C.
$ @ * % &
*a[i]
a
A construção aparentemente equivalente em Perl, $$aref[$i] primeiro dereferencia $aref, fazendo com que use $aref como referência para um array, e então dereferencia isso, e finalmente te diz o i-ésimo valor do array apontado pelo $AoA. Se você quisesse fazer como em C, teria que escrever ${$AoA[$i]} para forçar $AoA[$i] a ser avaliado antes do dereferenciador $ do início.
$$aref[$i]
${$AoA[$i]}
$
use strict
Se tudo isso está começando a parecer mais assustador do que vale a pena, relaxe. Perl tem algumas características para ajudá-lo a evitar os erros mais comuns. A melhor maneira de evitar confusão é iniciar todo programa assim:
#!/usr/bin/perl -w use strict;
Desse modo, você será forçado a declarar todas as suas variáveis com my() e também impedirá a "dereferência simbólica" acidental. Portanto, se você tivesse feito isso:
my $aref = [ [ "fred", "barney", "pebbles", "bambam", "dino", ], [ "homer", "bart", "marge", "maggie", ], [ "george", "jane", "elroy", "judy", ], ]; print $aref[2][2];
O compilador avisaria o erro imediatamente, em tempo de compilação, porque você estaria acessando acidentalmente @aref, uma variável não declarada, e você seria então lembrado para escrever da seguinte forma:
@aref
print $aref->[2][2]
Antes da versão 5.002, o debugger padrão de Perl não fazia um bom trabalho ao imprimir estruturas de dados complexas. Com 5.002 e acima, o debugger inclui várias funcionalidades novas, incluindo edição de linha de comando bem como o comando x para imprimir estruturas de dados complexas. Por exemplo, dada a atribuição para $AoA acima, aqui está a saída do debugger:
x
DB<1> x $AoA $AoA = ARRAY(0x13b5a0) 0 ARRAY(0x1f0a24) 0 'fred' 1 'barney' 2 'pebbles' 3 'bambam' 4 'dino' 1 ARRAY(0x13b558) 0 'homer' 1 'bart' 2 'marge' 3 'maggie' 2 ARRAY(0x13b540) 0 'george' 1 'jane' 2 'elroy' 3 'judy'
Apresentados com poucos comentários (estes receberão suas próprias páginas de documentação algum dia), aqui estão pequenos exemplos de código que ilustram vários tipos de estruturas de dados.
@AoA = ( [ "fred", "barney" ], [ "george", "jane", "elroy" ], [ "homer", "marge", "bart" ], );
# lendo de um arquivo while ( <> ) { push @AoA, [ split ]; } # chamando uma função for $i ( 1 .. 10 ) { $AoA[$i] = [ somefunc($i) ]; } # usando variáveis temporárias for $i ( 1 .. 10 ) { @tmp = somefunc($i); $AoA[$i] = [ @tmp ]; } # adição uma linha existente push @{ $AoA[0] }, "wilma", "betty";
# um elemento $AoA[0][0] = "Fred"; # outro elemento $AoA[1][1] =~ s/(\w)/\u$1/; # imprimir tudo com refs for $aref ( @AoA ) { print "\t [ @$aref ],\n"; } # imprimir tudo com índices for $i ( 0 .. $#AoA ) { print "\t [ @{$AoA[$i]} ],\n"; } # imprimir tudo de um em um for $i ( 0 .. $#AoA ) { for $j ( 0 .. $#{ $AoA[$i] } ) { print "elt $i $j is $AoA[$i][$j]\n"; } }
%HoA = ( flintstones => [ "fred", "barney" ], jetsons => [ "george", "jane", "elroy" ], simpsons => [ "homer", "marge", "bart" ], );
# lendo de um arquivo # flintstones: fred barney wilma dino while ( <> ) { next unless s/^(.*?):\s*//; $HoA{$1} = [ split ]; } # lendo de um arquivo; mais variáveis temporárias # flintstones: fred barney wilma dino while ( $line = <> ) { ($who, $rest) = split /:\s*/, $line, 2; @fields = split ' ', $rest; $HoA{$who} = [ @fields ]; } # chamando uma função que retorna uma lista for $group ( "simpsons", "jetsons", "flintstones" ) { $HoA{$group} = [ get_family($group) ]; } # idem, mas usando variáveis temporárias for $group ( "simpsons", "jetsons", "flintstones" ) { @members = get_family($group); $HoA{$group} = [ @members ]; } # adiciona novos membros na família push @{ $HoA{"flintstones"} }, "wilma", "betty";
# um elemento $HoA{flintstones}[0] = "Fred"; # outro elemento $HoA{simpsons}[1] =~ s/(\w)/\u$1/; # imprimir todas as coisas foreach $family ( keys %HoA ) { print "$family: @{ $HoA{$family} }\n" } # imprimir na totalidade com índices foreach $family ( keys %HoA ) { print "family: "; foreach $i ( 0 .. $#{ $HoA{$family} } ) { print " $i = $HoA{$family}[$i]"; } print "\n"; } # imprimir na totalidade ordenado por número dos membros foreach $family ( sort { @{$HoA{$b}} <=> @{$HoA{$a}} } keys %HoA ) { print "$family: @{ $HoA{$family} }\n" } # imprimir na totalidade ordenado pelo número e nome dos membros foreach $family ( sort { @{$HoA{$b}} <=> @{$HoA{$a}} || $a cmp $b } keys %HoA ) { print "$family: ", join(", ", sort @{ $HoA{$family} }), "\n"; }
@AoH = ( { Lead => "fred", Friend => "barney", }, { Lead => "george", Wife => "jane", Son => "elroy", }, { Lead => "homer", Wife => "marge", Son => "bart", } );
# lendo de um arquivo # formato: LEAD=fred FRIEND=barney while ( <> ) { $rec = {}; for $field ( split ) { ($key, $value) = split /=/, $field; $rec->{$key} = $value; } push @AoH, $rec; } # lendo de um arquivo # formato: LEAD=fred FRIEND=barney # sem temp while ( <> ) { push @AoH, { split /[\s+=]/ }; } # chamando uma função que retorna uma lista de pares chave/valor como # "lead","fred","daughter","pebbles" while ( %fields = getnextpairset() ) { push @AoH, { %fields }; } # idem, mas sem usar variáveis temporárias while (<>) { push @AoH, { parsepairs($_) }; } # adiciona chave/valor a um elemento $AoH[0]{pet} = "dino"; $AoH[2]{pet} = "santa's little helper";
# um elemento $AoH[0]{lead} = "fred"; # outro elemento $AoH[1]{lead} =~ s/(\w)/\u$1/; # imprime tudo com refs for $href ( @AoH ) { print "{ "; for $role ( keys %$href ) { print "$role=$href->{$role} "; } print "}\n"; } # imprime tudo com índices for $i ( 0 .. $#AoH ) { print "$i is { "; for $role ( keys %{ $AoH[$i] } ) { print "$role=$AoH[$i]{$role} "; } print "}\n"; } # imprime tudo, um por vez for $i ( 0 .. $#AoH ) { for $role ( keys %{ $AoH[$i] } ) { print "elt $i $role is $AoH[$i]{$role}\n"; } }
%HoH = ( flintstones => { lead => "fred", pal => "barney", }, jetsons => { lead => "george", wife => "jane", "his boy" => "elroy", }, simpsons => { lead => "homer", wife => "marge", kid => "bart", }, );
# lendo de um arquivo # flintstones: lead=fred pal=barney wife=wilma pet=dino while ( <> ) { next unless s/^(.*?):\s*//; $who = $1; for $field ( split ) { ($key, $value) = split /=/, $field; $HoH{$who}{$key} = $value; } # lendo de um arquivo; mais temps while ( <> ) { next unless s/^(.*?):\s*//; $who = $1; $rec = {}; $HoH{$who} = $rec; for $field ( split ) { ($key, $value) = split /=/, $field; $rec->{$key} = $value; } } # chamando uma função que retorna um hash chave,valor for $group ( "simpsons", "jetsons", "flintstones" ) { $HoH{$group} = { get_family($group) }; } # idem, mas usando variáveis temporárias for $group ( "simpsons", "jetsons", "flintstones" ) { %members = get_family($group); $HoH{$group} = { %members }; } # adiciona novos membros a família existente %new_folks = ( wife => "wilma", pet => "dino", ); for $what (keys %new_folks) { $HoH{flintstones}{$what} = $new_folks{$what}; }
# um elemento $HoH{flintstones}{wife} = "wilma"; # outro elemento $HoH{simpsons}{lead} =~ s/(\w)/\u$1/; # imprime tudo foreach $family ( keys %HoH ) { print "$family: { "; for $role ( keys %{ $HoH{$family} } ) { print "$role=$HoH{$family}{$role} "; } print "}\n"; } # imprime tudo reordenado foreach $family ( sort keys %HoH ) { print "$family: { "; for $role ( sort keys %{ $HoH{$family} } ) { print "$role=$HoH{$family}{$role} "; } print "}\n"; } # imprime tudo reordenado pelo número de membros foreach $family ( sort { keys %{$HoH{$b}} <=> keys %{$HoH{$a}} } keys %HoH ) { print "$family: { "; for $role ( sort keys %{ $HoH{$family} } ) { print "$role=$HoH{$family}{$role} "; } print "}\n"; } # estabelece um critério de ordenação (rank) para cada parte $i = 0; for ( qw(lead wife son daughter pal pet) ) { $rank{$_} = ++$i } # agora imprime tudo ordenado pelo números dos membros foreach $family ( sort { keys %{ $HoH{$b} } <=> keys %{ $HoH{$a} } } keys %HoH ) { print "$family: { "; # e imprime estes de acordo com a ordem do rank for $role ( sort { $rank{$a} <=> $rank{$b} } keys %{ $HoH{$family} } ) { print "$role=$HoH{$family}{$role} "; } print "}\n"; }
Aqui está um exemplo mostrando como criar e usar um registro ("record") cujos campos são de muitos tipos diferentes:
$rec = { TEXT => $string, SEQUENCE => [ @old_values ], LOOKUP => { %some_table }, THATCODE => \&some_function, THISCODE => sub { $_[0] ** $_[1] }, HANDLE => \*STDOUT, }; print $rec->{TEXT}; print $rec->{SEQUENCE}[0]; $last = pop @ { $rec->{SEQUENCE} }; print $rec->{LOOKUP}{"key"}; ($first_k, $first_v) = each %{ $rec->{LOOKUP} }; $answer = $rec->{THATCODE}->($arg); $answer = $rec->{THISCODE}->($arg1, $arg2); # cuidado de bloco extra de colchetes na referência a filehandle print { $rec->{HANDLE} } "a string\n"; use FileHandle; $rec->{HANDLE}->autoflush(1); $rec->{HANDLE}->print(" a string\n");
%TV = ( flintstones => { series => "flintstones", nights => [ qw(monday thursday friday) ], members => [ { name => "fred", role => "lead", age => 36, }, { name => "wilma", role => "wife", age => 31, }, { name => "pebbles", role => "kid", age => 4, }, ], }, jetsons => { series => "jetsons", nights => [ qw(wednesday saturday) ], members => [ { name => "george", role => "lead", age => 41, }, { name => "jane", role => "wife", age => 39, }, { name => "elroy", role => "kid", age => 9, }, ], }, simpsons => { series => "simpsons", nights => [ qw(monday) ], members => [ { name => "homer", role => "lead", age => 34, }, { name => "marge", role => "wife", age => 37, }, { name => "bart", role => "kid", age => 11, }, ], }, );
# Lendo de um arquivo # this is most easily done by having the file itself be # in the raw data format as shown above. perl is happy # to parse complex data structures if declared as data, so # sometimes it's easiest to do that # aqui está acumulado fragmento por fragmento $rec = {}; $rec->{series} = "flintstones"; $rec->{nights} = [ find_days() ]; @members = (); # assume este arquivo na sintaxe field=value while (<>) { %fields = split /[\s=]+/; push @members, { %fields }; } $rec->{members} = [ @members ]; # agora guarde de tudo $TV{ $rec->{series} } = $rec; ########################################################### # Agora, você provavelmente quer fazer campos extras interessantes que # incluem ponteiros de volta na mesma estrutura de dados, então se # mudar uma peça, muda em todo local, como por exemplo # se você quis um campo {kids} que fosse uma referência # para um array de registros kids sem ter que duplicar # registros e sem ter problemas de atualização ########################################################### foreach $family (keys %TV) { $rec = $TV{$family}; # ponteiro temporário @kids = (); for $person ( @{ $rec->{members} } ) { if ($person->{role} =~ /kid|son|daughter/) { push @kids, $person; } } # LEMBRE-SE: $rec e $TV{$family} apontam para os mesmos dados!! $rec->{kids} = [ @kids ]; } # Você copiou o array, mas o array em si contém ponteiros # para objetos não copiados. Isso significa que se você fazer Bart envelhecer via: $TV{simpsons}{kids}[0]{age}++; # Então isso também mudaria em print $TV{simpsons}{members}[2]{age}; # pois $TV{simpsons}{kids}[0] e $TV{simpsons}{members}[2] # ambos apontam para a mesma tabela hash anônima # Imprime tudo foreach $family ( keys %TV ) { print "the $family"; print " is on during @{ $TV{$family}{nights} }\n"; print "its members are:\n"; for $who ( @{ $TV{$family}{members} } ) { print " $who->{name} ($who->{role}), age $who->{age}\n"; } print "it turns out that $TV{$family}{lead} has "; print scalar ( @{ $TV{$family}{kids} } ), " kids named "; print join (", ", map { $_->{name} } @{ $TV{$family}{kids} } ); print "\n"; }
Você não pode facilmente amarrar uma estrutura de dados multi-nível (como hashes de hashes) para um arquivo dbm. O primeiro problema é que todos menos GBDM e o Berkeley DB tem limitações de tamanho, mas além disso, você também tem problemas em como referências vão se representadas no disco. Um módulo experimental que faz uma tentativa parcial de resolver esse necessidade é o MLDBM. Cheque o site CPAN mais próximo como descrito em perlmodlib para o código fonte do MLDBM.
perlref, perllol, perldata, perlobj
Tom Christiansen <tchrist@perl.com>
Nicholas Amorim <nicholasamorim@gmail.com>
Nelson Ferraz <nferraz@gmail.com>
Joênio Costa
Breno G. de Oliveira <garu@cpan.org>
Ronaldo Lima
Marco Lima
To install POD2::PT_BR, copy and paste the appropriate command in to your terminal.
cpanm
cpanm POD2::PT_BR
CPAN shell
perl -MCPAN -e shell install POD2::PT_BR
For more information on module installation, please visit the detailed CPAN module installation guide.