Mostrando postagens com marcador python. Mostrar todas as postagens
Mostrando postagens com marcador python. Mostrar todas as postagens

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

segunda-feira, 10 de maio de 2021

Expressões regulares em python

❝Algumas pessoas, quando confrontadas com um problema, pensam “Eu sei, usarei expressões regulares”. Agora eles tem dois problemas.❞
- Jamie Zawinski

Mergulho

Tirar um pequeno trecho de um grande bloco de texto é um desafio. Em Python, strings têm métodos para pesquisa e substituição: index(), find(), split(), count(), replace(). Mas esses métodos são limitados aos casos mais simples. Por exemplo, o método index() procura uma única substring codificada e a pesquisa sempre diferencia maiúsculas de minúsculas. Para fazer pesquisas que não diferenciam maiúsculas de minúsculas de uma string s, você deve chamar s.lower() ou s.upper() e certificar-se de que suas strings de pesquisa são as maiúsculas e minúsculas apropriadas. Os métodos replace() e split() têm as mesmas limitações.

Se seu objetivo pode ser alcançado com métodos de string, você deve usá-los. Eles são rápidos, simples e fáceis de ler, e há muito a ser dito sobre um código rápido, simples e legível. Mas se você estiver usando várias funções de string diferentes com instruções if para lidar com casos especiais, ou se estiver encadeando chamadas para split() e join() para fatiar e dividir suas strings, pode ser necessário passar para as expressões regulares.

As expressões regulares são uma forma poderosa e (principalmente) padronizada de pesquisar, substituir e analisar texto com padrões complexos de caracteres. Embora a sintaxe da expressão regular seja restrita e diferente do código normal, o resultado pode acabar sendo mais legível do que uma solução enrolada à mão que usa uma longa cadeia de funções de string. Existem até maneiras de incorporar comentários em expressões regulares, para que você possa incluir documentação refinada neles.

Observação

Se você usou expressões regulares em outras linguagens (como Perl, JavaScript ou PHP), a sintaxe do Python será muito familiar. Leia o resumo do módulo re para obter uma visão geral das funções disponíveis e seus argumentos.

Estudo de caso: Nº de endereços de rua

Esta série de exemplos foi inspirada por um problema da vida real que tive em meu trabalho diário há vários anos, quando precisei limpar e padronizar endereços de rua exportados de um sistema legado antes de importá-los para um sistema mais novo. (Veja, eu não apenas invento essas coisas; elas são realmente úteis). Este exemplo mostra como abordei o problema.

>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')                ①
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')                ②
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.')  ③
'100 NORTH BROAD RD.'
>>> import re                               ④
>>> re.sub('ROAD$', 'RD.', s)               ⑤
'100 NORTH BROAD RD.'
  1. Meu objetivo é padronizar um endereço de rua para que 'ROAD' seja sempre abreviado como 'RD.'. À primeira vista, pensei que era simples o suficiente para que pudesse usar apenas o método string replace(). Afinal, todos os dados já estavam em maiúsculas, portanto, as incompatibilidades de maiúsculas e minúsculas não seriam um problema. E a string de pesquisa, 'ROAD', era uma constante. E neste exemplo aparentemente simples, s.replace() realmente funciona.
  2. A vida, infelizmente, está cheia de contra-exemplos, e eu rapidamente descobri este. O problema aqui é que 'ROAD' aparece duas vezes no endereço, uma vez como parte do nome da rua 'BROAD' e outra como sua própria palavra. O método replace() vê essas duas ocorrências e substitui cegamente as duas; enquanto isso, vejo meus endereços sendo destruídos.
  3. Para resolver o problema de endereços com mais de uma substring 'ROAD', você poderia recorrer a algo assim: apenas pesquise e substitua 'ROAD' nos últimos quatro caracteres do endereço (s[-4:]), e deixe a string sozinha (s[:-4]). Mas você pode ver que isso já está ficando complicado. Por exemplo, o padrão depende do comprimento da string que você está substituindo. (Se você estivesse substituindo 'STREET' com 'ST.', você precisaria usar s[:-6] e s[-6:].replace(...)). Você gostaria de voltar em seis meses e depurar isso? Eu sei que não.
  4. É hora de passar para as expressões regulares. Em Python, todas as funcionalidades relacionadas às expressões regulares estão contidas no módulo re.
  5. Dê uma olhada no primeiro parâmetro: 'ROAD$'. Esta é uma expressão regular simples que corresponde 'ROAD' apenas quando ocorre no final de uma string. O $ significa “fim da cadeia.” (Há um caractere correspondente, o circunflexo ^, que significa “início da string”). Usando a função re.sub(), você pesquisa a string s para a expressão regular 'ROAD$' e a substitui por 'RD.'. Corresponde ao ROAD no final da string s, mas não corresponde ao ROAD que é parte da palavra BROAD, porque está no meio de s.

^ corresponde ao início de uma string. $ corresponde ao final de uma string.

Continuando com minha história de limpeza de endereços, logo descobri que o exemplo anterior, correspondendo 'ROAD' no final do endereço, não era bom o suficiente, porque nem todos os endereços incluíam uma designação de rua. Alguns endereços simplesmente terminavam com o nome da rua. Eu fugia com isso na maioria das vezes, mas se o nome da rua fosse 'BROAD', a expressão regular corresponderia 'ROAD' ao final da string como parte da palavra 'BROAD', o que não é o que eu queria.

>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)   ①
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)   ②
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)   ③
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s)  ④
'100 BROAD RD. APT 3'
  1. O que eu realmente queria era encontrar 'ROAD' quando estava no final da string e era sua própria palavra (e não uma parte de alguma palavra maior). Para expressar isso em uma expressão regular, você usa \b, que significa "um limite de palavra deve ocorrer bem aqui." Em Python, isso é complicado pelo fato de que o próprio caractere '\' em uma string deve ser ignorado. Isso às vezes é chamado de praga da barra invertida e é um dos motivos pelos quais as expressões regulares são mais fáceis em Perl do que em Python. Por outro lado, o Perl mistura expressões regulares com outra sintaxe, portanto, se você tiver um bug, pode ser difícil dizer se é um bug na sintaxe ou na sua expressão regular.
  2. Para contornar a praga da barra invertida, você pode usar o que é chamado de string bruta, prefixando a string com a letra r. Isso diz ao Python que nada nesta string deve ser ignorado; '\t' é um caractere de tabulação, mas na verdade r'\t' é o caractere de barra invertida \ seguido pela letra t. Eu recomendo sempre usar strings brutas ao lidar com expressões regulares; caso contrário, as coisas ficam muito confusas muito rapidamente (e as expressões regulares já são confusas o suficiente).
  3. *suspiro* Infelizmente, logo encontrei mais casos que contradiziam minha lógica. Nesse caso, o endereço da rua continha a palavra 'ROAD' como uma palavra inteira sozinha, mas não era no final, porque o endereço tinha um número de apartamento após a designação da rua. Como 'ROAD' não está bem no final da string, não corresponde, então toda a chamada para re.sub() acaba não substituindo nada, e você recebe a string original de volta, que não é o que você quer.
  4. Para resolver este problema, removi o caractere $ e adicionei outro \b. Agora, a expressão regular diz “corresponde 'ROAD' quando é uma palavra inteira sozinha em qualquer lugar da string”, seja no final, no início ou em algum lugar no meio.

Estudo de caso: números romanos

Você provavelmente já viu algarismos romanos, mesmo que não os tenha reconhecido. Você pode tê-los visto em direitos autorais de filmes antigos e programas de televisão (“Copyright MCMXLVI” em vez de “Copyright 1946”), ou nas paredes de dedicação de bibliotecas ou universidades (“estabelecido MDCCCLXXXVIII” em vez de “estabelecido 1888”). Você também pode tê-los visto em esboços e referências bibliográficas. É um sistema de representação de números que realmente data do antigo Império Romano (daí o nome).

Em algarismos romanos, existem sete caracteres que são repetidos e combinados de várias maneiras para representar números.

  • I = 1
  • V = 5
  • X = 10
  • L = 50
  • C = 100
  • D = 500
  • M = 1000

A seguir estão algumas regras gerais para a construção de algarismos romanos:

  • Às vezes, os caracteres são aditivos. I é 1, II é 2 e III é 3. VI é 6 (literalmente, “5 e 1”), VII é 7 e VIII é 8.
  • Os caracteres dezenas (I, X, C, e M) podem ser repetidos até três vezes. Em 4, você precisa subtrair do próximo caractere cincos mais alto. Você não pode representar 4 como IIII; em vez disso, é representado como IV (“1 menor que 5”). 40 é escrito como XL (“10 menos que 50”), 41 como XLI, 42 como XLII, 43 como XLIII e então 44 como XLIV (“10 menos que 50, então 1 menos que 5”).
  • Às vezes, os caracteres são... o oposto de aditivos. Ao colocar certos caracteres antes de outros, você subtrai do valor final. Por exemplo, em 9, você precisa subtrair do próximo caractere de dezenas mais alto: 8 é VIII, mas 9 é IX (“1 menor que 10”), não VIIII (já que o caractere I não pode ser repetido quatro vezes). 90 é XC, 900 é CM.
  • Os cinco caracteres não podem ser repetidos. 10 é sempre representado como X, nunca como VV. 100 é sempre C, nunca LL.
  • Os algarismos romanos são lidos da esquerda para a direita, portanto, a ordem dos caracteres é muito importante. DC é 600; CD é um número completamente diferente ( 400, “100 menor que 500”). CI é 101; IC não é nem mesmo um algarismo romano válido (porque você não pode subtrair 1 diretamente de 100; você precisaria escrevê-lo como XCIX “10menor que 100, então 1 menor que 10”).

Verificando Milhares

O que seria necessário para validar que uma string arbitrária é um numeral romano válido? Vamos pegar um dígito de cada vez. Como os algarismos romanos são sempre escritos da ordem superior para a inferior, vamos começar com a mais alta: a casa dos milhares. Para números 1000 e superiores, os milhares são representados por uma série de caracteres M.

>>> import re
>>> pattern = '^M?M?M?$'        ①
>>> re.search(pattern, 'M')     ②
<_sre.SRE_Match object at 0106FB58>
>>> re.search(pattern, 'MM')    ③
<_sre.SRE_Match object at 0106C290>
>>> re.search(pattern, 'MMM')   ④
<_sre.SRE_Match object at 0106AA38>
>>> re.search(pattern, 'MMMM')  ⑤
>>> re.search(pattern, '')      ⑥
<_sre.SRE_Match object at 0106F4A8>
  1. Esse padrão tem três partes. ^ corresponde ao que segue apenas no início da string. Se isso não fosse especificado, o padrão corresponderia independentemente de onde os caracteres M estivessem, o que não é o que você deseja. Você quer ter certeza de que os caracteres M, se estiverem lá, estão no início da string. M? opcionalmente, corresponde a um único caractere M. Como isso é repetido três vezes, você está combinando de zero a três caracteres M em uma linha. E $ corresponde ao final da string. Quando combinado com o caractere ^ no início, isso significa que o padrão deve corresponder a toda a string, sem outros caracteres antes ou depois dos caracteresb M.
  2. A essência do módulo re é a função search(), que usa uma expressão regular (padrão) e uma string ('M') para tentar fazer a correspondência com a expressão regular. Se uma correspondência for encontrada, search() retorna um objeto que possui vários métodos para descrever a correspondência; se nenhuma correspondência for encontrada, search() retorna None o valor nulo do Python. No momento, tudo o que você se importa é se o padrão corresponde, o que você pode dizer apenas olhando para o valor de retorno de search(). 'M' corresponde a esta expressão regular, porque a primeira correspondências opcionais de M foi encontrada e o segundo e terceiro caracteres opcionais M são ignorados.
  3. 'MM' corresponde porque o primeiro e o segundo caracteres opcionais M correspondem e o terceiro M é ignorado.
  4. 'MMM' corresponde porque todos os três caracteres M correspondem.
  5. 'MMMM' não corresponde. Todos os três caracteres M combinam, mas então a expressão regular insiste no final da string (por causa do caractere $), e a string ainda não termina (por causa do quarto M). Então search() retorna None.
  6. Curiosamente, uma string vazia também corresponde a essa expressão regular, uma vez que todos os caracteres M são opcionais.

Verificando Centenas

? torna um padrão opcional.

A casa das centenas é mais difícil do que a dos milhares, porque existem várias maneiras mutuamente exclusivas de expressá-la, dependendo de seu valor.

  • 100 = C
  • 200 = CC
  • 300 = CCC
  • 400 = CD
  • 500 = D
  • 600 = DC
  • 700 = DCC
  • 800 = DCCC
  • 900 = CM

Portanto, existem quatro padrões possíveis:

  • CM
  • CD
  • De zero a três caracteres C (zero se a casa das centenas for 0)
  • D, seguido de zero a três caracteres C

Os dois últimos padrões podem ser combinados:

  • um opcional D, seguido de zero a três caracteres C

Este exemplo mostra como validar a casa das centenas de um algarismo romano.

>>> import re
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'  ①
>>> re.search(pattern, 'MCM')             ②
<_sre.SRE_Match object at 01070390>
>>> re.search(pattern, 'MD')              ③
<_sre.SRE_Match object at 01073A50>
>>> re.search(pattern, 'MMMCCC')          ④
<_sre.SRE_Match object at 010748A8>
>>> re.search(pattern, 'MCMC')            ⑤
>>> re.search(pattern, '')                ⑥
<_sre.SRE_Match object at 01071D98>
  1. Este padrão começa igual ao anterior, verificando o início da string (^) e depois a casa dos milhares (M?M?M?). Em seguida, ele tem a nova parte, entre parêntesis, que define um conjunto de três padrões mutuamente exclusivos, separados por barras verticais: CM, CD, e D?C?C?C? (o que é um opcional D seguido por zero a três opcionais caracteres C). O analisador de expressão regular verifica cada um desses padrões em ordem (da esquerda para a direita), pega o primeiro que corresponde e ignora o resto.
  2. 'MCM' corresponde porque o primeiro M corresponde, o segundo e o terceiro caracteres M são ignorados e as CMcorrespondências (portanto, os padrões CDe D?C?C?C?nunca são considerados). MCMé a representação numérica romana de 1900.
  3. 'MD' corresponde porque o primeiro M corresponde, o segundo e o terceiro caracteres M são ignorados e o padrão D?C?C?C? corresponde a D (cada um dos três caracteres C são opcionais e são ignorados). MD é a representação em algarismo romano de 1500.
  4. 'MMMCCC' corresponde porque todos os três caracteres M correspondem e o padrão D?C?C?C? corresponde CCC (o D é opcional e é ignorado). MMMCCC é a representação numérica romana de 3300.
  5. 'MCMC' não corresponde. O primeiro Mcorresponde, o segundo e o terceiro caracteres M são ignorados e CM corresponde, mas o $ não corresponde porque você ainda não está no final da string (ainda possui um caractere C sem correspondência). O C que não corresponder como parte do padrão D?C?C?C?, porque o mutuamente exclusivos padrão CM já tem correspondente.
  6. Curiosamente, uma string vazia ainda corresponde a esse padrão, porque todos os caracteres M são opcionais e ignorados, e a string vazia corresponde ao padrão D?C?C?C? em que todos os caracteres são opcionais e ignorados.

Uau! Vê com que rapidez as expressões regulares podem ficar desagradáveis? E você cobriu apenas os milhares e centenas de casas de algarismos romanos. Mas se você seguiu tudo isso, as casas das dezenas e unidades são fáceis, porque são exatamente o mesmo padrão. Mas vamos examinar outra maneira de expressar o padrão.

Usando a sintaxe {n,m}

{1,4} corresponde entre 1 e 4 ocorrências de um padrão.

Na seção anterior, você estava lidando com um padrão em que o mesmo caractere poderia ser repetido até três vezes. Existe outra maneira de expressar isso em expressões regulares, que algumas pessoas consideram mais legíveis. Primeiro, olhe para o método que já usamos no exemplo anterior.

>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')     ①
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MM')    ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMM')   ③
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMMM')  ④
>>>
  1. Isso corresponde ao início da string e, em seguida, ao primeiro opcional M, mas não ao segundo e ao terceiro M (mas está tudo bem porque eles são opcionais) e, em seguida, ao final da string.
  2. Isso corresponde ao início da string e, em seguida, ao primeiro e ao segundo opcionais M, mas não ao terceiro M (mas está tudo bem porque é opcional) e, em seguida, ao final da string.
  3. Isso corresponde ao início da string, aos três opcionais M e ao final da string.
  4. Isso corresponde ao início da string e, em seguida, a todos os três opcionais M, mas não corresponde ao final da string (porque ainda há um M sem correspondência), portanto, o padrão não corresponde e retorna None.
>>> pattern = '^M{0,3}$'        ①
>>> re.search(pattern, 'M')     ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MM')    ③
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMM')   ④
<_sre.SRE_Match object at 0x008EEDA8>
>>> re.search(pattern, 'MMMM')  ⑤
>>>
  1. Este padrão diz: "Combine o início da string, em qualquer lugar de zero a três caracteres M e, a seguir, no final da string." O 0 e o 3 podem ser quaisquer números; se quiser combinar pelo menos um, mas não mais do que três caracteres M, você pode dizer M{1,3}.
  2. Isso corresponde ao início da string, depois a um M entre os três possíveis e, a seguir, ao final da string.
  3. Isso corresponde ao início da string, depois a dois M dos três possíveis e, a seguir, ao final da string.
  4. Isso corresponde ao início da string, depois a três M dos três possíveis e, a seguir, ao final da string.
  5. Isso corresponde ao início da string, depois a três M dos três possíveis, mas não corresponde ao final da string. A expressão regular permite até três caracteres M antes do final da string, mas você tem quatro, portanto, o padrão não corresponde e retorna None.

Verificando Dezenas e Uns

Agora, vamos expandir a expressão regular do numeral romano para cobrir a casa das dezenas e unidades. Este exemplo mostra a verificação de dezenas.

>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
>>> re.search(pattern, 'MCMXL')     ①
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCML')      ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLX')     ③
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXX')   ④
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXX')  ⑤
>>> 
  1. Isso coincide com o início da string, então o primeiro opcional M e, em seguida CM, em seguida XL, em seguida, no final da string. Lembre-se de que a sintaxe (A|B|C) significa “corresponder exatamente a um de A, B ou C”. Você corresponde XL, então você ignorar as escolhas XC e L?X?X?X?, e depois passar para o fim da string. MCMXL é a representação numérica romana de 1940.
  2. Isso corresponde ao início da string, depois ao primeiro opcional M, então CM, então L?X?X?X?. Do L?X?X?X?, ele corresponde ao L e ignora todos os três caracteres X opcionais. Em seguida, você se move para o final da string. MCML é a representação numérica romana de 1950.
  3. Isso corresponde ao início da string, depois ao primeiro opcional e M, a seguir CM, ao opcional L e ao primeiro opcional X, ignora o segundo e o terceiro opcionais X, a seguir, o final da string. MCMLX é a representação numérica romana de 1960.
  4. Isso corresponde ao início da string, depois ao primeiro opcional M, CM depois ao opcional L e aos três caracteres opcionais X e, por fim, ao final da string. MCMLXXX é a representação numérica romana de 1980.
  5. Isso coincide com o início da string, depois com o primeiro M opcional, depois com CM, depois com o L opcional e todos os três caracteres X opcionais e, a seguir, falha em coincidir com o final da string porque ainda há mais um X não contabilizado. Portanto, todo o padrão falha em corresponder e retorna Nenhum. MCMLXXXX não é um numeral romano válido.

(A|B) corresponde ao padrão A ou ao padrão B, mas não aos dois.

A expressão para a casa de uns segue o mesmo padrão. Vou poupar você dos detalhes e mostrar o resultado final.

>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'

Então, como é usar essa sintaxe {n,m} alternativa? Este exemplo mostra a nova sintaxe.

>>> pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
>>> re.search(pattern, 'MDLV')              ①
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMDCLXVI')          ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII')   ③
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'I')                 ④
<_sre.SRE_Match object at 0x008EEB48>
  1. Isso corresponde ao início da string, depois a um dos três caracteres M possíveis e a D?C{0,3}. Disto, ele corresponde ao D opcional e zero dos três caracteres C possíveis. Continuando, ele corresponde a L?X{0,3} combinando o L opcional e zero dos três caracteres X possíveis. Em seguida, ele corresponde a V?I{0,3} combinando o V opcional e zero dos três caracteres I possíveis e, finalmente, o final da string. MDLV é a representação numeral romana de 1555.
  2. Isso corresponde ao início da string, depois a dois dos três caracteres M possíveis, a D?C{0,3} com um D e um dos três caracteres C possíveis; então L?X{0,3} com um L e um dos três caracteres X possíveis; então V?I{0,3} com um V e um dos três caracteres I possíveis; então o fim da string. MMDCLXVI é a representação numérica romana de 2666.
  3. Isso corresponde ao início da string, então três de três caracteres M, então D?C{0,3} com um D e três de três caracteres C; então L?X{0,3} com um L e três dos três caracteres de X; então V? I {0,3} com um V e três dos três caracteres I; então o fim da string. MMMDCCCLXXXVIII é a representação em algarismo romano de 3888 e é o algarismo romano mais longo que você pode escrever sem sintaxe estendida.
  4. Observe de perto. (Sinto-me um mágico. “Observem atentamente, crianças, vou tirar um coelho da cartola”). Isso corresponde ao início da string, depois a zero em três M, depois corresponde a D?C{0,3} ignorando o D opcional e combinando zero de três C, então casa L?X{0,3} ignorando o L opcional e combinando zero de três X, então casa V?I{0,3} pulando o V opcional e combinando um de três I. Em seguida, o fim da string. Uau.

Se você acompanhou tudo isso e entendeu na primeira tentativa, está se saindo melhor do que eu. Agora imagine tentar entender as expressões regulares de outra pessoa, no meio de uma função crítica de um grande programa. Ou até imagine voltar às suas próprias expressões regulares alguns meses depois. Eu fiz isso, e não é uma visão bonita.

Agora vamos explorar uma sintaxe alternativa que pode ajudar a manter suas expressões sustentáveis.

Expressões regulares detalhadas

Até agora, você lidou apenas com o que chamarei de expressões regulares “compactas”. Como você viu, eles são difíceis de ler e, mesmo que você descubra o que um deles faz, não há garantia de que será capaz de entendê-los seis meses depois. O que você realmente precisa é de documentação embutida.

Python permite que você faça isso com algo chamado expressões regulares detalhadas. Uma expressão regular detalhada é diferente de uma expressão regular compacta de duas maneiras:

  • O espaço em branco é ignorado. Espaços, tabulações e retornos de carro não são correspondidos como espaços, tabulações e retornos de carro. Eles não são compatíveis de forma alguma. (Se quiser corresponder a um espaço em uma expressão regular detalhada, você precisará escapar dele colocando uma barra invertida na frente dele).
  • Os comentários são ignorados. Um comentário em uma expressão regular detalhada é como um comentário no código Python: ele começa com um caractere # e vai até o final da linha. Neste caso, é um comentário dentro de uma string de várias linhas em vez de dentro do seu código-fonte, mas funciona da mesma maneira.

Isso ficará mais claro com um exemplo. Vamos revisitar a expressão regular compacta com a qual você está trabalhando e torná-la uma expressão regular detalhada. Este exemplo mostra como.

>>> pattern = '''
    ^                   # beginning of string
    M{0,3}              # thousands - 0 to 3 Ms
    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
                        #            or 500-800 (D, followed by 0 to 3 Cs)
    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
                        #        or 50-80 (L, followed by 0 to 3 Xs)
    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
                        #        or 5-8 (V, followed by 0 to 3 Is)
    $                   # end of string
    '''
>>> re.search(pattern, 'M', re.VERBOSE)                 ①
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE)         ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE)   ③
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'M')                             ④
  1. A coisa mais importante a se lembrar ao usar expressões regulares verbosas é que você precisa passar um argumento extra ao trabalhar com elas: re.VERBOSE é uma constante definida no módulo re que sinaliza que o padrão deve ser tratado como uma expressão regular detalhada. Como você pode ver, esse padrão tem um pouco de espaço em branco (todos eles são ignorados) e vários comentários (todos eles são ignorados). Depois de ignorar o espaço em branco e os comentários, esta é exatamente a mesma expressão regular que você viu na seção anterior, mas é muito mais legível.
  2. Isso corresponde ao início da string, depois a um dos três possíveis M, então CM, L e a três dos três possíveis X, então IX, ao final da string.
  3. Isso corresponde ao início da string, então três de três possíveis M, então D e três de um possível três C, então L e três de um possível três X, então V e três de um possível três I, então o final de a string.
  4. Isso não corresponde. Por quê? Como não tem o sinalizador re.VERBOSE, a função re.search está tratando o padrão como uma expressão regular compacta, com espaços em branco significativos e marcas de hash literais. Python não consegue detectar automaticamente se uma expressão regular é prolixa ou não. Python assume que toda expressão regular é compacta, a menos que você declare explicitamente que ela é detalhada.

Estudo de caso: análise de números de telefone

\d corresponde a qualquer dígito numérico (0–9). \D corresponde a qualquer coisa, exceto dígitos.

Até agora, você se concentrou em combinar padrões inteiros. Ou o padrão corresponde ou não. Mas as expressões regulares são muito mais poderosas do que isso. Quando uma expressão regular faz jogo, você pode escolher partes específicas do mesmo. Você pode descobrir o que combinou onde.

Este exemplo veio de outro problema do mundo real que encontrei, novamente em um trabalho do dia anterior. O problema: analisar um número de telefone americano. O cliente queria poder inserir o número de forma livre (em um único campo), mas depois queria armazenar o código de área, tronco, número e, opcionalmente, um ramal separadamente no banco de dados da empresa. Eu vasculhei a web e encontrei muitos exemplos de expressões regulares que pretendiam fazer isso, mas nenhuma delas era permissiva o suficiente.

Aqui estão os números de telefone de que eu precisava para aceitar:

  • 800-555-1212
  • 800 555 1212
  • 800.555.1212
  • (800) 555-1212
  • 1-800-555-1212
  • 800-555-1212-1234
  • 800-555-1212x1234
  • 800-555-1212 ext. 1234
  • work 1-(800) 555.1212 #1234

Bastante variedade! Em cada um desses casos, preciso saber se o código de área era 800, o tronco era 555 e o restante do número de telefone era 1212. Para quem tem uma extensão, preciso saber se a extensão era 1234.

Vamos trabalhar no desenvolvimento de uma solução para análise de número de telefone. Este exemplo mostra a primeira etapa.

>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$')  ①
>>> phonePattern.search('800-555-1212').groups()             ②
('800', '555', '1212')
>>> phonePattern.search('800-555-1212-1234')                 ③
>>> phonePattern.search('800-555-1212-1234').groups()        ④
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'groups'
  1. Sempre leia as expressões regulares da esquerda para a direita. Este corresponde ao início da string, e então (\d{3}). O que é \d{3}? Bem, \d significa “qualquer dígito numérico” (0 a 9). O {3} no meio “corresponde exatamente a três dígitos numéricos”; é uma variação da sintaxe {n,m} que você viu antes. Colocar tudo entre parênteses significa “combine exatamente três dígitos numéricos, e então lembre-os como um grupo que eu posso pedir mais tarde ”. Em seguida, combine um hífen literal. Em seguida, combine outro grupo de exatamente três dígitos. Em seguida, outro hífen literal. Em seguida, outro grupo de exatamente quatro dígitos. Em seguida, combine o final da string.
  2. Para obter acesso aos grupos que o analisador de expressão regular lembrou ao longo do caminho, use o método groups() no objeto que o método search() retorna. Ele retornará uma tupla de quantos grupos foram definidos na expressão regular. Nesse caso, você definiu três grupos, um com três dígitos, um com três dígitos e um com quatro dígitos.
  3. Essa expressão regular não é a resposta final, porque não trata um número de telefone com um ramal no final. Para isso, você precisará expandir a expressão regular.
  4. E é por isso que você nunca deve “encadear” os métodos search() e groups() no código de produção. Se o método search() não retornar nenhuma correspondência, ele retornará None, não um objeto de correspondência de expressão regular. Chamar None.groups() levanta uma exceção perfeitamente óbvia: None não tem um método groups(). (Claro, é um pouco menos óbvio quando você obtém essa exceção nas profundezas do seu código. Sim, falo por experiência própria aqui).
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$')  ①
>>> phonePattern.search('800-555-1212-1234').groups()              ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800 555 1212 1234')                       ③
>>> 
>>> phonePattern.search('800-555-1212')                            ④
>>>
  1. Esta expressão regular é quase idêntica à anterior. Assim como antes, você corresponde ao início da string, a um grupo lembrado de três dígitos, a um hífen, a um grupo de três dígitos lembrado, a um hífen e a um grupo de quatro dígitos lembrado. A novidade é que você combina outro hífen e um grupo lembrado de um ou mais dígitos e, em seguida, o final da string.
  2. O método groups() agora retorna uma tupla de quatro elementos, uma vez que a expressão regular agora define quatro grupos a serem lembrados.
  3. Infelizmente, essa expressão regular também não é a resposta final, porque presume que as diferentes partes do número de telefone estão separadas por hífens. E se eles estiverem separados por espaços, vírgulas ou pontos? Você precisa de uma solução mais geral para combinar vários tipos diferentes de separadores.
  4. Ops! Essa expressão regular não apenas não faz tudo o que você deseja, como também é um retrocesso, porque agora você não pode analisar números de telefone sem um ramal. Não era isso que você queria; se a extensão estiver lá, você quer saber o que é, mas se não estiver, você ainda quer saber quais são as diferentes partes do número principal.

O próximo exemplo mostra a expressão regular para lidar com separadores entre as diferentes partes do número de telefone.

>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')  ①
>>> phonePattern.search('800 555 1212 1234').groups()  ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212-1234').groups()  ③
('800', '555', '1212', '1234')
>>> phonePattern.search('80055512121234')              ④
>>> 
>>> phonePattern.search('800-555-1212')                ⑤
>>> 
  1. Segure seu chapéu. Você está combinando o início da string, um grupo de três dígitos, então \D+. Que raio é aquilo? Bem, \D corresponde a qualquer caractere, exceto um dígito numérico e + significa “1 ou mais”. Portanto, \D+ corresponde a um ou mais caracteres que não são dígitos. Isso é o que você está usando em vez de um hífen literal, para tentar combinar diferentes separadores.
  2. Usando \D+ em vez de - significa, agora você pode combinar números de telefone onde as partes são separadas por espaços em vez de hifens.
  3. É claro que os números de telefone separados por hífens também funcionam.
  4. Infelizmente, essa ainda não é a resposta final, porque pressupõe que haja um separador. E se o número de telefone for inserido sem nenhum espaço ou hífen?
  5. Ups! Isso ainda não resolveu o problema de exigir extensões. Agora você tem dois problemas, mas pode resolvê-los com a mesma técnica.

O próximo exemplo mostra a expressão regular para lidar com números de telefone sem separadores.

>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①
>>> phonePattern.search('80055512121234').groups()      ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800.555.1212 x1234').groups()  ③
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()        ④
('800', '555', '1212', '')
>>> phonePattern.search('(800)5551212 x1234')           ⑤
  1. A única alteração que você fez desde a última etapa foi alterar todos os + para *. Em vez de \D+ entre as partes do número de telefone, agora você combina em \D*. Lembra que + significa “1 ou mais”? Bem, *significa “zero ou mais”. Portanto, agora você deve ser capaz de analisar números de telefone mesmo quando não há nenhum caractere separador.
  2. Eis que realmente funciona. Por quê? Você combinou o início da string, depois um grupo lembrado de três dígitos (800), depois zero caracteres não numéricos, um grupo lembrado de três dígitos (555), depois zero caracteres não numéricos e um grupo lembrado de quatro dígitos (1212), em seguida, zero caracteres não numéricos, um grupo lembrado de um número arbitrário de dígitos (1234) e, em seguida, o final da string.
  3. Outras variações também funcionam agora: pontos em vez de hífens e um espaço e um x antes da extensão.
  4. Finalmente, você resolveu o outro problema antigo: as extensões são opcionais novamente. Se nenhuma extensão for encontrada, o método groups() ainda retorna uma tupla de quatro elementos, mas o quarto elemento é apenas uma string vazia.
  5. Odeio ser o portador de más notícias, mas você ainda não acabou. Qual é o problema aqui? Há um caractere extra antes do código de área, mas a expressão regular assume que o código de área é a primeira coisa no início da string. Não tem problema, você pode usar a mesma técnica de “zero ou mais caracteres não numéricos” para pular os caracteres iniciais antes do código de área.

O próximo exemplo mostra como lidar com os caracteres principais em números de telefone.

>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①
>>> phonePattern.search('(800)5551212 ext. 1234').groups()                  ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                            ③
('800', '555', '1212', '')
>>> phonePattern.search('work 1-(800) 555.1212 #1234')                      ④
  1. Este é o mesmo que no exemplo anterior, exceto que agora você está combinando \D* zero ou mais caracteres não numéricos antes do primeiro grupo lembrado (o código de área). Observe que você não está se lembrando desses caracteres não numéricos (eles não estão entre parênteses). Se você encontrá-los, simplesmente os pule e comece a lembrar o código de área sempre que chegar a ele.
  2. Você pode analisar o número de telefone com êxito, mesmo com o primeiro parêntese esquerdo antes do código de área. (O parêntese direito após o código de área já foi tratado; é tratado como um separador não numérico e correspondido pelo \D* após o primeiro grupo lembrado).
  3. Apenas uma verificação de sanidade para ter certeza de que você não quebrou nada que funcionava antes. Como os caracteres iniciais são totalmente opcionais, isso corresponde ao início da string, depois a zero caracteres não numéricos, a um grupo lembrado de três dígitos (800), a um caractere não numérico (o hífen) e a um grupo de três lembrado dígitos (555), um caractere não numérico (o hífen), um grupo lembrado de quatro dígitos (1212), zero caracteres não numéricos, um grupo lembrado de zero dígitos e o final da string.
  4. É aqui que as expressões regulares me fazem querer arrancar os olhos com um objeto rombudo. Por que este número de telefone não corresponde? Porque existe um 1 antes do código de área, mas você presumiu que todos os caracteres iniciais antes do código de área eram caracteres não numéricos (\D*). Aargh.

Vamos voltar por um segundo. Até agora, todas as expressões regulares corresponderam desde o início da string. Mas agora você vê que pode haver uma quantidade indeterminada de coisas no início da string que você deseja ignorar. Em vez de tentar combinar tudo apenas para pular, vamos adotar uma abordagem diferente: não combine explicitamente o início da string. Essa abordagem é mostrada no próximo exemplo.

>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()         ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                        ③
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234').groups()                      ④
('800', '555', '1212', '1234')
  1. Observe a falta de ^ nesta expressão regular. Você não está mais correspondendo ao início da string. Não há nada que diga que você precisa combinar toda a entrada com sua expressão regular. O mecanismo de expressão regular fará o trabalho árduo de descobrir onde a string de entrada começa a corresponder e partir daí.
  2. Agora você pode analisar com êxito um número de telefone que inclui caracteres e um dígito à esquerda, além de qualquer número de qualquer tipo de separador em torno de cada parte do número de telefone.
  3. Verificação de sanidade. Isso ainda funciona.
  4. Isso ainda funciona também.

Vê com que rapidez uma expressão regular pode ficar fora de controle? Dê uma olhada rápida em qualquer uma das iterações anteriores. Você pode dizer a diferença entre um e o outro?

Enquanto você ainda entende a resposta final (e é a resposta final; se você descobriu um caso que não resolve, não quero saber sobre isso), vamos escrevê-la como uma expressão regular detalhada, antes você esquece por que fez as escolhas que fez.

>>> phonePattern = re.compile(r'''
        # don't match beginning of string, number can start anywhere
(\d{3})     # area code is 3 digits (e.g. '800')
\D*         # optional separator is any number of non-digits
(\d{3})     # trunk is 3 digits (e.g. '555')
\D*         # optional separator
(\d{4})     # rest of number is 4 digits (e.g. '1212')
\D*         # optional separator
(\d*)       # extension is optional and can be any number of digits
$           # end of string
''', re.VERBOSE)
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()  ①
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')                          ②
('800', '555', '1212', '')
  1. Além de estar espalhada por várias linhas, esta é exatamente a mesma expressão regular da última etapa, portanto, não é surpresa que analise as mesmas entradas.
  2. Verificação final de sanidade. Sim, ainda funciona. Você Terminou.

Resumo

Esta é apenas a ponta do iceberg do que as expressões regulares podem fazer. Em outras palavras, mesmo que você esteja completamente dominado por eles agora, acredite em mim, você não viu nada ainda.

Agora você deve estar familiarizado com as seguintes técnicas:

  • ^ corresponde ao início de uma string.
  • $ corresponde ao final de uma string.
  • \b corresponde a um limite de palavra.
  • \d corresponde a qualquer dígito numérico.
  • \D corresponde a qualquer caractere não numérico.
  • x? corresponde a um caractere x opcional (em outras palavras, corresponde a x zero ou uma vez).
  • x* corresponde a x zero ou mais vezes.
  • x+ corresponde x uma ou mais vezes.
  • x{n,m} corresponde a um caractere x pelo menos n vezes, mas não mais do que m vezes.
  • (a|b|c) corresponde exatamente a um de a, b ou c.
  • (x) em geral, é um grupo lembrado. Você pode obter o valor da correspondência usando o método groups() do objeto retornado por re.search.

As expressões regulares são extremamente poderosas, mas não são a solução correta para todos os problemas. Você deve aprender o suficiente sobre eles para saber quando são apropriados, quando resolverão seus problemas e quando causarão mais problemas do que soluções.

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

domingo, 9 de maio de 2021

Strings em python 3

❝Estou dizendo isso porque você é um dos meus amigos.
Meu alfabeto começa onde seu alfabeto termina!❞
- Dr. Seuss, On Beyond Zebra!

Algumas coisas chatas que você precisa entender antes de mergulhar

Poucas pessoas pensam nisso, mas o texto é incrivelmente complicado. Comece com o alfabeto. O povo de Bougainville possui o menor alfabeto do mundo; seu alfabeto Rotokas é composto de apenas 12 letras: A, E, G, I, K, O, P, R, S, T, U e V. Na outra extremidade do espectro, idiomas como chinês, japonês e O coreano tem milhares de caracteres. O inglês, é claro, tem 26 letras - 52 se você contar maiúsculas e minúsculas separadamente - mais um punhado de ! @ # $% & Sinais de pontuação.

Quando você fala sobre “texto”, provavelmente está pensando em “caracteres e símbolos na tela do meu computador”. Mas os computadores não lidam com caracteres e símbolos; eles lidam com bits e bytes. Cada pedaço de texto que você já viu na tela do computador é, na verdade, armazenado em uma codificação de caracteres específica. A grosso modo, a codificação de caracteres fornece um mapeamento entre o que você vê na tela e o que seu computador realmente armazena na memória e no disco. Existem muitas codificações de caracteres diferentes, algumas otimizadas para idiomas específicos, como russo, chinês ou inglês, e outras que podem ser usadas para vários idiomas.

Na verdade, é mais complicado do que isso. Muitos caracteres são comuns a várias codificações, mas cada codificação pode usar uma sequência diferente de bytes para realmente armazenar esses caracteres na memória ou no disco. Portanto, você pode pensar na codificação de caracteres como uma espécie de chave de descriptografia. Sempre que alguém lhe dá uma sequência de bytes - um arquivo, uma página da web, qualquer coisa - e afirma que é um “texto”, você precisa saber qual codificação de caracteres eles usaram para poder decodificar os bytes em caracteres. Se eles derem a chave errada ou nenhuma chave, você terá a tarefa nada invejável de decifrar o código sozinho. Provavelmente, você errará e o resultado será um jargão.

Tudo o que você pensava que sabia sobre strings está errado.

Certamente você já viu páginas da web como esta, com estranhos caracteres de ponto de interrogação onde deveriam estar apóstrofos. Isso geralmente significa que o autor da página não declarou sua codificação de caracteres corretamente, seu navegador ficou adivinhando e o resultado foi uma mistura de caracteres esperados e inesperados. Em inglês, é simplesmente irritante; em outros idiomas, o resultado pode ser completamente ilegível.

Existem codificações de caracteres para cada idioma principal do mundo. Como cada idioma é diferente e a memória e o espaço em disco são historicamente caros, cada codificação de caractere é otimizada para um idioma específico. Com isso, quero dizer que cada codificação usa os mesmos números (0–255) para representar os caracteres desse idioma. Por exemplo, você provavelmente está familiarizado com a codificação ASCII, que armazena caracteres em inglês como números que variam de 0 a 127. (65 é "A" maiúsculo, 97 é "a" minúsculo. O inglês tem um alfabeto muito simples, para que possa ser totalmente expresso em menos de 128 números. Para aqueles que podem contar na base 2, são 7 dos 8 bits em um byte.

Os idiomas da Europa Ocidental, como francês, espanhol e alemão, têm mais letras do que o inglês. Ou, mais precisamente, eles têm letras combinadas com vários sinais diacríticos, como o caractere ñ em espanhol. A codificação mais comum para esses idiomas é CP-1252, também chamada de “windows-1252” porque é amplamente usada no Microsoft Windows. A codificação CP-1252 compartilha caracteres com ASCII no intervalo 0-127, mas depois se estende para o intervalo 128-255 para caracteres como n-com-um-til sobre ele (241), u-com-dois-pontos sobre ele (252). No entanto, ainda é uma codificação de byte único; o maior número possível, 255, ainda cabe em um byte.

Depois, há idiomas como chinês, japonês e coreano, que têm tantos caracteres que exigem conjuntos de caracteres de bytes múltiplos. Ou seja, cada “caractere” é representado por um número de dois bytes de 0–65535. Mas diferentes codificações multibyte ainda compartilham o mesmo problema que diferentes codificações de um único byte, ou seja, que cada uma usa os mesmos números para significar coisas diferentes. Acontece que a gama de números é mais ampla, porque há muito mais caracteres para representar.

Isso era normal em um mundo sem rede, onde “texto” era algo que você digitava e ocasionalmente imprimia. Não havia muito “texto simples”. O código-fonte foi ASCII, e todos os outros utilizados processadores de texto, que definiram seus próprios formatos (não-texto) que seguiram informações codificação de caracteres, juntamente com um estilo rico. As pessoas liam esses documentos com o mesmo processador de texto do autor original, então tudo funcionava, mais ou menos.

Agora pense no surgimento de redes globais como e-mail e web. Muito “texto simples” voando ao redor do globo, sendo escrito em um computador, transmitido por um segundo computador e recebido e exibido por um terceiro computador. Os computadores só podem ver números, mas os números podem significar coisas diferentes. Ah não! O que fazer? Bem, os sistemas tiveram que ser projetados para transportar informações de codificação junto com cada pedaço de "texto simples". Lembre-se de que é a chave de descriptografia que mapeia números legíveis por computador em caracteres legíveis por humanos. Uma chave de descriptografia ausente significa texto truncado, jargão ou pior.

Agora pense em tentar armazenar vários trechos de texto no mesmo lugar, como na mesma tabela de banco de dados que contém todos os e-mails que você já recebeu. Você ainda precisa armazenar a codificação de caracteres ao lado de cada pedaço de texto para que possa exibi-lo corretamente. Acha que é difícil? Tente pesquisar em seu banco de dados de e-mail, o que significa converter entre várias codificações instantaneamente. Não parece divertido?

Agora pense na possibilidade de documentos multilíngues, onde caracteres de vários idiomas estão próximos uns dos outros no mesmo documento. (Dica: os programas que tentavam fazer isso normalmente usavam códigos de escape para alternar os "modos". Puf, você está no modo koi8-r russo, então 241 significa Я; puf, agora você está no modo grego Mac, então 241 significa ώ.) E, claro, você também desejará pesquisar esses documentos.

Agora chore muito, porque tudo que você pensava que sabia sobre strings está errado, e não existe "texto simples".

Unicode

Digite Unicode.

Unicode é um sistema projetado para representar todos os caracteres de todos os idiomas. Unicode representa cada letra, caractere ou ideograma como um número de 4 bytes. Cada número representa um caractere único usado em pelo menos um dos idiomas do mundo. (Nem todos os números são usados, mas mais de 65535 deles, portanto, 2 bytes não seriam suficientes.) Os caracteres usados em vários idiomas geralmente têm o mesmo número, a menos que haja uma boa razão etimológica para não o fazer. Independentemente disso, há exatamente 1 número por caractere e exatamente 1 caractere por número. Cada número sempre significa apenas uma coisa; não há “modos” para acompanhar. U+0041 é sempre 'A', mesmo que seu idioma não contenha um 'A'.

Diante disso, parece uma ótima ideia. Uma codificação para governar todos eles. Vários idiomas por documento. Não há mais "troca de modo" para alternar entre as codificações no meio do fluxo. Mas, de imediato, a pergunta óbvia deve saltar para você. Quatro bytes? Para cada caractere Isso parece muito desperdício, especialmente para idiomas como Inglês e Espanhol, que precisam de menos de um byte (256 números) para expressar cada caractere possível. Na verdade, é um desperdício até mesmo para idiomas baseados em ideogramas (como o chinês), que nunca precisam de mais de dois bytes por caractere.

Existe uma codificação Unicode que usa quatro bytes por caractere. É chamado de UTF-32, porque 32 bits = 4 bytes. UTF-32 é uma codificação direta; ele pega cada caractere Unicode (um número de 4 bytes) e representa o caractere com esse mesmo número. Isso tem algumas vantagens, a mais importante é que você pode encontrar o enésimo caractere de uma string em tempo constante, porque o enésimo caractere começa no 4 × enésimo byte. Ele também tem várias desvantagens, sendo a mais óbvia que são necessários quatro bytes para armazenar cada caractere estranho.

Mesmo que haja muitos caracteres Unicode, acontece que a maioria das pessoas nunca usará nada além do primeiro 65535. Portanto, existe outra codificação Unicode, chamada UTF-16 (porque 16 bits = 2 bytes). UTF-16 codifica cada caractere de 0-65535 como dois bytes, então usa alguns truques sujos se você realmente precisar representar os caracteres Unicode do "plano astral" raramente usados ​​além de 65535. Vantagem mais óbvia: UTF-16 é duas vezes mais espaço- eficiente como UTF-32, porque cada caractere requer apenas dois bytes para armazenar em vez de quatro bytes (exceto para aqueles que não o fazem). E você ainda pode encontrar facilmente o enésimo caractere de uma string em tempo constante, se assumir que a string não inclui nenhum caractere do plano astral, o que é uma boa suposição até o momento em que não é.

Mas também há desvantagens não óbvias para UTF-32 e UTF-16. Diferentes sistemas de computador armazenam bytes individuais de maneiras diferentes. Isso significa que o caractere U+4E2D pode ser armazenado em UTF-16 como 4E 2D ou 2D 4E, dependendo se o sistema é big-endian ou little-endian. (Para UTF-32, há ainda mais ordens de bytes possíveis). Contanto que seus documentos nunca saiam do computador, você está seguro - aplicativos diferentes no mesmo computador usarão a mesma ordem de bytes. Mas no minuto em que você quiser transferir documentos entre sistemas, talvez em uma rede mundial de computadores de algum tipo, precisará de uma maneira de indicar em qual ordem seus bytes são armazenados. Caso contrário, o sistema receptor não tem como saber se a sequência de dois bytes 4E 2D significa U+4E2D ou U+2D4E.

Para resolver este problema, as codificações Unicode multibyte definem uma "Marca de Ordem de Byte", que é um caractere especial não imprimível que você pode incluir no início do seu documento para indicar em que ordem seus bytes estão. Para UTF-16, a Marca de Ordem de Byte é U+FEFF. Se você receber um documento UTF-16 que começa com os bytes FF FE, saberá que a ordem dos bytes é uma maneira; se começar com FE FF, você sabe que a ordem dos bytes está invertida.

Ainda assim, o UTF-16 não é exatamente ideal, especialmente se você estiver lidando com muitos caracteres ASCII. Se você pensar bem, até mesmo uma página da web chinesa conterá muitos caracteres ASCII - todos os elementos e atributos que cercam os caracteres chineses imprimíveis. Ser capaz de encontrar o enésimo caractere em tempo constante é bom, mas ainda há o problema irritante desses caracteres do plano astral, o que significa que você não pode garantir que cada caractere tenha exatamente dois bytes, então você não pode realmente encontrar o enésimo caractere em tempo constante, a menos que você mantenha um índice separado. E cara, com certeza há muito texto ASCII no mundo...

Outras pessoas ponderaram essas questões e encontraram uma solução:

UTF-8

UTF-8 é um sistema de codificação de comprimento variável para Unicode. Ou seja, caracteres diferentes ocupam um número diferente de bytes. Para ASCII caracteres (AZ). UTF-8 usa apenas um byte por caractere. Na verdade, ele usa exatamente os mesmos bytes; os primeiros 128 caracteres (0–127) em UTF-8 são indistinguíveis de ASCII. Os caracteres “latinos estendidos” como ñ e ö acabam ocupando dois bytes. (Os bytes não são simplesmente o ponto de código Unicode como seriam em UTF-16; há algumas mudanças sérias de bits envolvidas). Caracteres chineses como 中 acabam ocupando três bytes. Os caracteres raramente usados do “plano astral” ocupam quatro bytes.

Desvantagens: como cada caractere pode ter um número diferente de bytes, encontrar o enésimo caractere é uma operação O(N) - ou seja, quanto mais longa a string, mais tempo leva para encontrar um caractere específico. Além disso, há um ajuste de bits envolvido para codificar caracteres em bytes e decodificar bytes em caracteres.

Vantagens: codificação supereficiente de caracteres ASCII comuns. Não é pior do que UTF-16 para caracteres latinos estendidos. Melhor do que UTF-32 para caracteres chineses. Além disso (e você terá que confiar em mim nisso, porque não vou mostrar a matemática), devido à natureza exata da manipulação de bits, não há problemas de ordenação de bytes. Um documento codificado em UTF-8 usa exatamente o mesmo fluxo de bytes em qualquer computador.

Mergulho

No Python 3, todas as strings são sequências de caracteres Unicode. Não existe uma string Python codificada em UTF-8, ou uma string Python codificada como CP-1252. “Esta string é UTF-8?” é uma pergunta inválida. UTF-8 é uma forma de codificar caracteres como uma sequência de bytes. Se você quiser pegar uma string e transformá-la em uma sequência de bytes em uma codificação de caracteres específica, o Python 3 pode ajudá-lo com isso. Se você quiser pegar uma sequência de bytes e transformá-la em uma string, o Python 3 pode ajudá-lo com isso também. Bytes não são caracteres; bytes são bytes. Caracteres são uma abstração. Uma string é uma sequência dessas abstrações.

>>> s = '深入 Python'    ①
>>> len(s)               ②
9
>>> s[0]                 ③
'深'
>>> s + ' 3'             ④
'深入 Python 3'
  1. Para criar uma string, coloque-a entre aspas. As strings Python podem ser definidas com aspas simples (') ou aspas duplas (").
  2. A função len() interna retorna o comprimento da string, ou seja, o número de caracteres. Esta é a mesma função que você usa para encontrar o comprimento de uma lista, tupla, conjunto ou dicionário. Uma string é como uma tupla de caracteres.
  3. Assim como obter itens individuais de uma lista, você pode obter caracteres individuais de uma string usando a notação de índice.
  4. Assim como nas listas, você pode concatenar strings usando o operador +.

Formatando Strings

As strings podem ser definidas com aspas simples ou duplas.

Vamos dar outra olhada em humansize.py:

SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],         ①
        1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}

def approximate_size(size, a_kilobyte_is_1024_bytes=True):
    '''Convert a file size to human-readable form.                          ②

    Keyword arguments:
    size -- file size in bytes
    a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
                                if False, use multiples of 1000

    Returns: string

    '''                                                                     ③
    if size < 0:
        raise ValueError('number must be non-negative')                     ④

    multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
    for suffix in SUFFIXES[multiple]:
        size /= multiple
        if size < multiple:
            return '{0:.1f} {1}'.format(size, suffix)                       ⑤

    raise ValueError('number too large')
  1. 'KB', 'MB', 'GB'... esses são cada strings.
  2. As docstrings de função são strings. Esta docstring abrange várias linhas, portanto, ela usa três aspas em uma linha para iniciar e terminar a string.
  3. Essas três aspas em uma linha encerram a docstring.
  4. Há outra string, sendo passada para a exceção como uma mensagem de erro legível.
  5. Há um... uau, o que diabos é isso?

Python 3 oferece suporte à formatação de valores em strings. Embora isso possa incluir expressões muito complicadas, o uso mais básico é inserir um valor em uma string com um único espaço reservado.

>>> username = 'mark'
>>> password = 'PapayaWhip'                             ①
>>> "{0}'s password is {1}".format(username, password)  ②
"mark's password is PapayaWhip"
  1. Não, minha senha não é realmente PapayaWhip.
  2. Há muita coisa acontecendo aqui. Primeiro, essa é uma chamada de método em um literal de string. Strings são objetos e objetos têm métodos. Em segundo lugar, toda a expressão é avaliada como uma string. Terceiro, {0} e {1} são campos de substituição, que são substituídos pelos argumentos passados ​​para o método format().

Nomes de campos compostos

O exemplo anterior mostra o caso mais simples, onde os campos de substituição são simplesmente inteiros. Os campos de substituição de inteiros são tratados como índices posicionais na lista de argumentos do método format(). Isso significa que {0} é substituído pelo primeiro argumento (username, neste caso), {1} é substituído pelo segundo argumento (password). Você pode ter tantos índices posicionais quantos argumentos e quantos argumentos quiser. Mas os campos de substituição são muito mais poderosos do que isso.

>>> import humansize
>>> si_suffixes = humansize.SUFFIXES[1000]      ①
>>> si_suffixes
['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
>>> '1000{0[0]} = 1{0[1]}'.format(si_suffixes)  ②
'1000KB = 1MB'
  1. Em vez de chamar qualquer função no módulo humansize, você está apenas pegando uma das estruturas de dados que ela define: a lista de sufixos “SI” (potências de 1000).
  2. Parece complicado, mas não é. {0} faria referência ao primeiro argumento passado ao método format(), si_suffixes. Mas si_suffixes é uma lista. Assim, {0[0]} refere-se ao primeiro item da lista que é o primeiro argumento passado para o método format(): 'KB'. Enquanto isso, {0[1]} refere-se ao segundo item da mesma lista: 'MB'. Tudo fora das chaves - incluindo 1000, o sinal de igual e os espaços - permanece intocado. O resultado final é a string '1000KB = 1MB'.

{0} é substituído pelo primeiro argumento de format(). {1} é substituído pelo 2º.

O que este exemplo mostra é que os especificadores de formato podem acessar itens e propriedades de estruturas de dados usando (quase) a sintaxe Python . Isso é chamado de nomes de campos compostos . Os seguintes nomes de campos compostos “simplesmente funcionam”:

  • Passar uma lista e acessar um item da lista por índice (como no exemplo anterior).
  • Passando um dicionário e acessando um valor do dicionário por chave.
  • Passar um módulo e acessar suas variáveis e funções por nome.
  • Passar uma instância de classe e acessar suas propriedades e métodos por nome.
  • Qualquer combinação das opções acima

Só para te impressionar, aqui está um exemplo que combina todas as opções acima:

>>> import humansize
>>> import sys
>>> '1MB = 1000{0.modules[humansize].SUFFIXES[1000][0]}'.format(sys)
'1MB = 1000KB'

Funciona assim:

  • O módulo sys contém informações sobre a instância do Python em execução no momento. Como você acabou de importá-lo, pode passar o próprio módulo sys como um argumento para o método format(). Portanto, o campo de substituição {0} refere-se ao módulo sys.
  • sys.modules é um dicionário de todos os módulos que foram importados nesta instância Python. As chaves são os nomes dos módulos como strings; os valores são os próprios objetos do módulo. Portanto, o campo de substituição {0.modules} refere-se ao dicionário de módulos importados.
  • sys.modules['humansize'] é o módulo humansize que você acabou de importar. O campo de substituição {0.modules[humansize]} refere-se ao módulo humansize. Observe a ligeira diferença de sintaxe aqui. No código Python real, as chaves do dicionário sys.modules são strings; para se referir a eles, você precisa colocar aspas ao redor do nome do módulo (por exemplo 'humansize'). Mas dentro de um campo de substituição, você pula as aspas em torno do nome da chave do dicionário (por exemplo humansize). Para citar o PEP 3101: Advanced String Formatting, “As regras para analisar uma chave de item são muito simples. Se começar com um dígito, é tratado como um número, caso contrário, é usado como uma string”.
  • sys.modules['humansize'].SUFFIXES é o dicionário definido na parte superior do módulo humansize. O campo de substituição {0.modules[humansize].SUFFIXES} refere-se a esse dicionário.
  • sys.modules['humansize'].SUFFIXES[1000] é uma lista de sufixos SI: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']. Portanto, o campo de substituição {0.modules[humansize].SUFFIXES[1000]} se refere a essa lista.
  • sys.modules['humansize'].SUFFIXES[1000][0] é o primeiro item da lista de sufixos SI: 'KB'. Portanto, o campo de substituição completo {0.modules[humansize].SUFFIXES[1000][0]} é substituído pela sequência de dois caracteres KB.

Especificadores de formato

Mas espere! Tem mais! Vamos dar uma outra olhada nessa estranha linha de código de humansize.py:

if size < multiple:
    return '{0:.1f} {1}'.format(size, suffix)

{1} é substituído pelo segundo argumento passado ao método format(), que é o suffix. Mas o que é {0:.1f}? São duas coisas: {0} que você reconhece e :.1f que não. A segunda metade (incluindo e após os dois pontos) define o especificador de formato, que refina ainda mais como a variável substituída deve ser formatada.

Observação

Os especificadores de formato permitem que você misture o texto de substituição de várias maneiras úteis, como a função printf() em C. Você pode adicionar zero ou espaçamento, alinhar strings, controlar a precisão decimal e até mesmo converter números em hexadecimais.

Em um campo de substituição, dois pontos (:) marcam o início do especificador de formato. O especificador de formato “.1” significa “arredondar para o décimo mais próximo” (ou seja, exibir apenas um dígito após a vírgula decimal). O especificador de formato “f” significa “número de ponto fixo” (em oposição à notação exponencial ou alguma outra representação decimal). Assim, dado um size de 698.24 e suffix de 'GB', a string formatada seria '698.2 GB', porque 698.24 é arredondado para uma casa decimal, então o sufixo é anexado após o número.

>>> '{0:.1f} {1}'.format(698.24, 'GB')
'698.2 GB'

Para todos os detalhes sangrentos sobre especificadores de formato, consulte a Minilinguagem de Especificação de Formato na documentação oficial do Python.

Outros métodos de string comuns

Além da formatação, as strings podem fazer vários outros truques úteis.

>>> s = '''Finished files are the re-  ①
... sult of years of scientif-
... ic study combined with the
... experience of years.'''
>>> s.splitlines()                     ②
['Finished files are the re-',
 'sult of years of scientif-',
 'ic study combined with the',
 'experience of years.']
>>> print(s.lower())                   ③
finished files are the re-
sult of years of scientif-
ic study combined with the
experience of years.
>>> s.lower().count('f')               ④
6
  1. Você pode inserir strings de várias linhas no shell interativo do Python. Depois de iniciar uma string de várias linhas com aspas triplas, apenas pressione ENTER e o shell interativo solicitará que você continue a string. Digitar as aspas triplas de fechamento encerra a string e o próximo ENTER executará o comando (neste caso, atribuindo a string a s).
  2. O método splitlines() pega uma string de várias linhas e retorna uma lista de strings, uma para cada linha do original. Observe que os retornos de carro no final de cada linha não estão incluídos.
  3. O método lower() converte toda a string em minúsculas. (Da mesma forma, o método upper() converte uma string em maiúsculas).
  4. O método count() conta o número de ocorrências de uma substring. Sim, realmente existem seis “f”s nessa frase!

Aqui está outro caso comum. Digamos que você tenha uma lista de pares de valores-chave no formato key1=value1&key2=value2 e queira dividi-los e fazer um dicionário no formato {key1: value1, key2: value2}.

>>> query = 'user=pilgrim&database=master&password=PapayaWhip'
>>> a_list = query.split('&')                                        ①
>>> a_list
['user=pilgrim', 'database=master', 'password=PapayaWhip']
>>> a_list_of_lists = [v.split('=', 1) for v in a_list if '=' in v]  ②
>>> a_list_of_lists
[['user', 'pilgrim'], ['database', 'master'], ['password', 'PapayaWhip']]
>>> a_dict = dict(a_list_of_lists)                                   ③
>>> a_dict
{'password': 'PapayaWhip', 'user': 'pilgrim', 'database': 'master'}
  1. O método split() da string tem um argumento obrigatório, um delimitador. O método divide uma string em uma lista de strings com base no delimitador. Aqui, o delimitador é um caractere e comercial, mas pode ser qualquer coisa.
  2. Agora temos uma lista de strings, cada uma com uma chave, seguida por um sinal de igual, seguida por um valor. Podemos usar uma compreensão de lista para iterar por toda a lista e dividir cada string em duas strings com base no primeiro sinal de igual. O segundo argumento opcional para o método split() é o número de vezes que você deseja dividir. 1 significa “dividir apenas uma vez”, portanto, o método split() retornará uma lista de dois itens. (Em teoria, um valor também pode conter um sinal de igual. Se você acabou de usar 'key=value=foo'.split('='), acabará com uma lista de três itens ['key', 'value', 'foo']).
  3. Por fim, o Python pode transformar essa lista de listas em um dicionário simplesmente passando-a para a função dict().

Observação

O exemplo anterior se parece muito com a análise de parâmetros de consulta numa URL, mas a análise de URL na vida real é, na verdade, mais complicada do que isso. Se você estiver lidando com parâmetros de consulta de URL, é melhor usar a função urllib.parse.parse_qs(), que lida com alguns casos extremos não óbvios.

Cortando uma string

Depois de definir uma string, você pode obter qualquer parte dela como uma nova string. Isso é chamado de fatiar uma string. Fatiar (slicing) strings funciona exatamente da mesma forma que fatiar listas, o que faz sentido, porque strings são apenas sequências de caracteres.

>>> a_string = 'My alphabet starts where your alphabet ends.'
>>> a_string[3:11]           ①
'alphabet'
>>> a_string[3:-3]           ②
'alphabet starts where your alphabet en'
>>> a_string[0:2]            ③
'My'
>>> a_string[:18]            ④
'My alphabet starts'
>>> a_string[18:]            ⑤
' where your alphabet ends.'
  1. Você pode obter uma parte de uma string, chamada de “fatia”, especificando dois índices. O valor de retorno é uma nova string contendo todos os caracteres da string, em ordem, começando com o índice da primeira fatia.
  2. Como listas de fatiamento, você pode usar índices negativos para fatiar strings.
  3. As strings são baseadas em zero, portanto, a_string[0:2] retorna os dois primeiros itens da string, começando em a_string[0], até, mas não incluindo a_string[2].
  4. Se o índice da fatia esquerda for 0, você pode deixá-lo de fora e 0 está implícito. Então a_string[:18] é o mesmo que a_string[0:18], porque o 0 inicial está implícito.
  5. Da mesma forma, se o índice de fatia correto for o comprimento da string, você pode deixá-lo de fora. Então a_string[18:] é o mesmo que a_string[18:44], porque esta string tem 44 caracteres. Há uma simetria agradável aqui. Nesta sequência de 44 caracteres, a_string[:18] retorna os primeiros 18 caracteres e a_string[18:] retorna tudo, exceto os primeiros 18 caracteres. Na verdade, a_string[:n] sempre retornará os primeiros n caracteres e a_string[n:] retornará o resto, independentemente do comprimento da string.

Strings vs. Bytes

Bytes são bytes; os caracteres são uma abstração. Uma sequência imutável de caracteres Unicode é chamada de string. Uma sequência imutável de números entre 0 e 255 é chamada de objeto de bytes.

>>> by = b'abcd\x65'  ①
>>> by
b'abcde'
>>> type(by)          ②
<class 'bytes'>
>>> len(by)           ③
5
>>> by += b'\xff'     ④
>>> by
b'abcde\xff'
>>> len(by)           ⑤
6
>>> by[0]             ⑥
97
>>> by[0] = 102       ⑦
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment
  1. Para definir um objecto bytes, b'' utilizar a sintaxe “byte literal”. Cada byte dentro do literal de byte pode ser um caractere ASCII ou um número hexadecimal codificado de \x00 a \xff (0-255).
  2. O tipo de objeto bytes é bytes.
  3. Assim como listas e strings, você pode obter o comprimento de um objeto bytes com a função interna len().
  4. Assim como listas e strings, você pode usar o operador + para concatenar objetos bytes. O resultado é um novo objeto bytes.
  5. Concatenar um objeto bytes de 5 bytes e um objeto de 1 byte bytesfornece um objeto bytes de 6 bytes.
  6. Assim como listas e strings, você pode usar a notação de índice para obter bytes individuais em um objeto bytes. Os itens de uma string são strings; os itens de um objeto bytes são inteiros. Especificamente, números inteiros entre 0–255.
  7. Um objeto bytes é imutável; você não pode atribuir bytes individuais. Se precisar alterar bytes individuais, você pode usar o fatiamento de string e os operadores de concatenação (que funcionam da mesma forma que as strings) ou pode converter o objeto bytes em um objeto bytearray.
>>> by = b'abcd\x65'
>>> barr = bytearray(by)  ①
>>> barr
bytearray(b'abcde')
>>> len(barr)             ②
5
>>> barr[0] = 102         ③
>>> barr
bytearray(b'fbcde')
  1. Para converter um objeto bytes em um objeto mutável bytearray, use a função interna bytearray().
  2. Todos os métodos e operações que você pode fazer em um objeto bytes, você pode fazer em um objeto bytearray também.
  3. A única diferença é que, com o objeto bytearray, você pode atribuir bytes individuais usando a notação de índice. O valor atribuído deve ser um número inteiro entre 0–255.

A única coisa que você nunca pode fazer é misturar bytes e strings.

>>> by = b'd'
>>> s = 'abcde'
>>> by + s                       ①
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't concat bytes to str
>>> s.count(by)                  ②
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'bytes' object to str implicitly
>>> s.count(by.decode('ascii'))  ③
1
  1. Você não pode concatenar bytes e strings. Eles são dois tipos de dados diferentes.
  2. Você não pode contar as ocorrências de bytes em uma string, porque não há bytes em uma string. Uma string é uma sequência de caracteres. Talvez você quisesse dizer “contar as ocorrências da string que obteria após decodificar essa sequência de bytes em uma codificação de caracteres específica”? Bem, então você precisa dizer isso explicitamente. Python 3 não converterá implicitamente bytes em strings ou strings em bytes.
  3. Por uma coincidência incrível, esta linha de código diz "conte as ocorrências da string que você obteria após decodificar esta sequência de bytes nesta codificação de caractere específica".

E aqui está o link entre strings e bytes: os objetos bytes têm um método decode() que recebe uma codificação de caracteres e retorna uma string, e as strings têm um método encode() que recebe uma codificação de caracteres e retorna um objeto bytes. No exemplo anterior, a decodificação foi relativamente direta - convertendo uma sequência de bytes na codificação ASCII em uma string de caracteres. Mas o mesmo processo funciona com qualquer codificação que suporte os caracteres da string - mesmo codificações legadas (não Unicode).

>>> a_string = '深入 Python'         ①
>>> len(a_string)
9
>>> by = a_string.encode('utf-8')    ②
>>> by
b'\xe6\xb7\xb1\xe5\x85\xa5 Python'
>>> len(by)
13
>>> by = a_string.encode('gb18030')  ③
>>> by
b'\xc9\xee\xc8\xeb Python'
>>> len(by)
11
>>> by = a_string.encode('big5')     ④
>>> by
b'\xb2`\xa4J Python'
>>> len(by)
11
>>> roundtrip = by.decode('big5')    ⑤
>>> roundtrip
'深入 Python'
>>> a_string == roundtrip
True
  1. Isso é uma string. Possui nove caracteres.
  2. Este é um objeto bytes. Possui 13 bytes. É a sequência de bytes que você obtém quando pega a_string e a codifica em UTF-8.
  3. Este é um objeto bytes. Possui 11 bytes. É a sequência de bytes que você obtém quando pega a_string e a codifica em GB18030.
  4. Este é um objeto bytes. Possui 11 bytes. É uma sequência de bytes totalmente diferente que você obtém quando pega a_string e a codifica em Big5.
  5. Isso é uma string. Possui nove caracteres. É a sequência de caracteres que você começa quando você toma by e decodificá-lo usando a codificação algoritmo Big5. É idêntico ao string original.

PostScript: codificação de caracteres do código-fonte do Python

Python 3 assume que seu código-fonte -  ou seja, cada arquivo .py - está codificado em UTF-8.

Observação

No Python 2, a codificação padrão para arquivos .py era ASCII. No Python 3, a codificação padrão é UTF-8.

Se quiser usar uma codificação diferente em seu código Python, você pode colocar uma declaração de codificação na primeira linha de cada arquivo. Esta declaração define um arquivo .py como windows-1252:

# -*- coding: windows-1252 -*-

Tecnicamente, a substituição da codificação de caracteres também pode estar na segunda linha, se a primeira linha for um comando hash-bang semelhante ao UNIX.

#!/usr/bin/python3
# -*- coding: windows-1252 -*-

Para obter mais informações, consulte PEP 263: Definindo Python Source Code Encodings.

Leitura Adicional

Em Unicode em Python:

No Unicode em geral:

Na codificação de caracteres em outros formatos:

Em strings e formatação de strings:

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