domingo, 9 de maio de 2021

Compreensões de listas, dicionários e sets em python

❝Nossa imaginação é esticada ao máximo, não, como na ficção, para imaginar coisas que não existem realmente, mas apenas para compreender as coisas que existem.❞
- Richard Feynman

Mergulho

Cada linguagem de programação tem aquele único recurso, algo complicado intencionalmente simplificado. Se você está vindo de outra linguagem, pode facilmente perdê-lo, porque sua antiga linguagem não tornava isso simples (porque estava ocupado fazendo outra coisa simples). Este capítulo irá ensiná-lo sobre compreensões de lista, compreensões de dicionário e compreensões de conjunto: três conceitos relacionados centrados em torno de uma técnica muito poderosa. Mas, primeiro, quero fazer um pequeno desvio em dois módulos que o ajudarão a navegar em seu sistema de arquivos local.

Trabalho com arquivos e diretórios

Python 3 vem com um módulo chamado os, que significa “sistema operacional”. O módulo os contém uma infinidade de funções para obter informações - e, em alguns casos, manipular - diretórios locais, arquivos, processos e variáveis de ambiente. Python faz o seu melhor para oferecer uma API unificada em todos os sistemas operacionais suportados para que seus programas possam ser executados em qualquer computador com o mínimo possível de código específico de plataforma.

O diretório de trabalho atual

Quando você está começando a usar o Python, vai passar muito tempo no Python Shell. Ao longo deste livro, você verá exemplos semelhantes a este:

  1. Importe um dos módulos da pasta examples
  2. Chame uma função nesse módulo
  3. Explique o resultado

Sempre há um diretório de trabalho atual.

Se você não souber sobre o diretório de trabalho atual, a etapa 1 provavelmente falhará com um ImportError. Por quê? Porque o Python procurará o módulo de exemplo no caminho de pesquisa de importação, mas não o encontrará porque a pasta examples não é um dos diretórios no caminho de pesquisa. Para superar isso, você pode fazer uma das duas coisas:

  1. Adicione a pasta examples ao caminho de pesquisa de importação.
  2. Mude o diretório de trabalho atual para a pasta examples.

O diretório de trabalho atual é uma propriedade invisível que o Python mantém na memória o tempo todo. Sempre há um diretório de trabalho atual, esteja você no Python Shell, executando seu próprio script Python na linha de comando ou executando um script Python CGI em um servidor da web em algum lugar.

O módulo os contém duas funções para lidar com o diretório de trabalho atual.

>>> import os                                            ①
>>> print(os.getcwd())                                   ②
C:\Python31
>>> os.chdir('/Users/pilgrim/diveintopython3/examples')  ③
>>> print(os.getcwd())                                   ④
C:\Users\pilgrim\diveintopython3\examples
  1. O módulo os vem com Python; você pode importá-lo a qualquer hora, em qualquer lugar.
  2. Use a função os.getcwd() para obter o diretório de trabalho atual. Quando você executa o Python Shell gráfico, o diretório de trabalho atual começa como o diretório onde está o executável do Python Shell. No Windows, isso depende de onde você instalou o Python; o diretório padrão é c:\Python31. Se você executar o Python Shell a partir da linha de comando, o diretório de trabalho atual começa como o diretório em que você estava quando executou python3.
  3. Use a função os.chdir() para alterar o diretório de trabalho atual.
  4. Quando chamei a função os.chdir(), usei um nome de caminho no estilo Linux (barras, sem letra de unidade), embora esteja no Windows. Este é um dos lugares onde o Python tenta esconder as diferenças entre os sistemas operacionais.

Trabalho com nomes de arquivos e diretórios

Já que estamos falando de diretórios, quero destacar o módulo os.path. os.path contém funções para manipular nomes de arquivos e nomes de diretórios.

>>> import os
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples/', 'humansize.py'))              ①
/Users/pilgrim/diveintopython3/examples/humansize.py
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples', 'humansize.py'))               ②
/Users/pilgrim/diveintopython3/examples\humansize.py
>>> print(os.path.expanduser('~'))                                                               ③
c:\Users\pilgrim
>>> print(os.path.join(os.path.expanduser('~'), 'diveintopython3', 'examples', 'humansize.py'))  ④
c:\Users\pilgrim\diveintopython3\examples\humansize.py
  1. A função os.path.join() constrói um nome de caminho a partir de um ou mais nomes de caminho parciais. Nesse caso, ele simplesmente concatena strings.
  2. Neste caso um pouco menos trivial, chamar a função os.path.join() adicionará uma barra extra ao nome do caminho antes de uni-lo ao nome do arquivo. É uma barra invertida em vez de uma barra normal, porque construí este exemplo no Windows. Se você replicar este exemplo no Linux ou Mac OS X, verá uma barra. Não mexa com barras; sempre use os.path.join() e deixe o Python fazer a coisa certa.
  3. A função os.path.expanduser() irá expandir um nome de caminho que usa ~ para representar o diretório pessoal do usuário atual. Isso funciona em qualquer plataforma em que os usuários tenham um diretório inicial, incluindo Linux, Mac OS X e Windows. O caminho retornado não tem uma barra final, mas a função os.path.join() não se importa.
  4. Combinando essas técnicas, você pode construir facilmente nomes de caminho para diretórios e arquivos no diretório pessoal do usuário. A função os.path.join() pode receber qualquer número de argumentos. Fiquei muito feliz quando descobri isso, já que addSlashIfNecessary() é uma das pequenas funções estúpidas que sempre preciso escrever ao construir minha caixa de ferramentas em uma nova linguagem. Não escreva esta pequena função estúpida em Python; pessoas inteligentes já cuidaram disso para você.

os.path também contém funções para dividir nomes de caminhos completos, nomes de diretórios e nomes de arquivos em suas partes constituintes.

>>> pathname = '/Users/pilgrim/diveintopython3/examples/humansize.py'
>>> os.path.split(pathname)                                        ①
('/Users/pilgrim/diveintopython3/examples', 'humansize.py')
>>> (dirname, filename) = os.path.split(pathname)                  ②
>>> dirname                                                        ③
'/Users/pilgrim/diveintopython3/examples'
>>> filename                                                       ④
'humansize.py'
>>> (shortname, extension) = os.path.splitext(filename)            ⑤
>>> shortname
'humansize'
>>> extension
'.py'
  1. A função split divide um caminho completo e retorna uma tupla contendo o caminho e o nome do arquivo.
  2. Lembra quando eu disse que você poderia usar a atribuição de várias variáveis para retornar vários valores de uma função? A função os.path.split() faz exatamente isso. Você atribui o valor de retorno da função split em uma tupla de duas variáveis. Cada variável recebe o valor do elemento correspondente da tupla retornada.
  3. A primeira variável, dirname, recebe o valor do primeiro elemento da tupla retornada da função os.path.split(), o caminho do arquivo.
  4. A segunda variável, nome do arquivo, recebe o valor do segundo elemento da tupla retornado da função os.path.split(), o nome do arquivo.
  5. os.path também contém a função os.path.splitext(), que divide o nome do arquivo e retorna uma tupla contendo o nome do arquivo e a extensão do arquivo. Você usa a mesma técnica para atribuir cada um deles a variáveis ​​separadas.

Listagem de diretórios

O módulo glob é outra ferramenta da biblioteca padrão do Python. É uma maneira fácil de obter o conteúdo de um diretório programaticamente e usa o tipo de curinga com o qual você pode já estar familiarizado por trabalhar na linha de comando.

O módulo glob usa curingas semelhantes a shell.

>>> os.chdir('/Users/pilgrim/diveintopython3/')
>>> import glob
>>> glob.glob('examples/*.xml')                  ①
['examples\\feed-broken.xml',
 'examples\\feed-ns0.xml',
 'examples\\feed.xml']
>>> os.chdir('examples/')                        ②
>>> glob.glob('*test*.py')                       ③
['alphameticstest.py',
 'pluraltest1.py',
 'pluraltest2.py',
 'pluraltest3.py',
 'pluraltest4.py',
 'pluraltest5.py',
 'pluraltest6.py',
 'romantest1.py',
 'romantest10.py',
 'romantest2.py',
 'romantest3.py',
 'romantest4.py',
 'romantest5.py',
 'romantest6.py',
 'romantest7.py',
 'romantest8.py',
 'romantest9.py']
  1. O módulo glob recebe um curinga e retorna o caminho de todos os arquivos e diretórios que correspondem ao curinga. Neste exemplo, o curinga é um caminho de diretório mais “*.xml”, que corresponderá a todos os arquivos .xml no subdiretório examples.
  2. Agora mude o diretório de trabalho atual para o subdiretório examples. A função os.chdir() pode ter nomes de caminhos relativos.
  3. Você pode incluir vários curingas em seu padrão glob. Este exemplo encontra todos os arquivos no diretório de trabalho atual que terminam em uma extensão .py e contêm a palavra test em qualquer lugar em seu nome de arquivo.

Obtendo metadados de arquivo

Cada sistema de arquivos moderno armazena metadados sobre cada arquivo: data de criação, data da última modificação, tamanho do arquivo e assim por diante. Python fornece uma única API para acessar esses metadados. Você não precisa abrir o arquivo; tudo que você precisa é o nome do arquivo.

>>> import os
>>> print(os.getcwd())                 ①
c:\Users\pilgrim\diveintopython3\examples
>>> metadata = os.stat('feed.xml')     ②
>>> metadata.st_mtime                  ③
1247520344.9537716
>>> import time                        ④
>>> time.localtime(metadata.st_mtime)  ⑤
time.struct_time(tm_year=2009, tm_mon=7, tm_mday=13, tm_hour=17,
  tm_min=25, tm_sec=44, tm_wday=0, tm_yday=194, tm_isdst=1)
  1. O diretório de trabalho atual é a pasta examples.
  2. feed.xml é um arquivo na pasta examples. Chamar a função os.stat() retorna um objeto que contém vários tipos diferentes de metadados sobre o arquivo.
  3. st_mtime é a hora da modificação, mas está em um formato que não é muito útil. (Tecnicamente, é o número de segundos desde a Época, que é definido como o primeiro segundo de 1º de janeiro de 1970. Sério).
  4. O módulo time faz parte da biblioteca padrão do Python. Ele contém funções para converter entre diferentes representações de tempo, formatar valores de tempo em strings e mexer com fusos horários.
  5. A função time.localtime() converte um valor de tempo de segundos desde a época (da propriedade st_mtime retornada da função os.stat()) em uma estrutura mais útil de ano, mês, dia, hora, minuto, segundo e assim por diante. Este arquivo foi modificado pela última vez em 13 de julho de 2009, por volta das 17:25.
# continued from the previous example
>>> metadata.st_size                              ①
3070
>>> import humansize
>>> humansize.approximate_size(metadata.st_size)  ②
'3.0 KiB'
  1. A função os.stat() também retorna o tamanho de um arquivo, na propriedade st_size. O arquivo feed.xml tem 3070 bytes.
  2. Você pode passar a propriedade st_size para a função approximate_size().

Construindo Nomes de Caminho Absolutos

Na seção anterior, a função glob.glob() retornou uma lista de nomes de caminhos relativos. O primeiro exemplo tinha nomes de caminho como 'examples\feed.xml', e o segundo exemplo tinha nomes de caminho relativos ainda mais curtos como 'romantest1.py'. Contanto que você permaneça no mesmo diretório de trabalho atual, esses nomes de caminho relativos funcionarão para abrir arquivos ou obter metadados de arquivos. Mas se você quiser construir um caminho de acesso absoluto - ou seja, um que inclua todos os nomes de diretório de volta ao diretório raiz ou letra de unidade - então você precisará da função os.path.realpath().

>>> import os
>>> print(os.getcwd())
c:\Users\pilgrim\diveintopython3\examples
>>> print(os.path.realpath('feed.xml'))
c:\Users\pilgrim\diveintopython3\examples\feed.xml

Compreensões de lista

Você pode usar qualquer expressão Python em uma compreensão de lista.

Uma compreensão de lista fornece uma maneira compacta de mapear uma lista em outra lista, aplicando uma função a cada um dos elementos da lista.

>>> a_list = [1, 9, 8, 4]
>>> [elem * 2 for elem in a_list]           ①
[2, 18, 16, 8]
>>> a_list                                  ②
[1, 9, 8, 4]
>>> a_list = [elem * 2 for elem in a_list]  ③
>>> a_list
[2, 18, 16, 8]
  1. Para entender isso, olhe da direita para a esquerda. a_list é a lista que você está mapeando. O python intérpreta percorre a_list um elemento de cada vez, atribuir temporariamente o valor de cada elemento para a variável elem. O Python então aplica a operação elem * 2 e anexa esse resultado à lista retornada.
  2. Uma compreensão de lista cria uma nova lista; não altera a lista original.
  3. É seguro atribuir o resultado de uma compreensão de lista à variável que você está mapeando. O Python constrói a nova lista na memória e, quando a compreensão da lista é concluída, ele atribui o resultado à variável original.

Você pode usar qualquer expressão Python em uma compreensão de lista, incluindo as funções no módulo os para manipular arquivos e diretórios.

>>> import os, glob
>>> glob.glob('*.xml')                                 ①
['feed-broken.xml', 'feed-ns0.xml', 'feed.xml']
>>> [os.path.realpath(f) for f in glob.glob('*.xml')]  ②
['c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml',
 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml',
 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml']
  1. Isso retorna uma lista de todos os arquivos .xml no diretório de trabalho atual.
  2. Essa compreensão de lista pega essa lista de arquivos .xml e a transforma em uma lista de nomes de caminho completos.

As compreensões de lista também podem filtrar itens, produzindo um resultado que pode ser menor do que a lista original.

>>> import os, glob
>>> [f for f in glob.glob('*.py') if os.stat(f).st_size > 6000]  ①
['pluraltest6.py',
 'romantest10.py',
 'romantest6.py',
 'romantest7.py',
 'romantest8.py',
 'romantest9.py']
  1. Para filtrar uma lista, você pode incluir uma cláusula if no final da compreensão da lista. A expressão após a palavra-chave if será avaliada para cada item da lista. Se a expressão for avaliada como True, o item será incluído na saída. Essa compreensão de lista examina a lista de todos os arquivos .py no diretório atual e a instrução if filtra essa lista testando se o tamanho de cada arquivo é maior do que 6000 bytes. Existem seis desses arquivos, portanto, a compreensão da lista retorna uma lista de seis nomes de arquivo.

Todos os exemplos de compreensões de lista até agora apresentam expressões simples - multiplique um número por uma constante, chame uma única função ou simplesmente retorne o item original da lista (após a filtragem). Mas não há limite para o quão complexa pode ser a compreensão de uma lista.

>>> import os, glob
>>> [(os.stat(f).st_size, os.path.realpath(f)) for f in glob.glob('*.xml')]            ①
[(3074, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml'),
 (3386, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml'),
 (3070, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml')]
>>> import humansize
>>> [(humansize.approximate_size(os.stat(f).st_size), f) for f in glob.glob('*.xml')]  ②
[('3.0 KiB', 'feed-broken.xml'),
 ('3.3 KiB', 'feed-ns0.xml'),
 ('3.0 KiB', 'feed.xml')]
  1. Esta compreensão de lista encontra todos os arquivos .xml no diretório de trabalho atual, obtém o tamanho de cada arquivo (chamando a função os.stat()) e constrói uma tupla do tamanho do arquivo e o caminho absoluto de cada arquivo (chamando a função os.path.realpath()).
  2. Essa compreensão se baseia na anterior para chamar a função approximate_size() com o tamanho do arquivo .xml de cada arquivo.

Compreensão do dicionário

Uma compreensão de dicionário é como uma compreensão de lista, mas constrói um dicionário em vez de uma lista.

>>> import os, glob
>>> metadata = [(f, os.stat(f)) for f in glob.glob('*test*.py')]    ①
>>> metadata[0]                                                     ②
('alphameticstest.py', nt.stat_result(st_mode=33206, st_ino=0, st_dev=0,
 st_nlink=0, st_uid=0, st_gid=0, st_size=2509, st_atime=1247520344,
 st_mtime=1247520344, st_ctime=1247520344))
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*test*.py')}  ③
>>> type(metadata_dict)                                             ④
<class 'dict'>
>>> list(metadata_dict.keys())                                      ⑤
['romantest8.py', 'pluraltest1.py', 'pluraltest2.py', 'pluraltest5.py',
 'pluraltest6.py', 'romantest7.py', 'romantest10.py', 'romantest4.py',
 'romantest9.py', 'pluraltest3.py', 'romantest1.py', 'romantest2.py',
 'romantest3.py', 'romantest5.py', 'romantest6.py', 'alphameticstest.py',
 'pluraltest4.py']
>>> metadata_dict['alphameticstest.py'].st_size                     ⑥
2509
  1. Esta não é uma compreensão de dicionário; é uma compreensão de lista. Ele encontra todos os arquivos .py com test em seus nomes e, em seguida, constrói uma tupla do nome do arquivo e dos metadados do arquivo (a partir da chamada da função os.stat()).
  2. Cada item da lista resultante é uma tupla.
  3. Esta é uma compreensão de dicionário. A sintaxe é semelhante à compreensão de uma lista, com duas diferenças. Primeiro, ele é colocado entre chaves em vez de colchetes. Em segundo lugar, em vez de uma única expressão para cada item, ele contém duas expressões separadas por dois pontos. A expressão antes dos dois pontos (f neste exemplo) é a chave do dicionário; a expressão após os dois pontos (os.stat(f) neste exemplo) é o valor.
  4. Uma compreensão de dicionário retorna um dicionário.
  5. As chaves deste dicionário específico são simplesmente os nomes de arquivo retornados da chamada para glob.glob('*test*.py').
  6. O valor associado a cada chave é o valor de retorno da função os.stat(). Isso significa que podemos “pesquisar” um arquivo por nome neste dicionário para obter seus metadados de arquivo. Uma das partes dos metadados é st_size o tamanho do arquivo. O arquivo alphameticstest.py tem 2509 bytes.

Como as compreensões de lista, você pode incluir uma cláusula if em uma compreensão de dicionário para filtrar a sequência de entrada com base em uma expressão que é avaliada com cada item.

>>> import os, glob, humansize
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*')}                                  ①
>>> humansize_dict = {os.path.splitext(f)[0]:humansize.approximate_size(meta.st_size) \     
...                   for f, meta in metadata_dict.items() if meta.st_size > 6000}          ②
>>> list(humansize_dict.keys())                                                             ③
['romantest9', 'romantest8', 'romantest7', 'romantest6', 'romantest10', 'pluraltest6']
>>> humansize_dict['romantest9']                                                            ④
'6.5 KiB'
  1. Esta compreensão de dicionário constrói uma lista de todos os arquivos no diretório de trabalho atual (glob.glob('*')), obtém os metadados de cada arquivo ( os.stat(f)) e constrói um dicionário cujas chaves são nomes de arquivos e cujos valores são os metadados de cada arquivo.
  2. Esta compreensão de dicionário se baseia na compreensão anterior, filtra arquivos menores que 6000 bytes (if meta.st_size > 6000) e usa essa lista filtrada para construir um dicionário cujas chaves são o nome do arquivo menos a extensão (os.path.splitext(f)[0]) e cujos valores são o tamanho aproximado de cada arquivo (humansize.approximate_size(meta.st_size)).
  3. Como você viu em um exemplo anterior, existem seis desses arquivos, portanto, existem seis itens neste dicionário.
  4. O valor de cada chave é a string retornada da função approximate_size().

Outras coisas divertidas para fazer com as compreensões de dicionário

Aqui está um truque com compreensões de dicionário que pode ser útil algum dia: trocar as chaves e os valores de um dicionário.

>>> a_dict = {'a': 1, 'b': 2, 'c': 3}
>>> {value:key for key, value in a_dict.items()}
{1: 'a', 2: 'b', 3: 'c'}

Claro, isso só funciona se os valores do dicionário forem imutáveis, como strings ou tuplas. Se você tentar fazer isso com um dicionário que contenha listas, a falha será espetacular.

>>> a_dict = {'a': [1, 2, 3], 'b': 4, 'c': 5}
>>> {value:key for key, value in a_dict.items()}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <dictcomp>
TypeError: unhashable type: 'list'

Definir Compreensões

Para não ficar de fora, os conjuntos também têm sua própria sintaxe de compreensão. É notavelmente semelhante à sintaxe para compreensões de dicionário. A única diferença é que os conjuntos têm apenas valores em vez de pares chave: valor.

>>> a_set = set(range(10))
>>> a_set
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> {x ** 2 for x in a_set}           ①
{0, 1, 4, 81, 64, 9, 16, 49, 25, 36}
>>> {x for x in a_set if x % 2 == 0}  ②
{0, 8, 2, 4, 6}
>>> {2**x for x in range(10)}         ③
{32, 1, 2, 4, 8, 64, 128, 256, 16, 512}
  1. As compreensões de conjuntos podem ter um conjunto como entrada. Esta compreensão de conjunto calcula os quadrados do conjunto de números de 0 a 9.
  2. Como as compreensões de lista e de dicionário, as compreensões de conjunto podem conter uma cláusula if para filtrar cada item antes de retorná-lo no conjunto de resultados.
  3. As compreensões de conjunto não precisam ter um conjunto como entrada; eles podem tomar qualquer sequência.

Leitura Adicional

Esse artigo é uma tradução de um capítulo do livro "Dive Into Python 3" escrito por Mark Pilgrim. Você pode ler o livro desde o início em português clicando aqui.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

0 comentários:

Postar um comentário