Ainda haveria mais a dizer sobre ciclos em Python, mas acho que o principal ficou bem expresso nesta série de posts.
Como conclusão:
- Quando definimos uma classe, podemos torná-la iterável implementando o método especial __iter__();
- List comprehensions são uma forma compacta de criar listas sobre iteráveis. Permitem tornar o código muito conciso;
- Generator expressions são muito parecidas com as List comprehensions, mas geram valores sob pedido tal como os generatores;
- itertools é um módulo da biblioteca padrão do Python com muitas funções para criar, manipular e consumir iteráveis.
Notas finais:
- A iteração está em todo o lado e é uma importante ferramenta em qualquer linguagem de programação;
- O iterador é o elemento central na criação e utilização de iteráveis. Pode ser muito útil no uso avançado de iteráveis.
- Python tem uma forma limpa e poderosa para lidar com as iterações, que é usada um pouco por toda a linguagem e bibliotecas padrão;
- Devemos procurar abstrair e custumizar as nossas iterações;
- Utilizando as ferramentas que os iteráveis nos dão, podemos repensar as iterações e torná-las mais claras, concisas, reutilizáveis e eficazes.
Depois de lerem a série de posts sobre ciclos em Python fica o desafio de reverem e tentarem melhorar os vossos códigos Python com o que aqui aprenderam.
A inspiração para este post veio daqui.
"Ciclos em Python, conclusão" é um post originalmente publicado no blog sergiosilvablog.com
Planeta
-
A melhor forma de abstrair as nossas iterações é fazer os nossos próprios objectos iteráveis. Neste post veremos como podemos tornar os nossos objectos em iteráveis.
class ListaTarefas(objecto):
def __init__(self):
self.tarefas = []
def __iter__(self):
return iter(self.tarefas)
lista_de_tarefas = ListaTarefas():
...
for tarefa in lista_de_tarefas:
#...
A forma de tornar os nossos objectos em iteráveis é implementando o método especial ___iter__(). A única coisa que o método ___iter__() faz é devolver um qualquer tipo de iterador.
A forma mais simples de tornar um objecto num iterável é usar a função iter() numa qualquer colecção de dados que tenhamos. No caso acima temos uma lista de tarefas (self.tarefas), e a função __iter__() pode simplesmente devolver iter(self.tarefas).
Uma vez feito isto, o nosso objecto pode ser iterado tal como qualquer outro iterável em Python. No código acima usámos um ciclo for, mas pode também ser usado de todas as outras formas em que os iteráveis podem ser usados.
class ListaTarefas(objecto):
def __init__(self):
self.tarefas = []
def __iter__(self):
for tarefa in self.tarefas:
if not tarefa.concluida:
yield tarefa
def todas(self):
return iter(self.tarefas)
def concluidas(self)
return (t for t in self.tarefas if t.concluida)
Uma maneira mais poderosa de implementar a função __iter__() é usar um generator. De recordar que ao chamar um generator é devolvido um iterador, que é o que faz a função __iter__().
No código acima alteramos a classe ListaTarefas para produzir apenas as tarefas não concluídas quando for iterada.
Relembro que os objectos iteráveis podem ser iterados em mais do que uma maneira. Podemos também ter outros métodos que produzam iteradores. Na classe ListaTarefas temos o método todas() que devolve todas as tarefas, devolvendo apenas um iterador para a lista self.tarefas e o método concluidas() devolve as tarefas concluídas utilizando uma expressão de generator. As expressões de generator (Generator expressions) têm uma sintaxe similar às List comprehensions, mas em vez de devolverem uma lista devolvem um iterador.
Usei três técnicas diferentes nos três métodos que devolvem iteradores, apenas como demonstração das várias possibilidades que o Python permite. A ideia é providenciar iteração customizada utilizando todas as ferramentas que o Python nos disponibiliza.
E agora só falta a conclusão, para terminar com esta série de posts sobre os ciclos em Python.
A inspiração para este post veio daqui.
"Ciclos em Python, como tornar os nossos objectos em Iteráveis" é um post originalmente publicado no blog sergiosilvablog.com -
Neste post vou falar um pouco sobre os mecanismos de baixo nível presentes na iteração.
Existem dois tipos de objectos envolvidos na iteração. Até agora tenho falado nos iteráveis que são objectos que contêm um conjunto de valores. Os iteráveis não podem contudo ser percorridos directamente, para fazer isso é necessário solicitar ao iterável o iterador.
iterador = iter(iteravel) #iteravel.__iter__()
value = next(iterador) #iterador.next() ou iterador.__next__()
value = next(iterador)
...
- Os iteráveis produzem um iterador.
- Os iteradores produzem um stream de valores.
A operação mais importante num iterável é o iter() que devolve um iterador. E a única operação disponível num iterador é next(), que devolve o próximo valor ou entra na excepção StopIteration, que significa que a iteração terminou.
Tudo o que é possível fazer é solicitar o próximo valor, e obtemos um valor ou a excepção StopIteration. Alguns iteradores têm mais operações, mas a única que é garantida é o next().
with open("ficheiro.txt") as f:
#lê a primeira linha
primeira_linha = next(f)
#lê o resto do ficheiro
for linha in f:
#...
Com a operação de baixo nível next() podemos usar os iteráveis de outras formas além de os percorrer em ciclos. Por exemplo num ficheiro podemos ler a primeira linha com next() e depois usar um ciclo for para ler o resto do ficheiro.
De reparar que o ciclo for não volta a ler a primeira linha do ficheiro. Um ficheiro aberto é um iterador e ele sabe qual é a sua posição actual no ficheiro e ao pedir-lhe novos valores ele devolve o próximo valor (linha do ficheiro) a partir da posição actual.
Os posts sobre ciclos em Python está mesmo mesmo perto do fim. Só mais um pouco de paciência :)
A inspiração para este post veio daqui.
"Ciclos em Python, operações de baixo nível" é um post originalmente publicado no blog sergiosilvablog.com -
Um outro exemplo da forma de repensarmos a iteração, supondo que temos de percorrer uma estrutura bidimensional, como uma folha de cálculo. Uma forma de o fazer seria com dois ciclos um dentro do outro, o primeiro ciclo percorria as linhas e o segundo as colunas. As variáveis dos dois ciclos juntos seriam usadas para aceder ao conteúdo das células da folha de cálculo.
for linha in range(altura):
for coluna in range(comprimento):
valor = folha_de_calculo.obtem_valor(linha, coluna)
calculo = fazer_algo(valor)
if celula_prendida(calculo):
break
Existe um problema com esta solução. Supondo que procuramos uma célula com determinada condição e encontrada essa condição, queremos terminar com a procura, não existe uma forma elegante de terminar dois ciclos de uma só vez. A expressão break do código acima apenas termina com o ciclo que percorre as colunas enquanto que o ciclo que percorre as linhas continua em execução a partir do inicio da próxima linha.
Claro que existem formas de escrever o código de maneira a terminar simultaneamente com ambos os ciclos, mas são formas confusas e desorganizadas.
def range_2d(comprimento, altura):
#produz uma stream de 2 coordenadas
for y in range(altura):
for x in range(comprimento):
yield x, y
for coluna, linha in range_2d(comprimento, altura):
valor = folha_de_calculo.obtem_valor(linha, coluna)
calculo = fazer_algo(valor)
if celula_prendida(calculo):
break
Uma solução mais interessante passa por alterar a iteração de forma a abstrair a sua natureza bidimensional. Criamos o generator range_2d() que recebe como argumentos a altura e o comprimento e percorre todas as coordenadas da matriz bidimensional para produzir um stream de pares (x, y) a representar as coordenadas. O generator faz isso utilizando o mesmo ciclo duplo encadeado que usamos na versão anterior, mas agora produz uma stream de pares que representa as coordenadas das células da folha de cálculo.
O nosso ciclo for usado para obter os valores da cada célula passou de um ciclo duplo encadeado para um ciclo simples. Cada iteração do generator range_2d() produz um novo par de coordenadas, processamos as coordenadas exactamente como fizemos na primeira versão do código, mas agora quando encontramos a célula pretendida podemos simplesmente terminar o ciclo, e seguir em frente.
Desta forma alteramos um ciclo duplo encadeado para um ciclo simples repensando sobre o que estávamos a iterar. Esta é uma melhor iteração porque encaixa melhor na nossa forma de descrevermos a iteração. Ninguém iria descrever a pesquisa de uma célula numa folha de cálculo dizendo "por cada linha da folha de cálculo, obter o valor de cada célula da linha", mas diria antes "obter o valor de cada célula na folha de cálculo", que é o que este ciclo for diz.
Este generator permite-nos descrever o processo de "obter o valor de cada célula na folha de cálculo" de uma maneira mais abstracta, permitindo que o programa execute o seu trabalho de forma mais expressiva e tornando o código mais fácil de ler, indo de encontro à nossa forma de pensar.
for celula in folha_de_calculo.celulas():
valor = celula.obtem_valor()
calculo = fazer_algo(valor)
if celula_prendida(calculo):
break
O melhor seria a folha de cálculo ter um método celulas() que providenciasse um iterador sobre as células da folha de cálculo. De reparar que o generator range_2d() socorre-se de inteiros para obter as células. Em vez de gerar inteiros seria muito melhor ter métodos para iterar directamente sobre os valores em que estamos interessados.
Ainda vamos continuar com esta temática durante mais dois ou três posts. Até já :)
A inspiração para este post veio daqui.
"Ciclos em Python, Generators - parte 3" é um post originalmente publicado no blog sergiosilvablog.com -
Os generatores são uma ferramenta poderosa para a abstracção da iteração. Tal como as funções são boas para abstrair uma série de expressões, e as classes são boas para abstrair uma colecção de dados e métodos para os manipular, os generatores são bons para abstrair a iteração.
Como exemplo, vamos considerar um programa que lê um ficheiro de texto e faz algum processamento com as suas linhas. O ficheiro poderá ter comentários, que são linhas que começam por "#" e pode ter linhas em branco, ambas devem ser ignoradas pelo programa.
f = open("meu_ficheiro.txt")
for linha in f:
linha = linha.strip()
if linha.startswith("#"):
#linha de comentário, não faz nada
continue
if not linha:
#linha em branco, não faz nada
continue
#linha com interesse
faz_algo(linha)
Aqui temos esboço do programa. Abrimos o ficheiro, percorremos as suas linhas. Removemos os espaços em branco do início e do fim das linhas, depois verificamos se a linha começa com "#", se sim não consideramos a linha e avançamos para a próxima, da mesma forma se a linha for uma linha em branco, avançamos também para a próxima linha do ficheiro.
Finalmente, se a linha não é um comentário nem uma linha em branco, então tratamos a linha da forma que entendermos. No fim a função faz_algo() terá processado todas as linhas do ficheiro que não sejam comentários nem linhas em branco.
Este programa funciona bem, mas mistura dois conceitos. O primeiro é, o que são linhas interessantes e quais devem ser ignoradas? O segundo é, o que fazer com as linhas interessantes? Estas duas questões poderão não ter nada a haver uma com a outra. Poderemos, por exemplo, precisar de reutilizar o primeiro (conceito) para processar diferentes tipos de ficheiros.
Deve-se evitar misturar conceitos, porque:
- Torna o código menos legível;
- É mais difícil reutilizar o código;
- Dificulta a manutenção futura do código;
- É mais propenso a bugs.
def linhas_interessantes(f)
for linha in f:
linha = linha.strip()
if linha.startswith("#"):
#linha de comentário, não faz nada
continue
if not linha:
#linha em branco, não faz nada
continue
#linha com interesse
yield linha
with open("meu_ficheiro.txt") as f:
for linha in linhas_interessantes(f):
faz_algo(linha)
with open("meu_outro_ficheiro.txt") as f:
for linha in linhas_interessantes(f):
faz_outra_coisa(linha)
Utilizando um generator podemos dividir o ciclo em duas partes. A primeira parte, responsável por identificar quais as linhas que são interessantes poderá ser implementada por um generator. O método linhas_interessantes() do exemplo acima é esse generator, ele analisa cada linha do ficheiro, identifica as linhas interessantes e em vez de fazer o processamento dessas linhas faz o yield das mesmas.
Agora podemos usar o generator linhas_interessantes() num ciclo for e obter apenas as linhas interessantes, todas as linhas que não interessam são filtradas pelo generator. Assim torna-se mais simples reutilizar a lógica de obter as linhas interessantes noutros ciclos.
De reparar que a forma de pensarmos a lógica da iteração também foi alterada. Passamos de inicialmente pensarmos a lógica como, "percorrer as linhas de um ficheiro e manualmente escolher as linhas interessantes", para, com a ajuda do generator, podermos passar a pensar como, "percorrer directamente as linhas interessantes de um ficheiro". Isto permite-nos ter um nível de abstracção mais elevado.
No próximo post continuarei a falar sobre ciclos em Python e também sobre generatores.
A inspiração para este post veio daqui.
"Ciclos em Python, Generators - parte 2" é um post originalmente publicado no blog sergiosilvablog.com -
Os generators são uma forma de criar iteráveis através de uma função.
Uma função normal retorna um valor com o uso da palavra return. Um generator é tal como uma função, mas em vez de usar a palavra return usa a palavra yield uma ou mais vezes (pode também usar a palavra return para sair da função). Ao chamar a função generator é criado um iterável, e ao iterar sobre o mesmo é executado o código do generetor. Cada vez que um yield é executado ele produz um novo valor no stream do iterável.
def ola_mundo():
yield "Ola"
yield "mundo"
for x in ola_mundo():
print x
Ola
mundo
Este pequeno exemplo ilustrativo de um generator, contém apenas duas chamadas da expressão yield. Ao iterarmos sobre a função ola_mundo() com um ciclo for verificamos que este generator produz uma stream de dois valores, "Ola" e "mundo".
Os generatores normalmente não são assim tão simples, tipicamente o código de um generator terá um ciclo for com chamadas à expressão yield.
def numeros_pares(stream):
for n in stream:
if n % 2 == 0:
yield n
for n in numeros_pares(nums):
faz_algo(n)
Podemos reescrever a função numeros_pares() como um generator. O código é praticamente o mesmo, mas em vez de produzir uma lista e devolvê-la no fim da função, simplesmente produz os valores assim que os encontra.
Chamamos esta função numeros_pares() da mesma forma, passando-lhe o iterável que queremos que seja trabalhado e a função devolve-nos um iterador que produzirá os valores pares desse iterável.
A função anterior de numeros_pares() construía uma lista de números pares que nos devolvia no fim, isso significa que nada mais acontecia no nosso programa enquanto a função não examinasse a sequência completa do iterável e seleccionasse os valores pares para a nova lista. Para um iterável pequeno isso poderá não ser um problema mas será certamente para um iterável grande.
Já a versão generator da função devolverá o primeiro número par que encontrar deixando o nosso programa disponível para prosseguir com o seu trabalho antes que toda a sequência do iterável seja examinada. Isto significa que a função pode trabalhar indefinidamente ou mesmo com streams infinitas.
Os generatores têm a vantagem de serem "preguiçosos", eles não "trabalham" até que o primeiro valor lhe seja pedido, e quando isso acontece limitam-se a dar esse valor e voltam a parar, não voltando a "trabalhar" até que o próximo valor seja pedido. Isto faz com que os generators usem poucos recursos e sejam mais indicados para certos tipos de iteráveis.
Os generators podem ser inicialmente confusos, eles são muito semelhantes a funções mas têm diferenças importantes. Primeiro chamar um generator não faz com que o seu código seja imediatamente executado, ele simplesmente cria um iterável. Só quando um ciclo for ou outro "consumidor" do iterável começar a pedir valores do stream é que o código do generator começa a ser executado.
Quando a expressão yield é encontrada, a execução é suspensa, e o valor é usado como o próximo valor da stream. Numa função normal a expressão return termina a função. Mas num generator o estado actual é memorizado e quando o próximo valor for solicitado, a execução do generator continua onde tinha ficado anteriormente, depois da expressão yield. O próximo valor da stream é produzido quando uma expressão yield é novamente executada. Todas as variáveis locais do generator mantêm os seus valores durante todo o percurso do stream. Isto torna os generatores muito convenientes para escrever iteráveis.
O iterador termina quando o generator retorna, quer explicitamente com a expressão return, ou quando executa a última expressão do código do generator.
A série de posts sobre ciclos em Python tem continuação nos próximos capítulos.
A inspiração para este post veio daqui.
"Ciclos em Python, Generators - parte 1" é um post originalmente publicado no blog sergiosilvablog.com -
Já vimos nos posts anteriores o poder da iteração em Python, vamos agora ver neste post como podemos personalizar as nossa iterações.
nums =[88, 73, 92, 72, 40, 38, 25, 20, 90, 72]
for n in nums:
if n % 2 == 0:
faz_algo(n)
def numeros_pares(stream):
valores=[]
for n in stream:
if n % 2 == 0:
valores.append(n)
return valores
for n in numeros_pares(nums):
faz_algo(n)
Uma forma de iteração indirecta é percorrer uma sequência de valores e entre eles escolher aqueles que nos interessam.
Neste exemplo temos uma lista de números aos quais queremos fazer_algo() com os valores pares. No primeiro exemplo percorremos a lista num ciclo for e testamos se cada valor é par e só no caso de ser par realizamos a nossa operação fazer_algo().
Uma alternativa seria escrever uma função que aceita uma sequência como argumento e produz a nova sequência que pretendemos. A função numeros_pares() faz isso. Itera sobre a lista que é enviada no argumento e devolve uma lista com os valores pares. Depois podemos usar essa lista num ciclo for para iterar directamente sobre os números pares.
Este exemplo tem um problema porque cria na realidade uma nova lista, uma lista com números pares. Veremos no próximo post como evitar isso.
A série de posts sobre ciclos em Python aproxima-se rapidamente do fim, mas continuará ainda a ser o assunto principal nos próximos posts.
A inspiração para este post veio daqui.
"Ciclos em Python, iteração personalizada" é um post originalmente publicado no blog sergiosilvablog.com -
Outra dúvida recorrente quando se lida com a iteração é iterar sobre duas listas.
Como vimos no post anterior (Ciclos em Python, problemas comuns e os índices) não precisamos de iterar sobre inteiros para indexar os valores numa iteração. Outro desafio numa iteração é como percorrer simultaneamente duas listas.
cidades =['Aveiro', 'Braga', 'Porto', 'Lisboa']
habitantes = [55291, 181894, 237584, 547631]
for i in range(len(cidades)):
cidade = cidades[i]
populacao = habitantes[i]
print ("{0} tem {1} habitantes".format(cidade, populacao))
Aveiro tem 55291 habitantes
Braga tem 181894 habitantes
Porto tem 237584 habitantes
Lisboa tem 547631 habitantes
A geração de inteiros para percorrer os valores das listas pelos seus índices é uma forma de o fazer, mas mais uma vez não existe necessidade de gerar inteiros para executar esta tarefa.
for cidade, populacao in zip(cidades, habitantes):
print ("{0} tem {1} habitantes".format(cidade, populacao))
Aveiro tem 55291 habitantes
Braga tem 181894 habitantes
Porto tem 237584 habitantes
Lisboa tem 547631 habitantes
A função zip() recebe dois iteréveis e junta-os num só, devolvendo um iterável de pares. Como se pode ver no exemplo acima, conseguimos obter o mesmo resultado que a versão que se socorre de inteiros.
De reparar que como não usa indexação, a função zip() funciona com qualquer iterável e não apenas com listas.
A função dict() recebe como argumento um stream de pares do tipo chave/valor e constrói um dicionário a partir deles. Se tivermos duas listas uma com as chaves, por exemplo com nomes de cidades e outra com os valores, por exemplo com o número de habitantes a função zip(cidades, habitantes) produzirá um stream de pares que poderá ser usado pela função dict dict(zip(cidades, habitantes)) para produzir um dicionário.
cidades =['Aveiro', 'Braga', 'Porto', 'Lisboa']
habitantes = [55291, 181894, 237584, 547631]
populacoes_por_cidade = dict(zip(cidades, habitantes))
print populacoes_por_cidade
{'Lisboa': 547631, 'Braga': 181894, 'Aveiro': 55291, 'Porto': 237584}
print max(populacoes_por_cidade.values())
547631
print max(populacoes_por_cidade.items(), key=lambda b: b[1])
('Lisboa', 547631)
print max(populacoes_por_cidade, key=populacoes_por_cidade.get)
Lisboa
Supondo que temos um dicionário com cidades e o seu número de habitantes.
Podemos utilizar a função max() para obter a maior população, o nome e a população da cidade com mais habitantes, ou apenas o nome da cidade mais populosa.
A iteração em Python permite-nos obter informação útil através de uma única e concisa expressão.
Os ciclos continuarão a ser o assunto principal dos próximos posts.
A inspiração para este post veio daqui.
"Ciclos em Python, iterar sobre duas listas" é um post originalmente publicado no blog sergiosilvablog.com -
Quando se lida com a iteração é recorrente surgirem determinadas dúvidas. Vejamos algumas delas.
Voltando à lista de números (ver post Ciclos em Python, o básico). Sabemos que podemos iterar sobre os valores da lista sem os indexar, mas por vezes podemos precisar mesmo de ter o índice dos valores.
Forma incorrecta de fazer
for i in range(len(my_list)):
v = my_list[i]
print i, v
Nesse caso poderíamos voltar a usar o range(len(my_list)) do exemplo 2. Mas existe uma melhor forma de o fazer.
Forma correcta de fazer
for i, v in enumerate(my_list):
print i, v
No código acima vemos como podemos utilizar a função enumerate() para obter dois valores em cada iteração. O i é o índice do valor e o v é o valor propriamente dito. Desta forma obtemos os valores com os seus índices sem ter que iterar sobre inteiros.
cidades = ['Aveiro', 'Braga', 'Lisboa', 'Porto']
list(enumerate(cidades))
[(0, 'Aveiro'), (1, 'Braga'), (2, 'Lisboa'), (3, 'Porto')]
Além de podermos iterar sobre os iteráveis podemos também manipular os iteráveis para produzir novos iteráveis que nos sejam mais convenientes. A função enumerate() pega num iterável e transforma-o num conjunto de pares, cada valor do iterável é associado a um número sequencial a começar no zero.
No exemplo acima, a lista de cidades é chamada com a função enumerate() que associa um número sequencial a cada cidade. A função list() dá-nos uma nova lista com o emparelhamento das cidades e os respectivos números.
for num, cidade in enumerate(cidades):
print num, cidade
0 Aveiro
1 Braga
2 Lisboa
3 Porto
Podemos usar a atribuição múltipla para obter os pares como dois valores separados. No exemplo acima às variáveis num e cidade são atribuídas os valores dos números das cidades e o nome das cidades provenientes da lista.
A vantagem de usar a função enumerate() é que ela funciona com qualquer iterável. Quando usamos índices para iterar sobre um iterável é necessário que o iterável suporte índices o que nem sempre acontece. Não é possível aceder, por exemplo, à 10ª linha de um ficheiro.
Além de o enumerate() funcionar com qualquer iterável é também mais compacto e directo.
i=0
for v in iteravel:
print i, v
i += 1
Outra forma comum de obter o índice de um iterável é manter um contador à parte, isto é como ter um atrelado agarrado a nós. Esta técnica funciona mas adiciona complexidade extra ao ciclo. Esta é uma forma mais difícil de ler o código e no caso do ciclo ser um pouco mais extenso do que este, o contador pode facilmente ficar esquecido ou fora do lugar em futuras alterações ao código. Quanto mais simples for o ciclo melhor. Menos partes a mexer significa menos bugs.
Os próximos posts continuarão a ser sobre ciclos em python.
A inspiração para este post veio daqui.
"Ciclos em Python, problemas comuns e os índices" é um post originalmente publicado no blog sergiosilvablog.com -
Além de providenciar muitos iteráveis interessantes, o python também faz uso da iteração em muitos sítios que não apenas em ciclos. Aqui ficam alguns exemplos:
A função list() cria uma lista a partir dos valores de um iterável que recebe como argumento. O argumento pode ser qualquer iterável, se for um dicionário teremos uma lista com as chaves do dicionário, se for um ficheiro teremos uma lista com as linhas do ficheiro, etc.
As List comprehensions são uma forma concisa de obter listas a partir de iteráveis.
A função sum() devolve a soma de todos os valores de um iterável, terá de ser um iterável de valores que possam ser adicionados. A função min() compara os valores de um iterável e devolve o menor deles e a função max() devolve o maior.
palavras = palavras = ['Este', 'teste', 'junta', 'palavras.']
frase = ' '.join(palavras)
print frase
Este teste junta palavras.
O método de strings join() devolve a concatenação das strings que recebe num iterável. O separador das strings é a string onde o método foi aplicado.
Estes são apenas alguns exemplos do uso de iteráveis fora dos ciclos, como 'stream' de valores que são passados para funções.
Mais posts se seguirão sobre os ciclos em python.
A inspiração para este post veio daqui.
"Ciclos em Python, uso de Iteráveis fora dos ciclos" é um post originalmente publicado no blog sergiosilvablog.com


