❝Oriente é Oriente e Ocidente é Ocidente, e nunca os dois se encontrarão.❞
- Rudyard Kipling
Mergulho
Os iteradores são o “molho secreto” do Python 3. Eles estão por toda parte, por trás de tudo, sempre fora de vista. As compreensões são apenas uma forma simples de iteradores. Os geradores são apenas uma forma simples de iteradores. Uma função que produz valores (yield
é uma maneira compacta e agradável de construir um iterador sem construir um iterador.
Lembra do gerador Fibonacci? Aqui está como um iterador criado do zero:
class Fib:
'''iterator that yields numbers in the Fibonacci sequence'''
def __init__(self, max):
self.max = max
def __iter__(self):
self.a = 0
self.b = 1
return self
def __next__(self):
fib = self.a
if fib > self.max:
raise StopIteration
self.a, self.b = self.b, self.a + self.b
return fib
Vamos pegar uma linha de cada vez.
class Fib:
class
? O que é uma class?
Definindo Classes
Python é totalmente orientado a objetos: você pode definir suas próprias classes, herdar de suas próprias classes ou classes internas e instanciar as classes que você definiu.
Definir uma classe em Python é simples. Tal como acontece com as funções, não existe uma definição de interface separada. Apenas defina a classe e comece a codificar. Uma classe Python começa com a palavra reservada class
, seguida pelo nome da classe. Tecnicamente, isso é tudo o que é necessário, já que uma classe não precisa herdar de nenhuma outra classe.
class PapayaWhip: ①
pass ②
-
O nome desta classe é
PapayaWhip
e não herda de nenhuma outra classe. Os nomes das classes geralmente são escritos em maiúscula,EachWordLikeThis
mas isso é apenas uma convenção, não um requisito. -
Você provavelmente adivinhou isso, mas tudo em uma classe é indentado, assim como o código em uma função, instrução
if
, loopfor
ou qualquer outro bloco de código. A primeira linha não identada está fora da classe.
Esta classe PapayaWhip
não define nenhum método ou atributo, mas sintaticamente, deve haver algo na definição, portanto, a instrução pass
. Esta é uma palavra reservada do Python que significa apenas “siga em frente, nada para ver aqui”. É uma instrução que não faz nada e é um bom marcador de posição quando você está removendo funções ou classes.
Observação
A instrução pass
em Python é como um conjunto vazio de chaves ({}
) em Java ou C.
Muitas classes são herdadas de outras classes, mas esta não é. Muitas classes definem métodos, mas esta não. Não há nada que uma classe Python absolutamente deva ter, exceto um nome. Em particular, os programadores C++ podem achar estranho que as classes Python não tenham construtores e destruidores explícitos. Embora não seja obrigatório, as classes Python podem ter algo semelhante a um construtor: o método __init__()
.
O Método __init__()
Este exemplo mostra a inicialização da classe Fib
usando o método __init__
.
class Fib:
'''iterator that yields numbers in the Fibonacci sequence''' ①
def __init__(self, max): ②
-
As classes também podem (e devem) ter
docstring
s, assim como módulos e funções. -
O método
__init__()
é chamado imediatamente após a criação de uma instância da classe. Seria tentador - mas tecnicamente incorreto - chamá-lo de “construtor” da classe. É tentador porque se parece com um construtor C++ (por convenção, o método__init__()
é o primeiro método definido para a classe), age como um (é a primeira parte do código executado em uma instância recém-criada da classe) e até mesmo soa como um. Incorreto, porque o objeto já foi construído no momento em que o método__init__()
é chamado e você já tem uma referência válida para a nova instância da classe.
O primeiro argumento de cada método de classe, incluindo o método __init__()
, é sempre uma referência à instância atual da classe. Por convenção, esse argumento é denominado self
. Este argumento preenche a função da palavra reservada this
em C++ ou Java, mas self
não é uma palavra reservada em Python, apenas uma convenção de nomenclatura. No entanto, por favor, não chame de nada além de self
; esta é uma convenção muito forte.
Em todos os métodos de classe, self
se refere à instância cujo método foi chamado. Mas, no caso específico do método __init__()
, a instância cujo método foi chamado também é o objeto recém-criado. Embora seja necessário especificar self
explicitamente ao definir o método, você não o especifica ao chamar o método; Python irá adicioná-lo para você automaticamente.
Instanciando uma classe
Instanciar classes em Python é simples. Para instanciar uma classe, basta chamar a classe como se fosse uma função, passando os argumentos que o método __init__()
requer. O valor de retorno será o objeto recém-criado.
>>> import fibonacci2
>>> fib = fibonacci2.Fib(100) ①
>>> fib ②
<fibonacci2.Fib object at 0x00DB8810>
>>> fib.__class__ ③
<class 'fibonacci2.Fib'>
>>> fib.__doc__ ④
'iterator that yields numbers in the Fibonacci sequence'
-
Você está criando uma instância da classe
Fib
(definida no módulofibonacci2
) e atribuindo a instância recém-criada à variávelfib
. Você está passando um parâmetro,100
que vai acabar como o argumentomax
no método__init__()
deFib
. -
fib
agora é uma instância da classeFib
. -
Cada instância de classe tem um atributo embutido
__class__
, que é a classe do objeto. Os programadores Java podem estar familiarizados com a classeClass
, que contém métodos comogetName()
egetSuperclass()
para obter informações de metadados sobre um objeto. Em Python, esse tipo de metadado está disponível por meio de atributos, mas a ideia é a mesma. -
Você pode acessar a instância da
docstring
da mesma forma que com uma função ou um módulo. Todas as instâncias de uma classe compartilham a mesmadocstring
.
Observação
Em Python, simplesmente chame uma classe como se fosse uma função para criar uma nova instância da classe. Não há new
operador explícito como em C ++ ou Java.
Variáveis de instância
Para a próxima linha:
class Fib:
def __init__(self, max):
self.max = max ①
-
O que é
self.max
? É uma variável de instância. É completamente separado demax
, que foi passado para o método__init__()
como um argumento.self.max
é “global” para a instância. Isso significa que você pode acessá-lo de outros métodos.
class Fib:
def __init__(self, max):
self.max = max ①
.
.
.
def __next__(self):
fib = self.a
if fib > self.max: ②
-
self.max
é definido no__init__()
método ... -
… E referenciado no método
__next__()
.
Variáveis de instância são específicas para uma instância de uma classe. Por exemplo, se você criar duas instâncias Fib
com valores máximos diferentes, cada uma se lembrará de seus próprios valores.
>>> import fibonacci2
>>> fib1 = fibonacci2.Fib(100)
>>> fib2 = fibonacci2.Fib(200)
>>> fib1.max
100
>>> fib2.max
200
Um Iterador Fibonacci
Agora você está pronto para aprender como construir um iterador. Um iterador é apenas uma classe que define um método __iter__()
.
Todos os três destes métodos de classe,
__init__
,__iter__
, e__next__
, começam e terminam com um par de sublinhado(_
) caracteres. Por que isso? Não há nada de mágico nisso, mas geralmente indica que esses são "métodos especiais". A única coisa “especial” sobre métodos especiais é que eles não são chamados diretamente; O Python os chama quando você usa alguma outra sintaxe na classe ou uma instância da classe. Mais sobre métodos especiais.
class Fib: ①
def __init__(self, max): ②
self.max = max
def __iter__(self): ③
self.a = 0
self.b = 1
return self
def __next__(self): ④
fib = self.a
if fib > self.max:
raise StopIteration ⑤
self.a, self.b = self.b, self.a + self.b
return fib ⑥
-
Para construir um iterador do zero,
Fib
precisa ser uma classe, não uma função. -
“Chamar”
Fib(max)
é realmente criar uma instância dessa classe e chamar seu método__init__()
passando o valormax
. O método__init__()
salva o valor máximo como uma variável de instância para que outros métodos possam se referir a ele posteriormente. -
O método
__iter__()
é chamado sempre que alguém faz uma chamadaiter(fib)
. (Como você verá em um minuto, um loopfor
o chamará automaticamente, mas você também pode chamá-lo manualmente). Depois de realizar a inicialização do início da iteração (neste caso, redefinirself.a
eself.b
, nossos dois contadores), o método__iter__()
pode retornar qualquer objeto que implemente um método__next__()
. Nesse caso (e na maioria dos casos),__iter__()
simplesmente retornaself
, já que essa classe implementa seu próprio método__next__()
. -
O método
__next__()
é chamado sempre que alguém chamanext()
um iterador de uma instância de uma classe. Isso fará mais sentido em um minuto. -
Quando o método
__next__()
gera uma exceçãoStopIteration
, isso sinaliza ao chamador que a iteração se esgotou. Ao contrário da maioria das exceções, isso não é um erro; é uma condição normal que significa apenas que o iterador não tem mais valores para gerar. Se o chamador for um loopfor
, ele notará essa exceçãoStopIteration
e sairá normalmente do loop. (Em outras palavras, ele engolirá a exceção). Esse pouco de mágica é na verdade a chave para usar iteradores em loopsfor
. -
Para cuspir o próximo valor, o método
__next __()
de um iterador simplesmente retorna o valor. Não useyield
aqui; isso é um pouco de açúcar sintático que só se aplica quando você está usando geradores. Aqui você está criando seu próprio iterador do zero; use em seu lugarreturn
.
Completamente confuso ainda? Excelente. Vamos ver como chamar este iterador:
>>> from fibonacci2 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
Ora, é exatamente o mesmo! Byte por byte idêntico ao que você chamou de Fibonacci-as-a-generator (módulo uma letra maiúscula). Mas como?
Há um pouco de magia envolvida em loops for
. Aqui está o que acontece:
-
O loop
for
chamaFib(1000)
, conforme mostrado. Isso retorna uma instância da classeFib
. Chame isso defib_inst
. -
Secretamente, e muito habilmente, o loop
for
chamaiter(fib_inst)
, que retorna um objeto iterador. Chame isso defib_iter
. Nesse caso,fib_iter
==fib_inst
, porque o método__iter__()
retornaself
, mas o loopfor
não sabe (ou se importa) com isso. -
Para “percorrer” o iterador, o loop
for
chamanext(fib_iter)
, que chama o método__next__()
no objetofib_iter
, que faz os cálculos do próximo número de Fibonacci e retorna um valor. O loopfor
pega esse valor e o atribui an
, depois executa o corpo do loopfor
para esse valor den
. -
Como o loop
for
sabe quando parar? Estou feliz que você perguntou! Quandonext(fib_iter)
gera uma exceçãoStopIteration
, o loopfor
engolirá a exceção e sairá normalmente. (Qualquer outra exceção passará e será gerada como de costume). E onde você viu uma exceçãoStopIteration
? No método__next__()
, é claro!
Um iterador de regra plural
iter(f) chama f.__ iter__
next(f) chama f.__ next__
Agora é a hora do final. Vamos reescrever o gerador de regras plurais como um iterador.
class LazyRules:
rules_filename = 'plural6-rules.txt'
def __init__(self):
self.pattern_file = open(self.rules_filename, encoding='utf-8')
self.cache = []
def __iter__(self):
self.cache_index = 0
return self
def __next__(self):
self.cache_index += 1
if len(self.cache) >= self.cache_index:
return self.cache[self.cache_index - 1]
if self.pattern_file.closed:
raise StopIteration
line = self.pattern_file.readline()
if not line:
self.pattern_file.close()
raise StopIteration
pattern, search, replace = line.split(None, 2)
funcs = build_match_and_apply_functions(
pattern, search, replace)
self.cache.append(funcs)
return funcs
rules = LazyRules()
Portanto, esta é uma classe que implementa __iter__()
e __next__()
, portanto, pode ser usada como um iterador. Em seguida, você instancia a classe e a atribui a rules
. Isso acontece apenas uma vez, na importação.
Vamos dar uma mordida na classe de cada vez.
class LazyRules:
rules_filename = 'plural6-rules.txt'
def __init__(self):
self.pattern_file = open(self.rules_filename, encoding='utf-8') ①
self.cache = [] ②
-
Quando instanciamos a
LazyRules
classe, abra o arquivo de padrão, mas não leia nada dele. (Isso vem depois.) -
Após abrir o arquivo de padrões, inicialize o cache. Você usará esse cache posteriormente (no método
__next__()
) ao ler as linhas do arquivo de padrão.
Antes de continuar, vamos dar uma olhada mais de perto em rules_filename
. Não está definido dentro do método __iter__()
. Na verdade, não é definido em nenhum método. É definido no nível da classe. É uma variável de classe e, embora você possa acessá-la como uma variável de instância (self.rules_filename
), ela é compartilhada por todas as instâncias da classe LazyRules
.
>>> import plural6
>>> r1 = plural6.LazyRules()
>>> r2 = plural6.LazyRules()
>>> r1.rules_filename ①
'plural6-rules.txt'
>>> r2.rules_filename
'plural6-rules.txt'
>>> r2.rules_filename = 'r2-override.txt' ②
>>> r2.rules_filename
'r2-override.txt'
>>> r1.rules_filename
'plural6-rules.txt'
>>> r2.__class__.rules_filename ③
'plural6-rules.txt'
>>> r2.__class__.rules_filename = 'papayawhip.txt' ④
>>> r1.rules_filename
'papayawhip.txt'
>>> r2.rules_filename ⑤
'r2-overridetxt'
-
Cada instância da classe herda o atributo
rules_filename
com o valor definido pela classe. - Alterar o valor do atributo em uma instância não afeta outras instâncias...
-
…Nem muda o atributo de classe. Você pode acessar o atributo de classe (em oposição ao atributo de uma instância individual) usando o atributo especial
__class__
para acessar a própria classe. -
Se você alterar o atributo de classe, todas as instâncias que ainda herdam esse valor (como
r1
aqui) serão afetadas. -
As instâncias que substituíram esse atributo (como
r2
aqui) não serão afetadas.
E agora de volta ao nosso show.
def __iter__(self): ①
self.cache_index = 0
return self ②
-
O método
__iter__()
será chamado sempre que alguém - digamos, um loopfor
- chamariter(rules)
. -
A única coisa que todo método
__iter__()
deve fazer é retornar um iterador. Nesse caso, ele retornaself
, o que sinaliza que essa classe define um método__next__()
que se encarregará de retornar valores ao longo da iteração.
def __next__(self): ①
.
.
.
pattern, search, replace = line.split(None, 2)
funcs = build_match_and_apply_functions( ②
pattern, search, replace)
self.cache.append(funcs) ③
return funcs
-
O método
__next__()
é chamado sempre que alguém - digamos, um loopfor
- chamanext(rules)
. Este método só fará sentido se começarmos pelo final e trabalharmos de trás para frente. Então vamos fazer isso. -
A última parte desta função deve parecer familiar, pelo menos. A função
build_match_and_apply_functions()
não mudou; é o mesmo de sempre. -
A única diferença é que, antes de retornar as funções match e apply (que são armazenadas na tupla
funcs
), vamos salvá-las emself.cache
.
Movendo-se para trás...
def __next__(self):
.
.
.
line = self.pattern_file.readline() ①
if not line: ②
self.pattern_file.close()
raise StopIteration ③
.
.
.
-
Um pouco de truque avançado de arquivo aqui. O método
readline()
(nota: singular, não pluralreadlines()
) lê exatamente uma linha de um arquivo aberto. Especificamente, a próxima linha. (Objetos de arquivo também são iteradores! São iteradores até o fim... ) -
Se houver uma linha para
readline()
ler,line
não será uma string vazia. Mesmo se o arquivo contiver uma linha em branco,line
terminará como uma string de um caractere'\n'
(um retorno de carro). Seline
for realmente uma string vazia, isso significa que não há mais linhas para ler do arquivo. -
Quando chegarmos ao final do arquivo, devemos fechá-lo e gerar a exceção mágica
StopIteration
. Lembre-se, chegamos a este ponto porque precisávamos de uma função de correspondência e aplicação para a próxima regra. A próxima regra vem da próxima linha do arquivo... mas não há próxima linha! Portanto, não temos valor a retornar. A iteração acabou. ( ♫ A festa acabou… ♫)
Movendo-se para trás todo o caminho até o início do método __next__()
...
def __next__(self):
self.cache_index += 1
if len(self.cache) >= self.cache_index:
return self.cache[self.cache_index - 1] ①
if self.pattern_file.closed:
raise StopIteration ②
.
.
.
-
self.cache
será uma lista das funções de que precisamos para combinar e aplicar regras individuais. (Pelo menos isso deve soar familiar!).self.cache_index
mantém registro de qual item em cache devemos retornar em seguida. Se ainda não esgotamos o cache (ou seja, se o comprimento deself.cache
é maior queself.cache_index
), ocorreu um acerto no cache! Viva! Podemos retornar a correspondência e aplicar funções do cache em vez de construí-las do zero. - Por outro lado, se não obtivermos um acerto do cache, e o objeto de arquivo tiver sido fechado (o que poderia acontecer, mais adiante no método, como você viu no trecho de código anterior), então não há mais nada que possamos Faz. Se o arquivo estiver fechado, significa que o esgotamos - já lemos todas as linhas do arquivo de padrão e já construímos e armazenamos em cache as funções de correspondência e aplicação para cada padrão. O arquivo está esgotado; o cache está esgotado; Estou exausto. Espere, o que? Aguente firme, estamos quase terminando.
Juntando tudo, eis o que acontece quando:
-
Quando o módulo é importado, ele cria uma única instância da classe
LazyRules
, chamadarules
, que abre o arquivo padrão, mas não lê a partir dele. - Quando perguntado sobre a primeira correspondência e a função de aplicação, ele verifica seu cache, mas descobre que o cache está vazio. Portanto, ele lê uma única linha do arquivo de padrão, constrói a correspondência e aplica funções a partir desses padrões e os armazena em cache.
- Digamos, para fins de argumentação, que a primeira regra corresponda. Nesse caso, nenhuma função adicional de correspondência e aplicação é construída e nenhuma linha adicional é lida do arquivo de padrão.
-
Além disso, para fins de argumentação, suponha que o chamador chame a função
plural()
novamente para pluralizar uma palavra diferente. O loopfor
na funçãoplural()
será chamadoiter(rules)
, o que redefinirá o índice do cache, mas não redefinirá o objeto de arquivo aberto. -
Na primeira vez, o loop
for
pedirá um valor derules
, que invocará seu método__next__()
. Desta vez, no entanto, o cache é inicializado com um único par de funções de correspondência e aplicação, correspondendo aos padrões na primeira linha do arquivo de padrão. Como foram construídos e armazenados em cache durante a pluralização da palavra anterior, eles são recuperados do cache. O índice do cache é incrementado e o arquivo aberto nunca é tocado. -
Digamos, para fins de argumentação, que a primeira regra não corresponda desta vez. Assim, o loop
for
volta a ocorrer e pede outro valor derules
. Isso invoca o método__next__()
uma segunda vez. Desta vez, o cache está esgotado - ele continha apenas um item e estamos pedindo um segundo - então o método__next__()
continua. Ele lê outra linha do arquivo aberto, compila e aplica funções a partir dos padrões e os armazena em cache. -
Este processo de leitura-construção-e-cache continuará enquanto as regras que estão sendo lidas do arquivo padrão não corresponderem à palavra que estamos tentando pluralizar. Se encontrarmos uma regra correspondente antes do final do arquivo, simplesmente a usamos e paramos, com o arquivo ainda aberto. O ponteiro do arquivo ficará onde quer que paramos de ler, esperando pelo próximo comando
readline()
. Nesse ínterim, o cache agora tem mais itens nele, e se começarmos tudo de novo tentando pluralizar uma nova palavra, cada um desses itens no cache será tentado antes de ler a próxima linha do arquivo de padrão.
Alcançamos o nirvana da pluralização.
-
Custo mínimo de inicialização. A única coisa que acontece em
import
é instanciar uma única classe e abrir um arquivo (mas não lê-lo). - Performance máxima. O exemplo anterior iria ler o arquivo e construir funções dinamicamente toda vez que você quisesse pluralizar uma palavra. Essa versão armazenará funções em cache assim que forem construídas e, no pior dos casos, só lerá o arquivo de padrão uma vez, não importa quantas palavras você pluralize.
- Separação de código e dados. Todos os padrões são armazenados em um arquivo separado. Código é código e dados são dados, e nunca os dois se encontrarão.
Observação
Isso é realmente o nirvana? Bem, sim e não. Aqui está algo a considerar com o exemplo LazyRules
: o arquivo de padrão é aberto (durante __init__()
) e permanece aberto até que a regra final seja alcançada. O Python acabará fechando o arquivo quando ele sair ou depois que a última instanciação da classe LazyRules
for destruída, mas ainda assim, isso pode levar muito tempo. Se esta classe fizer parte de um processo Python de longa execução, o interpretador Python pode nunca sair e o objeto LazyRules
nunca pode ser destruído.
Existem maneiras de contornar isso. Em vez de abrir o arquivo durante __init__()
e deixá-lo aberto enquanto lê as regras, uma linha por vez, você pode abrir o arquivo, ler todas as regras e fechá-lo imediatamente. Ou você pode abrir o arquivo, ler uma regra, salvar a posição do arquivo com o método tell()
, fechar o arquivo e depois abri-lo novamente e usar o método seek()
para continuar lendo de onde parou. Ou você não poderia se preocupar com isso e apenas deixar o arquivo aberto, como este código de exemplo faz. Programação é design, e design é tudo sobre trade-offs e restrições. Deixar um arquivo aberto por muito tempo pode ser um problema; tornar seu código mais complicado pode ser um problema. Qual deles é o maior problema depende de sua equipe de desenvolvimento, seu aplicativo e seu ambiente de execução.
Leitura adicional
- Tipos de iterador
- PEP 234: Iteradores
- PEP 255: Geradores Simples
- Truques do gerador para programadores de sistemas
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.
0 comentários:
Postar um comentário