terça-feira, 11 de maio de 2021

Closures e geradores em python

❝Minha ortografia é instável. É boa ortografia, mas oscila e as letras ficam nos lugares errados.❞
- Ursinho Pooh

Mergulho

Tendo crescido como filho de um bibliotecário e formado em inglês, sempre fui fascinado por línguas. Não são linguagens de programação. Bem, sim, linguagens de programação, mas também linguagens naturais. Faça o inglês. O inglês é uma língua esquizofrênica que empresta palavras do alemão, francês, espanhol e latim (para citar alguns). Na verdade, “pede emprestado” é a palavra errada; “Pilhagens” é mais parecido. Ou talvez “assimila” - como os Borg. Sim eu gosto disso.

We are the Borg. Your linguistic and etymological distinctiveness will be added to our own. Resistance is futile.

Neste capítulo, você aprenderá sobre substantivos no plural. Além disso, funções que retornam outras funções, expressões regulares avançadas e geradores. Mas primeiro, vamos falar sobre como fazer substantivos no plural. (Se você ainda não leu o capítulo sobre expressões regulares, agora seria uma boa hora. Este capítulo pressupõe que você entende os fundamentos das expressões regulares e rapidamente desce para usos mais avançados).

Se você cresceu em um país onde se fala inglês ou aprendeu inglês em uma escola formal, provavelmente você está familiarizado com as regras básicas:

  • Se uma palavra terminar em S, X ou Z, adicione ES. Bass torna-se Basses, fax torna-se faxes, e waltz se torna waltzes.
  • Se uma palavra terminar em um H barulhento, adicione ES; se terminar em H silencioso, basta adicionar S. O que é um H barulhento? Um que se combina com outras letras para fazer um som que você pode ouvir. Assim, o coach se torna um coaches e a rash se transforma em rashes, porque você pode ouvir os sons de CH e SH quando os diz. Mas cheetah torna-se cheetahs, porque o H é silenciosa.
  • Se uma palavra terminar em Y que soe como I, altere Y para IES; se o Y é combinado com uma vogal a soar como algo mais, basta adicionar S. Então vacancy se torna vacancies, mas day se torna days.
  • Se tudo mais falhar, basta adicionar S e esperar o melhor.

(Eu sei, há uma série de exceções. Man se torna men e woman se torna women, mas human se torna humans. Mouse torna-se mice e louse torna-se lice, mas house torna-se houses. Knife torna-se knives e wife torna-se wives, mas lowlife se torna lowlifes. E nem me fale em palavras que estão no seu próprio plural, como sheep, deer e haiku).

Outras línguas, é claro, são completamente diferentes.

Vamos projetar uma biblioteca Python que pluraliza automaticamente os substantivos em inglês. Começaremos apenas com essas quatro regras, mas lembre-se de que você inevitavelmente precisará adicionar mais.

Eu sei, vamos usar expressões regulares!

Então você está olhando para palavras, o que, pelo menos em inglês, significa que você está olhando para cadeias de caracteres. Você tem regras que dizem que você precisa encontrar diferentes combinações de caracteres e, em seguida, fazer coisas diferentes com eles. Isso parece um trabalho para expressões regulares!

import re

def plural(noun):          
    if re.search('[sxz]$', noun):             ①
        return re.sub('$', 'es', noun)        ②
    elif re.search('[^aeioudgkprt]h$', noun):
        return re.sub('$', 'es', noun)       
    elif re.search('[^aeiou]y$', noun):      
        return re.sub('y$', 'ies', noun)     
    else:
        return noun + 's'
  1. Esta é uma expressão regular, mas usa uma sintaxe que você não viu nas Expressões regulares. Os colchetes significam "corresponder exatamente a um desses caracteres". Então [sxz] significa “s, ou x, ou z”, mas apenas um deles. O $ deve ser familiar; ele corresponde ao final da string. Combinadas, isso testa expressão regulares, se substantivo termina com s, x ou z.
  2. Esta função re.sub() executa substituições de string baseadas em expressões regulares.

Vejamos as substituições de expressões regulares com mais detalhes.

>>> import re
>>> re.search('[abc]', 'Mark')    ①
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.sub('[abc]', 'o', 'Mark')  ②
'Mork'
>>> re.sub('[abc]', 'o', 'rock')  ③
'rook'
>>> re.sub('[abc]', 'o', 'caps')  ④
'oops'
  1. Será que a string Mark conter a, b ou c? Sim, contém a.
  2. OK, agora encontre a, b ou c e substitua-o por o. Mark torna-se Mork.
  3. A mesma função transforma rock em rook.
  4. Você pode pensar que isso iria transformar caps em oaps, mas isso não acontece. re.sub substitui todas as correspondências, não apenas a primeira. Portanto, essa expressão regular transforma caps em oops, porque tanto o c quanto o a são transformados em o.

E agora, de volta à função plural()...

def plural(noun):          
    if re.search('[sxz]$', noun):            
        return re.sub('$', 'es', noun)         ①
    elif re.search('[^aeioudgkprt]h$', noun):  ②
        return re.sub('$', 'es', noun)
    elif re.search('[^aeiou]y$', noun):        ③
        return re.sub('y$', 'ies', noun)     
    else:
        return noun + 's'
  1. Aqui, você está substituindo o final da string (combinada por $) pela string es. Em outras palavras, adicionando es à string. Você poderia realizar a mesma coisa com a concatenação de strings, por exemplo noun + 'es', mas optei por usar expressões regulares para cada regra, por motivos que ficarão claros posteriormente neste capítulo.
  2. Olhe bem, esta é outra nova variação. O ^ como o primeiro caractere dentro dos colchetes significa algo especial: negação. [^abc] significa “qualquer caractere único, exceto a , b ou c”. Então, [^aeioudgkprt] significa qualquer caractere, exceto a, e, i, o, u, d, g, k, p, r, ou t. Então, esse caractere precisa ser seguido por h, seguido pelo final da string. Você está procurando palavras que terminam em H, onde o H pode ser ouvido.
  3. Mesmo padrão aqui: coincidem com as palavras que terminam em Y, onde o caractere antes do Y é qualquer caracter diferente de a, e, i, o, ou u. Você está procurando palavras que terminam em Y que soam como I.

Vejamos as expressões regulares de negação com mais detalhes.

>>> import re
>>> re.search('[^aeiou]y$', 'vacancy')  ①
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.search('[^aeiou]y$', 'boy')      ②
>>> 
>>> re.search('[^aeiou]y$', 'day')
>>> 
>>> re.search('[^aeiou]y$', 'pita')     ③
  1. vacancy corresponde a esta expressão regular, porque termina no cy, e c não é a, e, i, o, ou u.
  2. boy não corresponde, porque termina em oy, e você disse especificamente que o caractere anterior ao y não poderia ser o. day não corresponde, porque termina em ay.
  3. pita não corresponde, porque não termina em y.
>>> re.sub('y$', 'ies', 'vacancy')               ①
'vacancies'
>>> re.sub('y$', 'ies', 'agency')
'agencies'
>>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy')  ②
'vacancies'
  1. Essa expressão regular transforma vacancy em vacancies e agency em agencies, que é o que você queria. Observe que também transformaria boy em boies, mas isso nunca acontecerá na função porque você chamou re.search primeiro para descobrir se deveria fazer essa substituição (re.sub).
  2. De passagem, quero salientar que é possível combinar essas duas expressões regulares (uma para descobrir se a regra se aplica e outra para aplicá-la de fato) em uma única expressão regular. É assim que ficaria. A maior parte deve parecer familiar: você está usando um grupo lembrado, que aprendeu no Estudo de caso: análise de números de telefone. O grupo é usado para lembrar o caractere antes da letra y. Então, na string de substituição, você usa uma nova sintaxe \1, que significa “ei, aquele primeiro grupo que você lembrou? coloque bem aqui”. Neste caso, você se lembra de c antes do y; quando você faz a substituição, você substitui c no lugar de c, e ies no lugar de y. (Se você tiver mais de um grupo lembrado, você pode usar \2 e \3 e assim por diante).

As substituições de expressões regulares são extremamente poderosas e a sintaxe \1 as torna ainda mais poderosas. Mas combinar a operação inteira em uma expressão regular também é muito mais difícil de ler e não mapeia diretamente para a maneira como você descreveu pela primeira vez as regras de pluralização. Você originalmente estabeleceu regras como “se a palavra terminar em S, X ou Z, adicione ES”. Se você olhar para esta função, terá duas linhas de código que dizem “se a palavra terminar em S, X ou Z, adicione ES”. Não fica muito mais direto do que isso.

Uma lista de funções

Agora você vai adicionar um nível de abstração. Você começou definindo uma lista de regras: se isso, faça aquilo, caso contrário, vá para a próxima regra. Vamos complicar temporariamente parte do programa para que você possa simplificar outra parte.

import re

def match_sxz(noun):
    return re.search('[sxz]$', noun)

def apply_sxz(noun):
    return re.sub('$', 'es', noun)

def match_h(noun):
    return re.search('[^aeioudgkprt]h$', noun)

def apply_h(noun):
    return re.sub('$', 'es', noun)

def match_y(noun):                             ①
    return re.search('[^aeiou]y$', noun)
        
def apply_y(noun):                             ②
    return re.sub('y$', 'ies', noun)

def match_default(noun):
    return True

def apply_default(noun):
    return noun + 's'

rules = ((match_sxz, apply_sxz),               ③
         (match_h, apply_h),
         (match_y, apply_y),
         (match_default, apply_default)
         )

def plural(noun):           
    for matches_rule, apply_rule in rules:       ④
        if matches_rule(noun):
            return apply_rule(noun)
  1. Agora, cada regra de correspondência é sua própria função, que retorna os resultados da chamada da função re.search().
  2. Cada regra de aplicação também é sua própria função, que chama a função re.sub() para aplicar a regra de pluralização apropriada.
  3. Em vez de ter uma função (plural()) com várias regras, você tem a estrutura de dados, rules, que é uma sequência de pares de funções.
  4. Como as regras foram divididas em uma estrutura de dados separada, a nova função plural() pode ser reduzida a algumas linhas de código. Usando um loop for, você pode retirar a correspondência e aplicar duas regras por vez (uma correspondência, uma aplicação) da estrutura de rules. Na primeira iteração do loop for, match_rule será obtido match_sxz e apply_rule será obtido apply_sxz. Na segunda iteração (presumindo que você tenha chegado tão longe), match_rule será atribuído match_h, e apply_rule será atribuído apply_h. A função tem a garantia de retornar algo eventualmente, porque a regra de correspondência final (match_default) simplesmente retorna True, o que significa que a regra de aplicação correspondente (apply_default) sempre será aplicado.

A variável “rules” é uma sequência de pares de funções.

O motivo pelo qual essa técnica funciona é que tudo em Python é um objeto, incluindo funções. A estrutura de dados das rules contém funções - não nomes de funções, mas objetos de função reais. Quando eles são atribuídos no loop for, match_rule e apply_rule são funções reais que você pode chamar. Na primeira iteração do loop for, isso equivale a chamar matches_sxz(noun) e, se retornar uma correspondência, chamar apply_sxz(noun).

Se esse nível adicional de abstração for confuso, tente desenrolar a função para ver a equivalência. Todo o loop for é equivalente ao seguinte:

def plural(noun):
    if match_sxz(noun):
        return apply_sxz(noun)
    if match_h(noun):
        return apply_h(noun)
    if match_y(noun):
        return apply_y(noun)
    if match_default(noun):
        return apply_default(noun)

A vantagem aqui é que a função plural() agora foi simplificada. Ele pega uma sequência de regras, definida em outro lugar, e itera por meio delas de maneira genérica.

  1. Obtenha uma regra de correspondência.
  2. Combina? Em seguida, chame a regra de aplicação e retorne o resultado.
  3. Sem correspondência? Vá para a etapa 1.

As regras podem ser definidas em qualquer lugar, de qualquer maneira. A função plural() não importa.

Agora, adicionar esse nível de abstração. Valeu a pena? Bem, ainda não. Vamos considerar o que seria necessário para adicionar uma nova regra à função. No primeiro exemplo, seria necessário adicionar uma instrução if à função plural(). Neste segundo exemplo, seria necessário adicionar duas funções match_foo() e apply_foo(), em seguida, atualizar a sequência de rules para especificar onde, na ordem, as novas funções de correspondência e aplicação devem ser chamadas em relação às outras regras.

Mas este é realmente apenas um trampolim para a próxima seção. Vamos continuar…

Uma lista de padrões

Definir funções nomeadas separadas para cada correspondência e aplicar regra não é realmente necessário. Você nunca liga para eles diretamente; você os adiciona à sequência de rules e os chama por lá. Além disso, cada função segue um de dois padrões. Todas as funções de correspondência são chamadas re.search()e todas as funções de aplicação são chamadas re.sub(). Vamos fatorar os padrões para que seja mais fácil definir novas regras.

import re

def build_match_and_apply_functions(pattern, search, replace):
    def matches_rule(word):                                     ①
        return re.search(pattern, word)
    def apply_rule(word):                                       ②
        return re.sub(search, replace, word)
    return (matches_rule, apply_rule)                           ③
  1. build_match_and_apply_functions() é uma função que cria outras funções dinamicamente. Ela pega pattern, search e substitui, então define uma função matches_rule() que chama re.search() com o pattern que foi passado para a função build_match_and_apply_functions() e a word que foi passada para a função matches_rule() que você está construindo. Uau.
  2. Construir a função apply_rule funciona da mesma maneira. A função apply_rule é uma função que recebe um parâmetro e chama re.sub() com os parâmetros de search e replace que foram passados ​​para a função build_match_and_apply_functions() e a word que foi passada para a função apply_rule() que você está construindo. Essa técnica de usar os valores de parâmetros externos dentro de uma função dinâmica é chamada de closures. Você está essencialmente definindo constantes dentro de apply_rule que você está construindo: leva um parâmetro (word), mas então atua sobre ele mais dois outros valores (search e replace) que foram configurados quando você definiu a função de aplicação.
  3. Finalmente, a função build_match_and_apply_functions() retorna uma tupla de dois valores: as duas funções que você acabou de criar. As constantes que você definiu nessas funções (pattern dentro da função matches_rule() e search e replace dentro da função apply_rule()) permanecem com essas funções, mesmo depois de retornar de build_match_and_apply_functions(). Isso é incrivelmente legal.

Se isso é incrivelmente confuso (e deveria ser, isso é coisa esquisita), pode ficar mais claro quando você vir como usá-lo.

patterns = \                                                        ①
  (
    ('[sxz]$',           '$',  'es'),
    ('[^aeioudgkprt]h$', '$',  'es'),
    ('(qu|[^aeiou])y$',  'y$', 'ies'),
    ('$',                '$',  's')                                 ②
  )
rules = [build_match_and_apply_functions(pattern, search, replace)  ③
         for (pattern, search, replace) in patterns]
  1. Nossas “regras” de pluralização são agora definidas como uma tupla de tuplas de strings (não funções). A primeira string em cada grupo é o padrão de expressão regular que você usaria re.search() para ver se essa regra corresponde. A segunda e a terceira strings em cada grupo são as expressões de pesquisa e substituição que você usaria re.sub() para realmente aplicar a regra para transformar um substantivo em seu plural.
  2. Há uma pequena mudança aqui, na regra de fallback. No exemplo anterior, a função match_default() simplesmente retornou True, o que significa que se nenhuma das regras mais específicas correspondesse, o código simplesmente adicionaria um s ao final da palavra dada. Este exemplo faz algo funcionalmente equivalente. A expressão regular final pergunta se a palavra tem um final ($ corresponde ao final de uma string). É claro que toda string tem um fim, até mesmo uma string vazia, portanto, essa expressão sempre corresponde. Assim, ele tem o mesmo propósito que a função match_default() que sempre retornou True: ele garante que, se nenhuma regra mais específica corresponder, o código adiciona um s ao final da palavra dada.
  3. Esta linha é mágica. Ele pega a sequência de strings em patterns e os transforma em uma sequência de funções. Como? Ao “mapear” as strings para a função build_match_and_apply_functions(). Ou seja, ele pega cada trinca de strings e chama a função build_match_and_apply_functions() com essas três strings como argumentos. A função build_match_and_apply_functions() retorna uma tupla de duas funções. Isso significa que as rules acabam sendo funcionalmente equivalentes ao exemplo anterior: uma lista de tuplas, onde cada tupla é um par de funções. A primeira função é a função de correspondência que chama re.search() e a segunda função é a função de aplicação que chama re.sub().

Completando esta versão do script está o principal ponto de entrada, a função plural().

def plural(noun):
    for matches_rule, apply_rule in rules:  ①
        if matches_rule(noun):
            return apply_rule(noun)
  1. Como a lista de rules é a mesma do exemplo anterior (realmente é), não deve ser surpresa que a função plural() não tenha mudado em nada. É completamente genérico; ele pega uma lista de funções de regra e as chama em ordem. Não importa como as regras são definidas. No exemplo anterior, eles foram definidos como funções nomeadas separadas. Agora eles são construídos dinamicamente mapeando a saída da função build_match_and_apply_functions() em uma lista de strings brutas. Não importa; a função plural() ainda funciona da mesma maneira.

Um arquivo de padrões

Você fatorou todo o código duplicado e adicionou abstrações suficientes para que as regras de pluralização sejam definidas em uma lista de strings. A próxima etapa lógica é pegar essas strings e colocá-las em um arquivo separado, onde podem ser mantidas separadamente do código que as usa.

Primeiro, vamos criar um arquivo de texto que contém as regras que você deseja. Nenhuma estrutura de dados extravagante, apenas strings delimitadas por espaços em branco em três colunas. Vamos chamá-lo plural4-rules.txt.

[sxz]$               $    es
[^aeioudgkprt]h$     $    es
[^aeiou]y$          y$    ies
$                    $    s

Agora vamos ver como você pode usar este arquivo de regras.

import re

def build_match_and_apply_functions(pattern, search, replace):  ①
    def matches_rule(word):
        return re.search(pattern, word)
    def apply_rule(word):
        return re.sub(search, replace, word)
    return (matches_rule, apply_rule)

rules = []
with open('plural4-rules.txt', encoding='utf-8') as pattern_file:  ②
    for line in pattern_file:                                      ③
        pattern, search, replace = line.split(None, 2)             ④
        rules.append(build_match_and_apply_functions(              ⑤
                pattern, search, replace))
  1. A função build_match_and_apply_functions() não mudou. Você ainda está usando closures para construir duas funções dinamicamente que usam variáveis definidas na função externa.
  2. A função global open() abre um arquivo e retorna um objeto de arquivo. Nesse caso, o arquivo que estamos abrindo contém as strings de padrão para substantivos pluralizantes. A instrução with cria o que é chamado de contexto: quando o bloco with termina, o Python fecha automaticamente o arquivo, mesmo se uma exceção for levantada dentro do bloco with. Você aprenderá mais sobre blocos with e objetos de arquivo no capítulo Arquivos.
  3. for line in <fileobject> lê os dados do arquivo aberto, uma linha por vez, e atribui o texto à variável line. Você aprenderá mais sobre como ler arquivos no capítulo Arquivos.
  4. Cada linha do arquivo realmente tem três valores, mas eles são separados por espaços em branco (tabulações ou espaços, não faz diferença). Para dividir, use o método split() das strings. O primeiro argumento do método split() é None, que significa “dividir em qualquer espaço em branco (tabulações ou espaços, não faz diferença)”. O segundo argumento é 2, que significa "dividir em espaços em branco 2 vezes (dividir uma vez retorna dois valores, dividir duas vezes retorna três valores e assim por diante) e, em seguida, deixe o resto da linha sozinho." Uma linha como [sxz]$ $ esserá dividida na lista ['[sxz]$', '$', 'es'], o que significa que o pattern será obtido '[sxz]$', search obterá '$' e a replace obterá 'es'. Isso é muito poder em uma pequena linha de código.
  5. Finalmente, você passa pattern, search e replace para a função build_match_and_apply_functions(), que retorna uma tupla de funções. Você anexa essa tupla à lista de rules e as rules acabam armazenando a lista de funções de correspondência e aplicação que a função plural() espera.

A melhoria aqui é que você separou completamente as regras de pluralização em um arquivo externo, para que ele possa ser mantido separadamente do código que o usa. Código é código, dados são dados e a vida é boa.

Geradores

Não seria ótimo ter uma função plural() genérica que analisa o arquivo de regras? Obtenha regras, verifique se há uma correspondência, aplique a transformação apropriada e seguir para a próxima regra. Isso é tudo que a função plural() precisa fazer, e isso é tudo que a função plural() deve fazer.

def rules(rules_filename):
    with open(rules_filename, encoding='utf-8') as pattern_file:
        for line in pattern_file:
            pattern, search, replace = line.split(None, 2)
            yield build_match_and_apply_functions(pattern, search, replace)

def plural(noun, rules_filename='plural5-rules.txt'):
    for matches_rule, apply_rule in rules(rules_filename):
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))

Como diabos isso funciona? Vejamos primeiro um exemplo interativo.

>>> def make_counter(x):
...     print('entering make_counter')
...     while True:
...         yield x                    ①
...         print('incrementing x')
...         x = x + 1
... 
>>> counter = make_counter(2)          ②
>>> counter                            ③
<generator object at 0x001C9C10>
>>> next(counter)                      ④
entering make_counter
2
>>> next(counter)                      ⑤
incrementing x
3
>>> next(counter)                      ⑥
incrementing x
4
  1. A presença da palavra-chave yield em make_counter significa que esta não é uma função normal. É um tipo especial de função que gera valores um de cada vez. Você pode pensar nisso como uma função recuperável. Chamá-lo retornará um gerador que pode ser usado para gerar valores sucessivos de x.
  2. Para criar uma instância do gerador make_counter, basta chamá-la como qualquer outra função. Observe que isso não executa realmente o código da função. Você pode dizer isso porque a primeira linha da função make_counter() chama print(), mas nada foi impresso ainda.
  3. A função make_counter() retorna um objeto gerador.
  4. A função next() pega um objeto gerador e retorna seu próximo valor. A primeira vez que você chama next() com o gerador de counter, ele executa o código make_counter() até a primeira instrução yield e retorna o valor gerado. Nesse caso, será 2, porque você criou originalmente o gerador chamando make_counter(2).
  5. Chamar repetidamente next() com o mesmo objeto gerador continua exatamente de onde parou e continua até chegar à próxima instrução yield. Todas as variáveis, estado local, etc. são salvos yield e restaurados em next(). A próxima linha de código esperando para ser executada chama print(), que imprime incrementing x. Depois disso, a declaração x = x + 1. Em seguida, ele percorre o loop while novamente e a primeira coisa que atinge é a instrução yield x, que salva o estado de tudo e retorna o valor atual de x (agora 3).
  6. Na segunda vez que você chama next(counter), você faz todas as mesmas coisas novamente, mas desta vez x é agora 4.

Uma vez que make_counter configura um loop infinito, você poderia teoricamente fazer isso para sempre, e ele simplesmente continuaria incrementando x e emitindo valores. Mas, em vez disso, vamos dar uma olhada em usos mais produtivos de geradores.

Um gerador de Fibonacci

“Yield” pausa uma função. “Next()” continua de onde parou.

def fib(max):
    a, b = 0, 1          ①
    while a < max:
        yield a          ②
        a, b = b, a + b  ③
  1. A sequência de Fibonacci é uma sequência de números onde cada número é a soma dos dois números anteriores. Ele começa com 0 e 1 aumenta lentamente no início, depois cada vez mais rapidamente. Para iniciar a sequência, você precisa de duas variáveis: a começa em 0 e b começa em 1.
  2. a é o número atual na sequência, portanto, forneça-o.
  3. b é o próximo número na sequência, então atribua-o a a, mas também calcule o próximo valor (a + b) e atribua-o a b para uso posterior. Observe que isso acontece em paralelo; se a for 3 e b for 5, a, b = b, a + b definirá a como 5 (o valor anterior de b) e b como 8 (a soma dos valores anteriores de a e b).

Portanto, você tem uma função que produz números de Fibonacci sucessivos. Claro, você poderia fazer isso com recursão, mas dessa forma é mais fácil de ler. Além disso, funciona bem com loops for.

>>> from fibonacci import fib
>>> for n in fib(1000):      ①
...     print(n, end=' ')    ②
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
>>> list(fib(1000))          ③
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]
  1. Você pode usar um gerador como fib() em um loop for diretamente. O loop for irá chamar automaticamente a função next() para obter valores do gerador fib() e atribuí-los à variável n de índice do loop for.
  2. Cada vez que percorre o loop for, n obtém um novo valor da instrução yield em fib() e tudo o que você precisa fazer é imprimi-lo. Uma vez que os números de fib() acabam (a torna-se maior que max, o que neste caso é 1000), o loop for termina normalmente.
  3. Este é útil: passe um gerador para a função list() e ele iterará por todo o gerador (assim como o loop for no exemplo anterior) e retornará uma lista de todos os valores.

Um gerador de regra plural

Vamos voltar para o script plural5.py e ver como essa versão da função plural() funciona.

def rules(rules_filename):
    with open(rules_filename, encoding='utf-8') as pattern_file:
        for line in pattern_file:
            pattern, search, replace = line.split(None, 2)                   ①
            yield build_match_and_apply_functions(pattern, search, replace)  ②

def plural(noun, rules_filename='plural5-rules.txt'):
    for matches_rule, apply_rule in rules(rules_filename):                   ③
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))
  1. Nenhuma mágica aqui. Lembre-se de que as linhas do arquivo de regras têm três valores separados por espaços em branco, então você usa line.split(None, 2) para obter as três “colunas” e atribuí-las a três variáveis locais.
  2. E então você cede. O que você cede? Duas funções, construídas dinamicamente com seu velho amigo build_match_and_apply_functions(), que são idênticas aos exemplos anteriores. Em outras palavras, rules() é um gerador que gera correspondência e aplica funções sob demanda.
  3. Como rules() é um gerador, você pode usá-lo diretamente em um loop for. Na primeira vez no loop for, você chamará a função rules(), que abrirá o arquivo de padrões, lerá a primeira linha, construirá dinamicamente uma função de correspondência e uma função de aplicação a partir dos padrões nessa linha e produzirá as funções construídas dinamicamente. Na segunda vez no loop for, você continuará exatamente de onde parou rules() (que foi no meio do loop for line in pattern_file). A primeira coisa que ele fará é ler a próxima linha do arquivo (que ainda está aberta), construir dinamicamente outra correspondência e aplicar a função com base nos padrões dessa linha no arquivo e produzir as duas funções.

O que você ganhou no estágio 4? Hora de inicialização. No estágio 4, quando você importou o módulo plural4, ele leu todo o arquivo de padrões e construiu uma lista de todas as regras possíveis, antes mesmo que você pudesse pensar em chamar a função plural(). Com os geradores, você pode fazer tudo preguiçosamente: você lê a primeira regra, cria funções e as experimenta, e se isso funcionar, você nunca lê o resto do arquivo ou cria quaisquer outras funções.

O que você perdeu? Desempenho! Cada vez que você chama a função plural(), o gerador rules() começa do início - o que significa reabrir o arquivo de padrões e ler desde o início, uma linha de cada vez.

E se você pudesse ter o melhor dos dois mundos: custo mínimo de inicialização (não execute nenhum código import) e desempenho máximo (não crie as mesmas funções repetidamente). Ah, e você ainda deseja manter as regras em um arquivo separado (porque código é código e dados são dados), contanto que nunca precise ler a mesma linha duas vezes.

Para fazer isso, você precisará construir seu próprio iterador. Mas antes de fazer isso, você precisa aprender sobre as classes Python.

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