O que é a avaliação preguiçosa em Python? – Python real

Ser preguiçoso nem sempre é uma coisa ruim. Cada linha de código que o senhor escreve tem pelo menos uma expressão que o Python precisa avaliar. A avaliação preguiçosa do Python é quando o Python adota a opção preguiçosa e atrasa o cálculo do valor retornado por uma expressão até que esse valor seja necessário.

Uma expressão em Python é uma unidade de código que é avaliada como um valor. Exemplos de expressões incluem nomes de objetos, chamadas de funções, expressões com operadores aritméticos, literais que criam tipos de objetos incorporados, como listas, entre outros. Entretanto, nem todas as instruções são expressões. Por exemplo, if declarações e for não retornam um valor.

O Python precisa avaliar cada expressão que encontra para usar seu valor. Neste tutorial, o senhor aprenderá sobre as diferentes maneiras pelas quais o Python avalia essas expressões. O senhor entenderá por que algumas expressões são avaliadas imediatamente, enquanto outras são avaliadas posteriormente na execução do programa. Então, o que é avaliação preguiçosa em Python?

Em resumo: a avaliação preguiçosa do Python gera objetos somente quando necessário

Uma expressão é avaliada como um valor. No entanto, o senhor pode separar o tipo de avaliação das expressões em dois tipos:

  1. Avaliação ansiosa
  2. Avaliação preguiçosa

Avaliação ansiosa refere-se aos casos em que o Python avalia uma expressão assim que a encontra. Veja a seguir alguns exemplos de expressões que são avaliadas de forma ansiosa:

Ambientes interativos, como o REPL padrão do Python usado neste exemplo, exibe o valor de uma expressão quando a linha contém apenas a expressão. Esta seção de código mostra alguns exemplos de declarações e expressões:

  • Linhas 1 e 2: O primeiro exemplo inclui o operador de adição +, que o Python avalia assim que o encontra. O REPL mostra o valor 15.
  • Linhas 4 a 6: O segundo exemplo inclui duas linhas:
    • O declaração de importação inclui a palavra-chave import seguida do nome de um módulo. O nome do módulo random é avaliado com ansiedade.
    • O chamada de função random.randint() é avaliada com antecedência e seu valor é retornado imediatamente. Todas as funções padrão são avaliadas com antecedência. O senhor aprenderá mais tarde sobre as funções geradoras, que se comportam de forma diferente.
  • Linhas 8 a 12: O exemplo final tem três linhas de código:
    • O literal para criar uma lista é uma expressão que é avaliada de forma ansiosa. Essa expressão contém vários literais de números inteiros, que são expressões avaliadas imediatamente.
    • O declaração de atribuição atribui o objeto criado pelo literal de lista ao nome numbers. Essa declaração não é uma expressão e não retorna um valor. No entanto, ela inclui a lista literal no lado direito, que é uma expressão avaliada com antecedência.
    • A linha final contém o nome numbers, que é avaliada com ansiedade para retornar o objeto de lista.

A lista que o senhor criou no exemplo final é criada por completo quando o senhor a define. O Python precisa alocar memória para a lista e todos os seus elementos. Essa memória não será liberada enquanto a lista existir em seu programa. A alocação de memória neste exemplo é pequena e não afetará o programa. Entretanto, objetos maiores exigem mais memória, o que pode causar problemas de desempenho.

Avaliação preguiçosa refere-se aos casos em que o Python não calcula os valores de uma expressão imediatamente. Em vez disso, os valores são retornados no momento em que são necessários no programa. A avaliação preguiçosa também pode ser chamada de chamada por necessidade.

Esse atraso no momento em que o programa avalia uma expressão atrasa o uso de recursos para criar o valor, o que pode melhorar o desempenho de um programa ao distribuir o processo demorado em um período de tempo maior. Isso também evita a geração de valores que não serão usados no programa. Isso pode ocorrer quando o programa é encerrado ou passa para outra parte de sua execução antes que todos os valores gerados sejam usados.

Quando grandes conjuntos de dados são criados usando expressões lazily-evaluated, o programa não precisa usar a memória para armazenar o conteúdo da estrutura de dados. Os valores são gerados somente quando são necessários.

Um exemplo de avaliação preguiçosa ocorre na expressão for quando o usuário itera usando range():

O incorporado range() é o construtor da função range do Python. O range não armazena todos os um milhão de números inteiros que ele representa. Em vez disso, o objeto for cria um objeto range_iterator a partir do range que gera o próximo número da sequência quando ele é necessário. Portanto, o programa nunca precisa ter todos os valores armazenados na memória ao mesmo tempo.

A avaliação preguiçosa também permite que o senhor crie estruturas de dados infinitas, como uma transmissão ao vivo de dados de áudio ou vídeo que se atualiza continuamente com novas informações, pois o programa não precisa armazenar todos os valores na memória ao mesmo tempo. Estruturas de dados infinitas não são possíveis com a avaliação ansiosa, pois não podem ser armazenadas na memória.

Há desvantagens na avaliação diferida. Todos os erros gerados por uma expressão também são adiados para um ponto posterior do programa. Esse atraso pode dificultar a depuração.

A avaliação preguiçosa dos inteiros representados por range() em um for é um exemplo de avaliação preguiçosa. O senhor conhecerá mais exemplos na seção seguinte deste tutorial.

Quais são os exemplos de avaliação preguiçosa em Python?

Na seção anterior, você aprendeu a usar range() em um for o que leva a uma avaliação preguiçosa dos inteiros representados pelo range . Há outras expressões no Python que levam à avaliação preguiçosa. Nesta seção, você explorará as principais.

Outros tipos de dados incorporados

Os built-ins do Python zip() e enumerate() criam dois tipos de dados internos poderosos. O senhor explorará como esses tipos de dados estão vinculados à avaliação preguiçosa com o exemplo a seguir. Digamos que o senhor precise criar uma programação semanal, ou rota, que mostre quais membros da equipe levarão café pela manhã.

No entanto, a cafeteria está sempre ocupada nas manhãs de segunda-feira e ninguém quer ser responsável pelas segundas-feiras. Então, o senhor decide randomizar o rodízio toda semana. O senhor começa com uma lista contendo os nomes dos membros da equipe:

O senhor também embaralha os nomes usando random.shuffle(), que altera a lista no lugar. É hora de criar uma lista numerada para fixar no quadro de avisos toda semana:

O senhor usa enumerate() para iterar pela lista de nomes e também acessar um índice à medida que o senhor itera. Por padrão, enumerate() começa a contar a partir do zero. No entanto, o usuário usa a opção start para garantir que o primeiro número seja um.

Mas o que o enumerate() faz nos bastidores? Para explorar isso, o senhor pode chamar enumerate() e atribuir o objeto que ele retorna a um nome de variável:

O objeto criado é um enumerate que é um objeto iterador. Os iteradores são uma das principais ferramentas que permitem que o Python seja preguiçoso, pois seus valores são criados sob demanda. A chamada para enumerate() emparelha cada item em names com um número inteiro.

No entanto, ele não cria esses pares imediatamente. Os pares não são armazenados na memória. Em vez disso, eles são gerados quando o senhor precisa deles. Uma maneira de avaliar um valor de um iterador é chamar a função integrada next():

O numbered_names não contém todos os pares dentro dele. Quando ele precisa criar o próximo par de valores, ele busca o próximo nome na lista original names e o emparelha com o próximo número inteiro. O senhor pode confirmar isso alterando o terceiro nome da lista names antes de buscar o próximo valor em numbered_names:

Mesmo que o senhor tenha criado o enumerate objeto numbered_names antes de o senhor alterou o conteúdo da lista e buscou o terceiro item em names depois que o senhor o senhor fez a alteração. Esse comportamento é possível porque o Python avalia a variável enumerate de forma preguiçosa.

Olhe novamente para a lista numerada que o senhor criou anteriormente com o for que mostra que Sarah deve comprar café primeiro. Como Sarah é programadora de Python, ela perguntou se o 1 ao lado de seu nome significa que ela deve comprar café na terça-feira, já que segunda-feira deveria ser 0.

O senhor decide não ficar com raiva. Em vez disso, o senhor atualiza seu código para usar zip() para emparelhar nomes com dias da semana em vez de números. Observe que o senhor recria e embaralha a lista novamente, pois fez alterações nela:

Quando o senhor liga para zip(), o senhor cria um zip que é outro iterador. O programa não cria cópias dos dados em weekdays e names para criar os pares. Em vez disso, ele cria os pares sob demanda. Esse é outro exemplo de avaliação preguiçosa. O senhor pode explorar o zip diretamente, como o senhor fez com o objeto enumerate :

O programa não precisou criar e armazenar cópias dos dados quando o senhor chamou o enumerate() e o zip() por causa da avaliação preguiçosa. Outra consequência desse tipo de avaliação é que os dados não são fixos quando o senhor cria o enumerate ou zip objetos. Em vez disso, o programa usa os dados presentes nas estruturas de dados originais quando é necessário um valor do enumerate ou zip objetos.

Iteradores em itertools

Os iteradores são estruturas de dados preguiçosas, pois seus valores são avaliados quando são necessários e não imediatamente quando o usuário define o iterador. Há muitos outros iteradores no Python além do enumerate e zip. Todo iterável é um iterador em si ou pode ser convertido em um iterador usando iter().

No entanto, nesta seção, o senhor explorará O itertools módulo, que tem várias dessas estruturas de dados. O senhor aprenderá sobre duas dessas ferramentas agora e poderá experimentar algumas das outras depois de concluir este tutorial.

Na seção anterior, o senhor trabalhou com uma lista de membros da equipe. Agora, o senhor se une a outra equipe para participar de um questionário e deseja imprimir a lista de nomes de toda a equipe do questionário:

O iterável que o senhor usa na função for é o objeto criado pelo itertools.chain(), que encadeia as duas listas em um único iterável. No entanto, itertools.chain() não cria uma nova lista, mas um iterador, que é avaliado de forma preguiçosa. Portanto, o programa não cria cópias das cadeias de caracteres com os nomes, mas busca as cadeias de caracteres quando elas são necessárias nas listas first_name e second_name.

Aqui está outra maneira de observar a relação entre o iterador e as estruturas de dados originais:

A função sys.getrefcount() conta o número de vezes que um objeto é referenciado no programa. Observe que sys.getrefcount() sempre mostra mais uma referência ao objeto que vem da chamada ao sys.getrefcount() . Portanto, quando há apenas uma referência a um objeto no restante do programa, o senhor pode ver a referência a um objeto, sys.getrefcount() mostra duas referências.

Quando o senhor cria o chain o senhor cria outra referência para as duas listas, pois quiz_team precisa de uma referência ao local onde os dados originais estão armazenados. Portanto, sys.getrefcount() mostra uma referência extra ao first_team. Mas essa referência desaparece quando o senhor esgota o iterador:

Avaliação preguiçosa de estruturas de dados, como itertools.chain dependem dessa referência entre o iterador, como o itertools.chaine a estrutura que contém os dados, como first_team.

Outra ferramenta do itertools que destaca a diferença entre avaliação ansiosa e preguiçosa é a itertools.islice(), que é a versão de avaliação preguiçosa da fatia do Python. Crie uma lista de números e uma fatia padrão dessa lista:

Agora, o senhor pode criar uma versão de iterador da fatia usando itertools.islice():

Os argumentos do itertools.islice() incluem o iterável que o senhor deseja fatiar e os inteiros para determinar os índices de início e parada da fatia, como em uma fatia padrão. O senhor também pode incluir um argumento extra que representa o tamanho da etapa. A saída final não mostra os valores na fatia, pois eles ainda não foram gerados. Eles serão criados quando necessário.

Por fim, altere um dos valores da lista e faça um loop na fatia padrão e na fatia do iterador para comparar os resultados:

O senhor modifica o terceiro elemento da lista numbers. Essa alteração não afeta a fatia padrão, que ainda contém os números originais. Quando o senhor cria uma fatia padrão, o Python avalia essa fatia com entusiasmo e cria uma nova lista contendo o subconjunto de dados da sequência original.

No entanto, a fatia do iterador é avaliada de forma preguiçosa. Portanto, quando o senhor altera o terceiro valor na lista antes de o senhor percorra a fatia do iterador, o valor em iterator_slice também é afetado.

O senhor visitará o site itertools mais adiante neste tutorial para explorar mais alguns de seus iteradores.

Expressões do gerador e funções do gerador

As expressões que criam estruturas de dados incorporadas, como listas, tuplas ou dicionários, são avaliadas com ansiedade. Elas geram e armazenam todos os itens nessas estruturas de dados imediatamente. Um exemplo desse tipo de expressão é uma compreensão de lista:

A expressão no lado direito do operador de atribuição (=) cria uma compreensão de lista. Essa expressão é avaliada com ansiedade, e os dez valores de cara ou coroa são criados e armazenados na nova lista.

A compreensão de lista inclui um expressão condicional que retorna a string "Heads" ou "Tails" dependendo do valor da condição entre os if e else palavras-chave. As random.random() cria um float entre 0 e 1. Portanto, há uma chance de 50% de que o valor criado seja "Heads" ou "Tails".

O senhor pode substituir os colchetes por parênteses no lado direito do operador de atribuição:

A expressão entre parênteses é um expressão geradora. Embora pareça semelhante à compreensão de lista, essa expressão não é avaliada de forma ansiosa. Ela cria um objeto gerador. Um objeto gerador é um tipo de iterador que gera valores quando eles são necessários.

O objeto gerador coin_toss não armazena nenhum dos valores de string. Em vez disso, ele gerará cada valor quando for necessário. O senhor pode gerar e buscar o próximo valor usando a função integrada next():

A expressão que gera "Heads" ou "Tails" só é avaliado quando o senhor chama next(). Esse gerador gerará dez valores, pois o senhor usa range(10) no campo for do gerador. Como o senhor chamou next() duas vezes, restam oito valores a serem gerados:

O for itera oito vezes, uma vez para cada um dos itens restantes no gerador. Uma expressão de gerador é a alternativa de avaliação preguiçosa para criar uma lista ou uma tupla. Ela deve ser usada uma vez, ao contrário de suas contrapartes ávidas, como listas e tuplas.

O senhor também pode criar um objeto gerador usando um função geradora. Uma função geradora é uma definição de função que tem um yield em vez de uma declaração return . O senhor pode definir uma função geradora para criar um objeto gerador semelhante ao que usou no exemplo do lançamento de moeda acima:

O senhor cria um novo objeto gerador sempre que chama a função geradora. Ao contrário das funções padrão com returnque são avaliadas por completo, um gerador é avaliado de forma preguiçosa. Portanto, quando o primeiro valor é necessário, o código na função geradora executa o código até o primeiro yield declaração. Ele produz esse valor e faz uma pausa, aguardando a próxima vez que um valor for necessário.

Esse processo continua sendo executado até que não haja mais yield e a função geradora termine, gerando um StopIteration exceção. O protocolo de iteração no for captura essa StopIteration que é usado para sinalizar o fim do for loop.

A iteração preguiçosa em Python também permite que o senhor crie várias versões da estrutura de dados que são independentes umas das outras:

Os dois geradores, first_coin_tosses e second_coin_tossessão geradores separados criados a partir da mesma função geradora. O senhor avalia os três primeiros valores de first_coin_tosses. Isso deixa sete valores no primeiro gerador.

Em seguida, o senhor converte o segundo gerador em uma lista. Isso avalia todos os seus valores para armazená-los na lista second_as_list. Há dez valores, pois os valores que o senhor obteve do primeiro gerador não têm efeito sobre o segundo.

O senhor confirma que não há mais valores restantes no segundo gerador quando chama next() e recebe um StopIteration . No entanto, o primeiro gerador, first_coin_tosses, ainda tem valores a serem avaliados, pois é independente do segundo gerador.

Os geradores, e os iteradores em geral, são ferramentas centrais ao lidar com a avaliação preguiçosa em Python. Isso ocorre porque eles só produzem valores quando são necessários e não armazenam todos os seus valores na memória.

Avaliação de curto-circuito

Os exemplos de avaliação preguiçosa que o senhor viu até agora se concentraram em expressões que criam estruturas de dados. Entretanto, esses não são os únicos tipos de expressões que podem ser avaliadas de forma preguiçosa. Considere a expressão and e or operadores. Um equívoco comum é que esses operadores retornam True ou False. Em geral, não.

O senhor pode começar a explorar and com alguns exemplos:

Os dois primeiros exemplos têm operandos booleanos e retornam um booleano. O resultado é True somente quando ambos os operandos são True. No entanto, o terceiro exemplo não retorna um booleano. Em vez disso, ele retorna 0, que é o segundo operando em 1 and 0. E 0 and 1 também retorna 0mas, dessa vez, é o primeiro operando. O inteiro 0 é falso, o que significa que o bool(0) retorna False.

Da mesma forma, o número inteiro 1 é verdadeiro, o que significa que bool(1) retorna True. Todos os números inteiros diferentes de zero são verdadeiros. Quando o Python precisa de um valor booleano, como em um if ou com operadores como o and e orele converte o objeto em um booleano para determinar se deve ser tratado como verdadeiro ou falso.

Quando o usuário usa a função and o programa avalia o primeiro operando e verifica se ele é verdadeiro ou falso. Se o primeiro operando for falso, não há necessidade de avaliar o segundo operando, pois ambos precisam ser verdadeiros para que o resultado geral seja verdadeiro. Isso é o que ocorre na expressão 0 and 1 em que o and retorna o primeiro valor, que é falso. Portanto, a expressão inteira é falsa.

O Python não avalia o segundo operando quando o primeiro é falso. Isso é chamado de avaliação de curto-circuito e é um exemplo de avaliação preguiçosa. O Python só avalia o segundo operando se precisar dele.

Se o primeiro operando for verdadeiro, o Python avalia e retorna o segundo operando, qualquer que seja seu valor. Se o primeiro operando for verdadeiro, a veracidade do segundo operando determina a veracidade geral do and expressão.

Os dois últimos exemplos incluem operandos que são verdadeiros. O segundo operando é retornado em ambos os casos para tornar a expressão inteira verdadeira. O senhor pode confirmar que o Python não avalia o segundo operando se o primeiro for falso com os exemplos a seguir:

No primeiro exemplo, o primeiro operando é 0, e o print() nunca é chamada. No segundo exemplo, o primeiro operando é verdadeiro. Portanto, Python avalia o segundo operando, chamando a função print() . Observe que o resultado da função and é o valor retornado pela print(), que é None.

Outra demonstração impressionante de curto-circuito é quando o senhor usa uma expressão inválida como o segundo operando em um and expressão:

A chamada int("python") levanta uma ValueError pois a string "python" não pode ser convertida em um número inteiro. No entanto, nesse primeiro exemplo, o and retorna 0 sem gerar o erro. O segundo operando nunca foi avaliado!

O or funciona de forma semelhante. Entretanto, somente um operando precisa ser verdadeiro para que toda a expressão seja avaliada como verdadeira. Portanto, se o primeiro operando for verdadeiro, ele será retornado, e o segundo operando não será avaliado:

Em todos esses exemplos, o primeiro operando é retornado, pois é verdadeiro. O segundo operando é ignorado e nunca é avaliado. O senhor confirma isso com o exemplo final, que não gera um ValueError. Isso é uma avaliação de curto-circuito no or expressão. O Python é preguiçoso e não avalia expressões que não têm efeito sobre o resultado final.

No entanto, se o primeiro operando for falso, o resultado da expressão or é determinado pelo segundo operando:

A função integrada any() e all() também são avaliadas de forma preguiçosa usando a avaliação de curto-circuito. A any() retorna True se qualquer um dos elementos em um iterável for verdadeiro:

A lista que o senhor usa na primeira chamada para any() contém o número inteiro 0, o booleano Falsee uma string vazia. Todos os três objetos são falsos e any() retornam False. No segundo exemplo, o elemento final é uma cadeia de caracteres não vazia, que é verdadeira. A função retorna True.

A função para de avaliar os elementos do iterável quando encontra o primeiro valor verdadeiro. O senhor pode confirmar isso usando um truque semelhante ao que usou com a função and e or com a ajuda de uma função geradora, sobre a qual o senhor aprendeu na seção anterior:

O senhor define a função geradora lazy_values() com quatro yield declarações. A terceira declaração é inválida, pois o "python" não pode ser convertido em um número inteiro. O senhor cria um gerador quando chama essa função na chamada para any().

O programa não gera nenhum erro, e o any() retorna True. A avaliação do gerador foi interrompida quando any() encontrou a string "hello", que é o primeiro valor verdadeiro no gerador. A função any() realiza uma avaliação preguiçosa.

No entanto, se a expressão inválida não tiver nenhum valor verdadeiro antes dela, ela será avaliada e gerará um erro:

Os dois primeiros valores são falsos. Portanto, any() avalia o terceiro valor, o que gera o erro ValueError.

A função all() se comporta de forma semelhante. No entanto, all() exige que todos os elementos do iterável sejam verdadeiros. Portanto, all() entra em curto-circuito quando encontra o primeiro valor falso. O senhor atualiza a função geradora lazy_values() para verificar esse comportamento:

Esse código não gera um erro, pois o all() retorna False quando avalia a string vazia, que é o segundo elemento no gerador.

O curto-circuito, como outras formas de avaliação preguiçosa, evita a avaliação desnecessária de expressões quando essas expressões não são necessárias em tempo de execução.

Ferramentas de programação funcional

Programação funcional é um paradigma de programação no qual as funções só têm acesso aos dados inseridos como argumentos e não alteram o estado dos objetos, retornando novos objetos. Um programa escrito nesse estilo consiste em uma série dessas funções, muitas vezes com a saída de uma função usada como entrada para outra função.

Como os dados são frequentemente passados de uma função para outra, é conveniente usar a avaliação preguiçosa das estruturas de dados para evitar o armazenamento e a movimentação repetida de grandes conjuntos de dados.

Três das principais ferramentas da programação funcional são as funções incorporadas do Python map() e filter() funções e reduce()que faz parte do functools . Tecnicamente, as duas primeiras não são funções, mas construtores do módulo map e filter classes. Entretanto, o senhor as usa da mesma forma que usa as funções, especialmente no paradigma da programação funcional.

O senhor pode explorar map() e filter() com o exemplo a seguir. Crie uma lista de strings contendo nomes. Primeiro, o senhor deseja converter todos os nomes em letras maiúsculas:

O map() aplica a função str.upper() em cada item do iterável. Cada nome na lista é passado para str.upper()e o valor retornado é usado.

No entanto, map() não cria uma nova lista. Em vez disso, ele cria um map que é um iterador. Não é de surpreender que os iteradores apareçam com frequência em um tutorial sobre avaliação preguiçosa, pois eles são uma das principais ferramentas para a avaliação preguiçosa de valores!

O senhor pode avaliar cada valor, um de cada vez, usando next():

O senhor também pode converter o map em uma lista. Isso avalia os valores para que eles possam ser armazenados na lista:

Há apenas três nomes nessa lista. O senhor já avaliou e usou os dois primeiros nomes quando chamou next() duas vezes. Como os valores são avaliados quando são necessários e não armazenados na estrutura de dados, o senhor só pode usá-los uma vez.

Agora, o senhor deseja manter apenas os nomes que contenham pelo menos uma letra a. O senhor pode usar filter() para essa tarefa. Primeiro, o senhor precisará recriar o map que representa as letras maiúsculas, pois o senhor já esgotou esse gerador na sessão REPL:

Cada item no segundo argumento do filter(), que é o map objeto namesé passado para o lambda function que o senhor inclui como o primeiro argumento. Somente os valores para os quais a função lambda retorna True são mantidos. Os demais são descartados.

O senhor reutiliza a variável chamada names em cada estágio. Se preferir, o senhor pode usar identificadores de variáveis diferentes, mas se não precisar manter os resultados intermediários, é melhor usar a mesma variável. O objeto que o filter() retorna é outro iterador, um filter objeto. Portanto, seus valores ainda não foram avaliados.

O senhor pode converter o objeto filter para uma lista, como o senhor fez no exemplo anterior. Mas, nesse caso, tente fazer um loop usando um for em vez disso:

A primeira chamada de função para map() converte os nomes em letras maiúsculas. A segunda chamada, dessa vez para filter(), mantém apenas os nomes que incluem a letra a. O senhor usa letras maiúsculas A no código, pois o senhor já converteu todos os nomes para maiúsculas.

Por fim, o senhor só mantém nomes com quatro letras. O código abaixo mostra todos os map() e filter() já que o senhor precisa recriar esses iteradores todas as vezes:

O senhor pode reordenar as operações para tornar a avaliação geral mais preguiçosa. A primeira operação converte todos os nomes em maiúsculas, mas como o senhor descarta alguns desses nomes posteriormente, seria melhor evitar a conversão desses nomes. O senhor pode filtrar os nomes primeiro e convertê-los em maiúsculas na etapa final. O senhor adiciona "Andy" à lista de nomes para garantir que seu código funcione independentemente de a letra necessária ser maiúscula ou minúscula:

A primeira chamada ao filter() agora verifica se o senhor está usando letras maiúsculas ou minúsculas a está no nome. Como é mais provável que a letra a não for a primeira letra do nome, o senhor define o primeiro operando como ("a" in x) no campo or para aproveitar o curto-circuito com o or operador.

A avaliação preguiçosa obtida com o uso do map e filter iteradores significa que as estruturas de dados temporárias que contêm todos os dados não são necessárias em cada chamada de função. Isso não terá um impacto significativo nesse caso, pois a lista contém apenas seis nomes, mas pode afetar o desempenho com grandes conjuntos de dados.

Operações de leitura de arquivos

O exemplo final de expressões que são avaliadas de forma preguiçosa se concentrará na leitura de dados de um arquivo de valores separados por vírgula, geralmente chamado de arquivo CSV. Os arquivos CSV são um formato básico de arquivo de planilha eletrônica. São arquivos de texto com a extensão .csv que têm valores separados por vírgulas para indicar valores que pertencem a células diferentes na planilha. Cada linha termina com o caractere de nova linha "\n" para mostrar onde cada linha termina.

O senhor pode usar qualquer arquivo CSV que desejar para esta seção ou pode copiar os dados abaixo e salvá-los como um novo arquivo de texto com o comando .csv extensão. Nomeie o arquivo CSV superhero_pets.csv e coloque-o na pasta do seu projeto:

O senhor explorará duas maneiras de ler dados desse arquivo CSV. Na primeira versão, o senhor abrirá o arquivo e usará o comando .readlines() para objetos de arquivo:

O senhor importa pprint para permitir a impressão bonita de grandes estruturas de dados. Depois de abrir o arquivo CSV usando o with gerenciador de contextoespecificando a codificação do arquivo, o usuário chama o comando .readlines() para o arquivo aberto. Esse método retorna uma lista que contém todos os dados da planilha. Cada item da lista é uma cadeia de caracteres que contém todos os elementos em uma linha.

Essa avaliação é ávida, pois o .readlines() extrai todo o conteúdo da planilha e o armazena em uma lista. Essa planilha não contém muitos dados. No entanto, esse caminho pode levar a uma pressão significativa sobre os recursos de memória se o senhor estiver lendo grandes quantidades de dados.

Em vez disso, o senhor pode usar a função csv do Python, que faz parte da biblioteca padrão. Para simplificar esse código no REPL, o senhor pode abrir o arquivo sem usar um with gerenciador de contexto. No entanto, o senhor deve se lembrar de fechar o arquivo ao fazer isso. Em geral, o senhor deve usar o with para abrir arquivos sempre que possível:

O senhor adiciona o argumento nomeado newline="" ao abrir o arquivo a ser usado com o csv para garantir que todas as novas linhas dentro dos campos sejam tratadas corretamente. O objeto retornado por csv.reader() não é uma lista, mas um iterador. O senhor já se deparou com iteradores várias vezes neste artigo para saber o que esperar.

O conteúdo da planilha não é armazenado em uma estrutura de dados no programa Python. Em vez disso, o Python buscará preguiçosamente cada linha quando for necessário, obtendo os dados diretamente do arquivo, que ainda está aberto:

A primeira chamada para next() aciona a avaliação do primeiro item da tabela data iterador. Essa é a primeira linha da planilha, que é a linha do cabeçalho. O senhor chama next() mais duas vezes para obter as duas primeiras linhas de dados.

O senhor pode usar um for para iterar pelo restante do iterador e avaliar os itens restantes:

O senhor avaliou o cabeçalho e as duas primeiras linhas no código anterior. Portanto, o for só tem as três últimas linhas para iterar. E é uma boa prática fechar o arquivo Como o senhor não está usando um with declaração.

O reader() na função csv permite que o senhor avalie as linhas da planilha de forma preguiçosa, buscando cada linha somente quando for necessário. Entretanto, chamar o .readlines() em um arquivo aberto avalia as linhas de forma ansiosa, buscando todas elas imediatamente.

Como uma estrutura de dados pode ter elementos infinitos?

A avaliação preguiçosa de expressões também permite estruturas de dados com elementos infinitos. Estruturas de dados infinitas não podem ser obtidas por meio da avaliação ansiosa, pois não é possível gerar e armazenar elementos infinitos na memória! Entretanto, quando os elementos são gerados sob demanda, como na avaliação preguiçosa, é possível ter um objeto que represente um número infinito de elementos.

O itertools tem várias ferramentas que podem ser usadas para criar iteráveis infinitos. Uma delas é o itertools.count()que produz números sequenciais indefinidamente. O senhor pode definir o valor inicial e o tamanho da etapa ao criar um count iterador:

O iterador quarters produzirá valores 0,25 maior que o anterior e continuará produzindo valores para sempre. Entretanto, nenhum desses valores é gerado quando o senhor define quarters. Cada valor é gerado quando é necessário, por exemplo, ao chamar next() ou como parte de um processo de iteração, como um for loop.

Outra ferramenta que o senhor pode usar para criar iteradores infinitos é o itertools.cycle(). O senhor pode explorar essa ferramenta com a lista de nomes de membros da equipe que usou anteriormente neste tutorial para criar um rodízio de quem está encarregado de pegar o café da manhã. O senhor decide que não quer regenerar a lista toda semana, portanto, cria um iterador infinito que percorre os nomes:

O objeto retornado por itertools.cycle() é um iterador. Portanto, ele não cria todos os seus elementos quando é criado pela primeira vez. Em vez disso, ele gera valores quando eles são necessários:

O cycle iterador rota começa a produzir cada nome da lista original names. Quando todos os nomes tiverem sido fornecidos uma vez, o iterador começa a fornecer os nomes do início da lista novamente. Esse iterador nunca ficará sem valores para fornecer, pois reiniciará do início da lista sempre que chegar ao último nome.

Esse é um objeto com um número infinito de elementos. Entretanto, apenas cinco cadeias de caracteres são armazenadas na memória, pois há apenas cinco nomes na lista original.

O iterador rota é iterável, como todos os iteradores. Portanto, o senhor pode usá-lo como parte de um for loop statement. No entanto, isso agora cria um loop infinito, pois o for nunca recebe um StopIteration para acionar o fim do loop.

O senhor também pode obter estruturas de dados infinitas usando funções geradoras. O senhor pode recriar o rota definindo primeiro a função geradora generate_rota():

Na função geradora generate_rota(), o senhor gerencia manualmente o índice para buscar itens do iterável, aumentando o valor depois que cada item é obtido e zerando-o quando chega ao final do iterável. A função geradora inclui um while True o que torna essa estrutura de dados infinita.

Neste exemplo, a função geradora replica o comportamento que o senhor pode obter com itertools.cycle(). No entanto, o senhor pode criar qualquer gerador com requisitos personalizados usando essa técnica.

Quais são as vantagens da avaliação preguiçosa em Python?

O senhor pode revisitar um exemplo anterior para explorar uma das principais vantagens da avaliação preguiçosa. O senhor criou uma lista e um objeto gerador com vários resultados de um lançamento de moeda anteriormente neste tutorial. Nesta versão, o senhor criará um milhão de lançamentos de moedas em cada um deles:

O senhor cria uma lista e um objeto gerador. Ambos os objetos representam um milhão de cadeias de caracteres com "Heads" ou "Tails". No entanto, a lista ocupa mais de oito milhões de bytes de memória, enquanto o gerador usa apenas 200 bytes. O número de bytes pode ser um pouco diferente, dependendo da versão do Python que o senhor estiver usando.

A lista contém todas as um milhão de cadeias de caracteres, enquanto o gerador não contém, pois ele gerará esses valores quando forem necessários. Quando o senhor tem grandes quantidades de dados, o uso da avaliação ansiosa para definir estruturas de dados pode pressionar os recursos de memória do programa e afetar o desempenho.

Esse exemplo também mostra outra vantagem de usar a avaliação preguiçosa ao criar uma estrutura de dados. O senhor poderia usar a expressão condicional que retorna "Heads" ou "Tails" de forma aleatória diretamente no código sempre que o senhor precisar. Entretanto, criar um gerador pode ser uma opção melhor.

Como o senhor incluiu a lógica de como criar os valores de que precisa na expressão do gerador, pode usar uma expressão mais estilo declarativo de codificação no restante de seu código. O senhor afirma o que o senhor afirma o senhor deseja alcançar sem se concentrar no como o senhor para conseguir isso. Isso pode tornar seu código mais legível.

Outra vantagem da avaliação preguiçosa são os ganhos de desempenho que podem ser obtidos ao evitar a avaliação de expressões desnecessárias. Esses benefícios tornam-se perceptíveis em programas que avaliam um grande número de expressões.

O senhor pode demonstrar esse benefício de desempenho usando o comando timeit módulo na biblioteca padrão do Python. O senhor pode explorar isso com a avaliação de curto-circuito ao usar o módulo and . As duas expressões a seguir são semelhantes e retornam a mesma veracidade:

Essas expressões retornam um valor verdadeiro se ambas as chamadas para random.randint() retornarem valores diferentes de zero. Elas retornarão 0 se pelo menos uma função retornar 0. No entanto, é mais provável que o random.randint(0, 1) retorne 0 em comparação com o random.randint(0, 10).

Portanto, se o senhor precisar avaliar essa expressão repetidamente em seu código, a primeira versão é mais eficiente devido à avaliação de curto-circuito. O senhor pode cronometrar o tempo necessário para avaliar essas expressões várias vezes:

A saída mostra o tempo necessário para um milhão de avaliações de cada expressão. Há cinco tempos separados para cada expressão. A primeira versão é a que tem random.randint(0, 1) como seu primeiro operando, e é executada mais rapidamente do que a segunda, que tem os operandos trocados.

A avaliação do and entra em curto-circuito quando o primeiro random.randint() chamada retorna 0. Desde então random.randint(0, 1) tem 50 por cento de chance de retornar 0, cerca de metade das avaliações do and só chamará a primeira expressão do random.randint().

Quando random.randint(0, 10) é o primeiro operando, a avaliação da expressão só entrará em curto-circuito uma vez a cada onze vezes que for executada, pois há onze valores possíveis retornados por random.randint(0, 10).

As vantagens de reduzir o consumo de memória e melhorar o desempenho podem ser significativas em alguns projetos em que a demanda por recursos é importante. Entretanto, há algumas desvantagens na avaliação preguiçosa. O senhor as explorará na próxima seção.

Quais são as desvantagens da avaliação preguiçosa em Python?

A avaliação preguiçosa reduz os requisitos de memória e as operações desnecessárias ao atrasar a avaliação. Entretanto, esse atraso também pode dificultar a depuração. Se houver um erro em uma expressão avaliada de forma preguiçosa, a exceção não será levantada imediatamente. Em vez disso, o senhor só encontrará o erro em um estágio posterior da execução do código, quando a expressão for avaliada.

Para demonstrar isso, volte à lista de membros da equipe que o senhor usou anteriormente neste tutorial. Nessa ocasião, o senhor deseja acompanhar os pontos que eles ganharam durante um exercício de formação de equipe:

O senhor cria uma lista de jogadores e cada item da lista é um dicionário. Cada dicionário contém três pares de valores-chave para armazenar o nome do jogador, o número de jogos que ele jogou e o número total de pontos que marcou.

O senhor está interessado no número médio de pontos por jogo de cada jogador, portanto, crie um gerador com esse valor para cada jogador:

A expressão do gerador é avaliada de forma preguiçosa. Portanto, os valores necessários não são avaliados imediatamente. Agora, o senhor pode começar a chamar o next() para obter o número médio de pontos por jogo para cada jogador:

Seu código avalia e retorna os valores para os três primeiros jogadores. No entanto, ele gera um ZeroDivisionError quando tenta avaliar o quarto valor. Denise não gostou do evento de formação de equipe e não participou de nenhum dos jogos. Portanto, ela jogou zero jogos e marcou zero pontos. A operação de divisão em sua expressão geradora gera uma exceção nesse caso.

A avaliação ansiosa geraria esse erro no momento em que o senhor criasse o objeto. O senhor pode substituir os parênteses por colchetes para criar uma compreensão de lista em vez de um gerador:

Nesse cenário, o erro é gerado imediatamente. Erros atrasados podem dificultar sua identificação e correção, levando a uma maior dificuldade com o depuração código. Uma biblioteca Python popular de terceiros, TensorFlow, passou da avaliação preguiçosa para a avaliação ansiosa como opção padrão para facilitar a depuração. Os usuários podem então ativar a avaliação preguiçosa usando um decorador quando concluírem o processo de depuração.

Conclusão

Neste tutorial, o senhor aprendeu o que é a avaliação lazy em Python e como ela é diferente da avaliação eager. Algumas expressões não são avaliadas quando o programa as encontra pela primeira vez. Em vez disso, elas são avaliadas quando os valores são necessários no programa.

Esse tipo de avaliação é chamado de avaliação preguiçosa e pode resultar em um código mais legível que também é mais eficiente em termos de memória e desempenho. Por outro lado, a avaliação ansiosa é quando uma expressão é avaliada por completo imediatamente.

O modo de avaliação ideal depende de vários fatores. Para conjuntos de dados pequenos, não há benefícios perceptíveis no uso da avaliação preguiçosa para eficiência e desempenho da memória. Entretanto, as vantagens da avaliação preguiçosa tornam-se mais importantes para grandes quantidades de dados. A avaliação preguiçosa também pode dificultar a detecção e a correção de erros e bugs.

A avaliação preguiçosa também não é ideal quando o senhor está gerando estruturas de dados, como iteradores, e precisa usar os valores repetidamente no programa. Isso ocorre porque o senhor precisará gerar os valores novamente sempre que precisar deles.

No Python, a avaliação preguiçosa geralmente ocorre nos bastidores. No entanto, o senhor também precisará decidir quando usar expressões que são avaliadas de forma ansiosa ou preguiçosa, como quando precisa criar uma lista ou um objeto gerador. Agora, o senhor está equipado com o conhecimento necessário para entender como lidar com os dois tipos de avaliação.

Faça o teste: Teste seus conhecimentos com nosso questionário interativo “O que é avaliação preguiçosa em Python?”. O senhor receberá uma pontuação após a conclusão para ajudá-lo a acompanhar seu progresso no aprendizado:


Questionário interativo

O que é avaliação preguiçosa em Python?

Neste teste, o senhor testará sua compreensão das diferenças entre avaliação preguiçosa e ansiosa em Python. Ao responder a este teste, o senhor revisitará como o Python otimiza o uso da memória e a sobrecarga computacional ao decidir quando computar valores.