Armazenando senhas de forma segura

Dica ao leitor: não deixe de ler a seção “O mecanismo correto”.

Diga aí, você anota a senha do seu email em um post-it e deixa ele colado em seu monitor, visível a qualquer um? Aposto que não, pois seria muito fácil para alguém acessar sua conta e mandar emails engraçadinhos para seus contatos. Da mesma forma que você protege sua senha, os aplicativos que utilizam informações de login e senha para autenticar usuários (como o seu serviço de email) também devem cuidar das senhas armazenadas neles, não devendo nunca guardá-las em texto puro. Vamos ver nesse post como funciona a autenticação, e as melhores práticas para implementar esse serviço na sua aplicação de forma razoavelmente segura.

Autenticação

Você já parou para pensar em como funciona a autenticação em um serviço de email? Primeiro, você digita o seu nome de usuário e senha em um formulário web, como o exemplo abaixo:

Usuário: pythonhelp
  Senha: *******

Esses dados são então enviados para o servidor que está fornecendo o serviço de autenticação do seu email. Lá dentro, o serviço de autenticação irá procurar por um usuário chamado pythonhelp no banco de dados de usuários. Se encontrá-lo, irá realizar uma comparação (mais para frente veremos que não é uma simples comparação de duas strings) para verificar se a senha fornecida no formulário web corresponde à senha do usuário. Em caso positivo, a autenticação ocorre com sucesso e você pode então acessar sua conta de email. Em caso negativo, aquela mensagenzinha chata avisando que você errou seu nome de usuário ou sua senha aparece na tela. (A propósito, você já percebeu que a maioria dos serviços não informa se o que erramos foi o nome de usuário ou se foi a senha? Esse tipo de informação é usualmente interessante para um invasor em potencial.)

Deixando de lado alguns detalhes, a autenticação funciona basicamente da forma descrita acima. Antes de vermos como isso tudo poderia ser implementado, veremos como NÃO deve ser implementado um serviço de autenticação.

Como NÃO implementar autenticação

Vamos desenvolver o serviço de autenticação para a nossa aplicação. Para isso, criamos uma tabela no banco de dados chamada USUARIOS, que contém duas informações sobre cada usuário: seu nome de usuário e sua senha. Como você pode ver abaixo, armazenamos a senha dos usuários em texto puro, ou seja, as senhas estão visíveis a qualquer pessoa que obtiver acesso ao banco de dados.

+--------------+
| USUARIOS     |
+--------------+------------------------+
|    NOME      |        SENHA           |
+--------------+------------------------+
| joaozinho    | teste                  |
+--------------+------------------------+
| pedrinho     | teste123               |
+--------------+------------------------+
| maria        | t35t3                  |
+--------------+------------------------+

Para fazer a autenticação, basta que o usuário forneça a sua senha e que comparemos a senha fornecida com a que está armazenada no BD.

Isso até funciona, mas eu é que não forneceria a minha senha para um sistema meia-boca desses que vai armazená-la em texto puro no banco de dados. Sabe por quê? Porque uma vez que alguém obtenha acesso ao banco de dados do sistema, basta isso para quebrar a privacidade de todos os usuários:

user@host:~/$ sqlite usuarios.db
sqlite> select * from usuarios;
joaozinho|teste
pedrinho|teste123
maria|t35t3

You're doing it wrong!

Que coisa, não? As senhas estão expostas. Falha de segurança gravíssima! Sabendo que a maioria dos usuários usa a mesma senha para os logins em vários sites, dá pra ter uma idéia do estrago né?

Lição número 1: JAMAIS ARMAZENE SENHAS EM TEXTO PURO!

Um jeito melhor

Agora que você já sabe como não fazer, vamos ver uma forma um pouquinho melhor (ainda não a correta) de implementar um mecanismo de autenticação.

Dessa vez nós não vamos armazenar as senhas dos usuários no BD. O que vamos armazenar é uma informação relacionada à senha e gerada a partir dela, o chamado hash da senha.

O que é o Hash?

O hash de um valor é o resultado da aplicação de uma função de hashing a tal valor. Esse resultado é, em geral, muito diferente do valor original. Uma função de hashing H recebe como entrada um valor x e retorna como resultado o hash h correspondente àquele valor:

H(x) -> h

Vamos calcular o hash do valor 'teste123':

H('teste123') -> 'aa1bf4646de67fd9086cf6c79007026c'

Vamos agora calcular o hash do valor 'teste12':

H('teste12') -> '0940004e70ce8d82b440d3c1244dfdee'

Vamos calcular novamente o hash do valor 'teste123':

H('teste123') -> 'aa1bf4646de67fd9086cf6c79007026c'

Agora vamos aplicar a função de hash ao valor 'aa1bf4646de67fd9086cf6c79007026c' (que é o hash de 'teste123'):

H(‘aa1bf4646de67fd9086cf6c79007026c’) -> ‘0728a200630cec4b33e33e20646bc54a’

Observando com atenção, você pode notar algumas coisas sobre as funções de hash:

  1. A função de hash gera um resultado cujo valor é muito diferente do valor original.
  2. Quando alteramos levemente o valor de entrada para a função de hash, o valor retornado por ela muda completamente (veja os exemplos de aplicação nas entradas 'teste123' e 'teste12'). Isso é chamado de efeito avalanche.
  3. Quando aplicamos H novamente à entrada 'teste123', obtivemos o valor idêntico ao obtido pela primeira vez. Ou seja, a função de hash é determinística.
  4. O último exemplo nos mostra que uma função de hash não é reversível, isto é, dado o hash de um valor, não conseguiremos descobrir o valor original através desse hash.

A partir das observações acima, podemos inferir algumas propriedades que as funções de hash possuem:

  1. É improvável (muito improvável mesmo) que você modifique a entrada da função sem modificar o resultado dela.
  2. É impossível gerar o valor de entrada a partir do resultado.
  3. É muito difícil (muito mesmo) encontrar dois valores para os quais a função de hashing produza o mesmo resultado.
  4. Sempre que aplicada ao mesmo valor x, uma mesma função H(x) irá retornar o mesmo resultado h.

* Nos exemplos acima, usei o algoritmo de hashing MD5, embora existam vários outros que poderiam ser igualmente usados, como SHA-1, SHA-256, etc.

Autenticação com hash

Conhecendo as propriedades acima, podemos criar um mecanismo de autenticação mais seguro. O objetivo é armazenar as informações de senha de forma mais protegida, em vez de deixá-la exposta em texto puro.

A primeira idéia pode ser simplesmente armazenar apenas o hash da senha. Parece uma boa idéia, afinal, a propriedade 2 diz que se alguém roubar o valor do hash da sua senha, não conseguirá obter a senha propriamente dita, e a propriedade 4 nos possibilita verificar se a senha está correta comparando o hash do que o usuário digitou com o hash armazenado no banco de dados.

Veja abaixo como ficaria o nosso novo BD de usuários. Para cada usuário, armazenamos o hash da senha que ele forneceu no cadastro.

+-----------+
| USUARIOS  |                                  
+-----------+----------------------------------+
| NOME      |         HASH DA SENHA            |
+-----------+----------------------------------+
| joaozinho | 698dc19d489c4e4db73e28a713eab07b |
+-----------+----------------------------------+
| pedrinho  | aa1bf4646de67fd9086cf6c79007026c |
+-----------+----------------------------------+
| maria     | c7ac7410983dc7efbb2e5c062c515b7d |
+-----------+----------------------------------+

Quando o usuário quiser se autenticar no sistema, teremos em mãos a senha fornecida por ele na tela de login, mas não podemos compará-la diretamente ao valor armazenado no banco, pois o que está armazenado é o hash da senha. O serviço de autenticação deverá aplicar a função de hash sobre a senha fornecida pelo usuário e comparar o resultado com o que está armazenado no BD. Se o valor obtido for idêntico ao hash armazenado no banco de dados para aquele usuário, a autenticação ocorre com sucesso. Caso contrário, erro de autenticação.

Agora as senhas estarão um pouquinho mais seguras. Isso mesmo, somente um pouco, pois existem meios para descobrir o valor da senha original através do hash dela. A próxima seção vai descrever um pouco isso.

Ataques ao banco de dados de senhas (ou hashes delas)

Uma coisa que acontece com frequência é um sistema sofrer uma invasão e os invasores realizarem uma cópia do seu banco de dados. Considere que os invasores roubaram o seguinte banco de dados:

+-----------+
| USUARIOS  |                                  
+-----------+----------------------------------+
| NOME      |         HASH DA SENHA            |
+-----------+----------------------------------+
| joaozinho | 698dc19d489c4e4db73e28a713eab07b |  <-- hash de "teste"
+-----------+----------------------------------+
| pedrinho  | aa1bf4646de67fd9086cf6c79007026c |  <-- hash de "teste123"
+-----------+----------------------------------+
| maria     | 2a1bdbdad93b1081007abc4b419d8f0b |  <-- hash de "t35t3"
+-----------+----------------------------------+
|    ...    |               ...                |
+-----------+----------------------------------+

O que eles poderiam fazer com esses dados? De acordo com as propriedades que vimos sobre as funções de hashing, não seria possível extrair o valor original que gerou o hash, então você imaginaria que as senhas estariam seguras dessa maneira.

Teoricamente, sim. Mas agora imagine que o invasor (uma pessoa muuuuuuito paciente), de posse do hash da senha do usuário joaozinho (698dc19d489c4e4db73e28a713eab07b), comece a fazer alguns testes com valores comumente usados como senhas:

>>> import hashlib
>>> print hashlib.md5('123').hexdigest()
202cb962ac59075b964b07152d234b70
>>> print hashlib.md5('abcde').hexdigest()
ab56b4d92b40713acc5af89985d4b786
>>> print hashlib.md5('bla').hexdigest()
128ecf542a35ac5270a87dc740918404
>>> print hashlib.md5('teste').hexdigest()
698dc19d489c4e4db73e28a713eab07b

Opa! O invasor acabou de descobrir a senha do joaozinho (teste), pois o hash obtido para essa string é o mesmo hash que está armazenado para o joaozinho no BD. Se ele continuar testando valores que considera prováveis de serem usados como senha, ele pode, eventualmente, acabar fazendo:

>>> print hashlib.md5('teste123').hexdigest()
aa1bf4646de67fd9086cf6c79007026c

Então ele terá descoberto a senha do usuário pedrinho, pois o hash gerado para teste123 possui o mesmo valor que está armazenado para esse usuário. A esse tipo de ataque, chamamos de Ataque por força bruta.

Imagino que você esteja pensando que o invasor terá muito trabalho para descobrir a senha do usuário maria, testando milhões de possibilidades antes de chegar em t35t3. Pois é. Se ele for testando manualmente, dificilmente irá descobrir a senha. Mas imagine agora que o invasor tenha escrito um programa que gere uma tabela gigantesca contendo possíveis senhas e seus hashes:

+----------------------------------+---------------------+
|              HASH                |       SENHA         |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+
| 698dc19d489c4e4db73e28a713eab07b | teste               |
+----------------------------------+---------------------+
| e959088c6049f1104c84c9bde5560a13 | teste1              |
+----------------------------------+---------------------+
| 38851536d87701d2191990e24a7f8d4e | teste2              |
+----------------------------------+---------------------+
| 56c1056afb34f0d5ad809821d417a52b | t3st3               |
+----------------------------------+---------------------+
| 2a1bdbdad93b1081007abc4b419d8f0b | t35t3               |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+
| 128ecf542a35ac5270a87dc740918404 | bla                 |
+----------------------------------+---------------------+
| 14a310c18e7ee2627b3de4ff82b11e76 | bl4                 |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+

Tendo essa tabela pré-calculada, uma vez que o invasor esteja de posse do hash da senha da maria (2a1bdbdad93b1081007abc4b419d8f0b), basta pesquisar por esse hash na tabela de hashes. Se encontrar um registro que possua tal hash, basta obter a senha que o acompanha.

É evidente que essa tabela deve ser gigantesca, gigantesca mesmo, para conter uma boa quantidade de combinações de valores de senhas. Para “facilitar o trabalho”, invasores do mundo todo colaboram na criação e busca em tabelas desse tipo. Essas tabelas são conhecidas como Rainbow Tables.

Não só isso, os próprios usuários colaboram com os invasores, usando as mesmas senhas em vários lugares, ou usando palavras simples de adivinhar, de forma que bem antes duma tabela dessas cobrir todas as possibilidades de hash, ela já pode ser extremamente útil para os invasores.

Lição número 2: O HASH SOZINHO NÃO FAZ MILAGRE!

Evitando as Rainbow Tables

Para lidar com isso, existe uma técnica chamada de salgar senhas (tradução literal do inglês “salting passwords”), que consiste em adicionar um “temperinho” na senha antes de armazenar. A idéia é gerar uma string contendo alguns valores aleatórios e concatenar essa string à senha do usuário na hora gerar o hash. Assim, ao invés de tomar somente a senha do usuário como entrada, a função de hashing passa a tomar como entrada também a string aleatória (chamada de salt, ou sal). Esse sal também é armazenado no BD para que posteriormente seja possível que o serviço de autenticação realize a verificação.

O que isso traz de segurança para o sistema? Vamos visualizar uma tabela que armazena o nome do usuário, o hash da concatenação entre senha e sal, e o sal. A senha de cada usuário é a mesma senha mostrada lá no início do post.

+-----------+
| USUARIOS  |                                  
+-----------+----------------------------------+-----------+
| NOME      |       HASH DE (SENHA+SAL)        |    SAL    |
+-----------+----------------------------------+-----------+
| joaozinho | e3e923b2c0d3890270a2cb6d52b13bf6 |   h6ja8   |
+-----------+----------------------------------+-----------+
| pedrinho  | 046b8f04069efab205d9f7bcc099e3d3 |   5uoWB   |
+-----------+----------------------------------+-----------+
| maria     | 960e18e4dc393660fdf3caba8634f38e |   jtm7a   |
+-----------+----------------------------------+-----------+

Veja como foram gerados os hashes armazenados:

>>> print hashlib.md5('teste'+'h6ja8').hexdigest()
e3e923b2c0d3890270a2cb6d52b13bf6
>>> print hashlib.md5('teste123'+'5uoWB').hexdigest()
046b8f04069efab205d9f7bcc099e3d3
>>> print hashlib.md5('t35t3'+'jtm7a').hexdigest()
960e18e4dc393660fdf3caba8634f38e

Está achando estranho o fato de termos armazenado o sal em texto puro no BD? Pois é, se o invasor roubar nossa base, ele terá de lambuja o valor do sal. Mas isso não é um problema, pois o objetivo principal de salgarmos a senha é impossibilitar a utilização de rainbow tables, afinal a idéia principal por trás dessas tabelas é o cálculo prévio dos hashes de vários valores. Ao roubar o sal e o hash de uma senha, o invasor teria que recalcular toda a rainbow table para poder descobrir a senha do usuário. E essa tarefa é muito custosa.

Assim, se cada usuário possuir um sal diferente, a rainbow table terá que ser recalculada para cada usuário, tornando essa atividade praticamente impossível.

Considere que o invasor possui a rainbow table abaixo, bem como o hash (e3e923b2c0d3890270a2cb6d52b13bf6) e o sal (h6ja8) do usuário joaozinho (roubados da base de dados de usuários).

+----------------------------------+---------------------+
|              HASH                |       SENHA         |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+
| 698dc19d489c4e4db73e28a713eab07b | teste               |
+----------------------------------+---------------------+
| e959088c6049f1104c84c9bde5560a13 | teste1              |
+----------------------------------+---------------------+
| 38851536d87701d2191990e24a7f8d4e | teste2              |
+----------------------------------+---------------------+
| 507eb04c9c427e9f961e47a7204fac41 | teste3              |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+
| 128ecf542a35ac5270a87dc740918404 | bla                 |
+----------------------------------+---------------------+
| df5ea29924d39c3be8785734f13169c6 | blabla              |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+

Para obter a senha do usuário, o invasor terá que recalcular toda a tabela. Por exemplo, terá que calcular o hash de “testeh6ja8”, de “teste1h6ja8”, e assim por diante, até encontrar o hash correspondente. Considerando que ela pode ter zilhões de registros, essa é uma tarefa bastante demorada. Imagine recalcular toda a tabela para todas as possíveis combinações de valores no sal.

Perceba que o sal que utilizamos é bastante curto. Quanto mais longo for o sal, mais protegidas contra rainbow tables as senhas estarão, pois o número de combinações possíveis de valores para o sal aumentam exponencialmente.

Dessa forma, o sal derruba a maior força das rainbow tables, que é a busca rápida por elementos (que ocorre graças ao cálculo prévio dos valores). Ao usar valores de sal suficientemente longos, estamos aumentando MUITO (MUITO MESMO) a quantidade de entradas que uma rainbow table deve ter para servir como uma base de hashes pré-calculados (segundo a Wikipedia, hoje em dia são usados salts de até 128 bits). Se, além disso, usarmos um sal diferente para cada usuário, então tornamos impossível o uso de tabelas pré-computadas para a descoberta das senhas dos nossos usuários.

Mas, ainda assim, hashes salgados não são a solução definitiva para armazenamento de senhas. Leia a próxima seção para descobrir o porquê.

O mecanismo correto

Mesmo sendo um mecanismo muito mais seguro do que armazenando a senha em texto puro ou o hash simples, o armazenamento de hashes de senhas salgadas ainda não é a melhor solução. Apesar de amplamente utilizados, os algoritmos de hashing como MD5 e SHA-1 não são recomendados para o armazenamento de senhas, pois são bastante velozes para realizar o hashing de um valor. Um algoritmo rápido torna a geração de ataques via força bruta e rainbow tables mais fácil, pois o tempo necessário para gerar as tabelas acaba sendo pequeno.

Um algoritmo para uso no armazenamento de senhas tem como requisito ser lento. Lento o suficiente para atrapalhar o atacante, mas não o bastante para atrapalhar o usuário.

Uma forma recomendada de armazenar as senhas dos usuários é usando o bcrypt. O BCrypt é um mecanismo criptográfico criado para lidar com senhas. Assim sendo, uma de suas características é ser demorado para geração do hash.

Para ter uma idéia da diferença na velocidade dos dois mecanismos, observe o resultados de uns testes que fiz usando o timeit:

$ python -m timeit -s "import bcrypt; salt = bcrypt.gensalt()" "bcrypt.hashpw('teste', salt)"
10 loops, best of 3: 243 msec per loop

$ python -m timeit -s "import hashlib" "hashlib.md5('teste')"
1000000 loops, best of 3: 0.409 usec per loop

Enquanto o MD5 gera os hashes em uma média de 0.409 microsegundos por hash, o bcrypt leva em média 243 milisegundos por hash.

Instalando o bcrypt

A implementação Python do bcrypt não está disponível com a biblioteca-padrão, portanto é necessário instalá-la através do gerenciador de pacotes pip:

sudo pip install py-bcrypt

Obs.: o módulo é escrito em linguagem C, portanto o código será compilado. Para isso, é necessário possuir instalados (em sistema Ubuntu): build-essential e python-dev.

Alternativamente, num sistema Debian ou Ubuntu você pode instalar o pacote no repositório do apt:

sudo apt-get install python-bcrypt

Usando o bcrypt

Assim como a maioria dos módulos Python, o bcrypt é facinho de usar. Vamos fazer alguns exemplos.

Gerando o hash de uma senha

>>> import bcrypt
>>> print bcrypt.hashpw('teste123', bcrypt.gensalt())
$2a$12$axTaPizQ6q2V8VCLseqEq.Rwm1aZVx7oimvjPmmLWTNE3uW15xpIu

Execute o código acima no seu interpretador Python local e perceba a diferença no tempo de resposta entre o bcrypt e o MD5.

Verificando a senha do usuário

Para verificar se determinado hash é correto, devemos passá-lo como argumento para a função hashpw, juntamente com a senha:

>>> print bcrypt.hashpw("teste123", "$2a$12$axTaPizQ6q2V8VCLseqEq.Rwm1aZVx7oimvjPmmLWTNE3uW15xpIu")
$2a$12$axTaPizQ6q2V8VCLseqEq.Rwm1aZVx7oimvjPmmLWTNE3uW15xpIu

Se ela retornar o mesmo hash que foi passado como argumento, é porque o hash corresponde à senha passada como primeiro argumento.

Podemos então criar uma função de validação de senha de usuário:

def valida_senha(senha_digitada, hash_senha):
    return bcrypt.hashpw(senha_digitada, hash_senha) == hash_senha

Um sistema de autenticação usando BD

Vamos agora implementar um mecanismo para autenticação de usuários, usando sqlite3 e bcrypt.

# -*- encoding:utf-8 -*-
import bcrypt
import sqlite3

def valida_senha(senha_digitada, hash_senha):
    return bcrypt.hashpw(senha_digitada, hash_senha) == hash_senha

def insere_usuario(conexao, usuario, senha):
    hash_senha = bcrypt.hashpw(senha, bcrypt.gensalt())
    conexao.execute('insert into USUARIOS values ("%s", "%s")' % (usuario, hash_senha))
    conexao.commit()

def usuario_autenticado(conexao, usuario, senha):
    cursor = conexao.execute('select SENHA from USUARIOS where NOME = "%s"' % (usuario,))
    dados = cursor.fetchone()
    hash_senha = str(dados[0])
    return valida_senha(senha, hash_senha)

# alguns testes
if __name__=='__main__':
    conexao = sqlite3.connect('arquivo.db')
    insere_usuario(conexao, 'maria', 'teste')
    insere_usuario(conexao, 'joao', 'teste123')
    if usuario_autenticado(conexao, 'joao', 'teste123'):
        print 'joao está autenticado!'
    else:
        print 'Xiiiii...'

 

Antes de terminar …

O post mostrou quão simples é a implementação de um mecanismo seguro para armazenamento das senhas dos usuários em um banco de dados. Agora você não tem mais desculpas para implementar a autenticação usando senhas armazenadas em texto puro.

Se você armazena as senhas em texto puro, é fácil alterar seus algoritmos de autenticação e gerar os hashes das senhas existentes (faça backup antes, claro :)). Vale a pena gastar um tempinho corrigindo isso em seu sistema.

Finalizando, o py-bcrypt é tão simples de ser utilizado que me deixa à vontade para deixar como lição final:

LIÇÃO FINAL: USE O BCRYPT PARA ARMAZENAMENTO DE SENHAS DE USUÁRIOS!

LIÇÃO DEFINITIVA: NÃO SEJA RELAPSO COM AS INFORMAÇÕES DOS SEUS USUÁRIOS!

Leia mais sobre o assunto

Em inglês:

P.S.: obrigado ao eliasdorneles por ter revisado esse post. 🙂

Supresas divertidas em Python – Easter Eggs

Existem algumas brincadeiras que os desenvolvedores do interpretador Python “esconderam” como surpresas engraçadas para o usuário. Por exemplo, abra um shell Python e digite:

>>> import antigravity

Após digitar tal comando, será aberta uma janela do navegador com uma tirinha do xkcd, que brinca com Python: http://xkcd.com/353/

Outra brincadeira bem engraçada é a seguinte:

>>> from __future__ import braces
File "<stdin>", line 1
SyntaxError: not a chance

Repare na mensagem de erro. Ela faz uma brincadeira com o fato de programas em Python não precisarem e não utilizarem { e } para delimitar os blocos de código, isso porque Python define os blocos através da indentação do código.

O easter egg que é provavelmente o mais famoso é o seguinte:

>>> import this

Ao digitar esse comando, será impresso na tela um texto conhecido como “The Zen of Python”, que apresenta em alguns versos a filosofia empregada no desenvolvimento da linguagem.

Faça os testes você mesmo. Se você conhece algum outro easter egg em Python, poste aqui nos comentários.

Variáveis e valores

Ao criarmos uma variável em nosso código, na realidade estamos é criando um objeto e uma referência para esse objeto. Por exemplo, considere o código abaixo:

>>> x  = 100

O que fizemos foi “criar uma variável” com o valor 100 dentro dela, certo? Mais ou menos…  O que acontece, na realidade, é que 100 é um objeto do tipo inteiro que é alocado em algum lugar na memória. E x nada mais é do que uma referência (um ponteiro) para aquele objeto.

O código abaixo cria uma lista de 3 elementos e faz com que a variável x a referencie.

>>> x = [1, 2, 3]

Ao executar o código seguinte, o que irá acontecer? Errou quem acha que estamos fazendo uma cópia da lista [1, 2, 3] em y.

>>> y = x

Antes de explicar, vejamos seu conteúdo.

>>> print y
[1, 2, 3]

Ué, não é cópia mesmo? Vejamos:

>>> x.append(10)
>>> print y
[1, 2, 3, 10]

O que aconteceu? Adicionamos um valor ao final da “lista x” e ele apareceu milagrosamente na “lista y”? Não. Quando executamos y = x, fizemos com que a variável y passasse a apontar para o mesmo objeto apontado por x (a lista [1,2,3]). Assim, ao executarmos o append(10) sobre o objeto apontado por x, estamos modificando o objeto apontado por y também (que na realidade é o mesmo!).

Ou seja, ao executar y = x não fizemos uma cópia da lista, mas sim da referência a tal lista.

E como fazemos para copiar a lista? Bem, em Python existe um tipo de operação chamado de slicing, que funciona sobre objetos como as listas e que gera um novo objeto do mesmo tipo, com os elementos que estão contidos entre as posições especificadas pelos índices. Por exemplo:

>>> x = [1, 2, 3, 4, 5, 6, 7]
>>> print x[1:5]
[2, 3, 4, 5]

Essa operação gerou uma nova lista, contendo os valores das posições 1 até 5-1 (4). Se omitirmos os índices, estaremos nos referindo à lista como um todo, do início ao fim. Assim:

>>> print x[:]
[1, 2, 3, 4, 5, 6, 7]
>>> y = x[:]
>>> x.append(10)
>>> print x
[1, 2, 3, 4, 5, 6, 7, 10]
>>> print y
[1, 2, 3, 4, 5, 6, 7]

Como podemos ver no código acima, ao executarmos y = x[:], estamos gerando uma nova lista, com conteúdo idêntico à lista apontada por x, e fazendo com que y faça referência a ela. Então, para que façamos uma cópia de uma lista apontada pela variável x em uma variável y, fazemos o seguinte:

>>> y = x[:]

Então, lembre-se, para fazer uma cópia de uma lista inteira, podemos usar o “truque” do slicing e lembre-se também de que uma simples atribuição de uma variável para outra não copia a lista, mas sim a referência.

Porém, nem tudo são flores… Se a lista em questão contiver referências a outros objetos, ao invés dos objetos em si, a “cópia” feita será uma cópia rasa, pois os objetos referenciados dentro da lista não serão copiados. O contrário disso seria a cópia profunda (deep copy), onde os objetos referenciados na lista é que serão copiados, ao invés de apenas as suas referências.

Leia mais sobre as listas: http://docs.python.org/tutorial/datastructures.html

Número de argumentos variável em funções

Para quem não sabe, Python possui uma sintaxe que permite que definamos uma função com um número indefinido de argumentos. Isso pode ser feito através do * (asterisco). Por exemplo:

def func(x, *args):
    print x
    print args
func(1, 2, 3, 4, 5)

Execute o código acima, e verá a seguinte saída:

1
(2, 3, 4, 5)

Ou seja, o primeiro argumento fornecido pelo usuário (o inteiro 1) é atribuído ao primeiro parâmetro da função. Os argumentos restantes (os inteiros 2, 3, 4 e 5) são empacotados em uma tupla e passados ao parâmetro *args. Desse modo, os elementos de args podem ser acessados como os elementos de uma tupla qualquer, seja por índice, ou realizando uma travessia com for in. Por exemplo, uma função que realiza a soma de todos os argumentos recebidos:

def soma(*args):
    result = 0
    for elem in args:
        result += elem
    return result

soma(1, 2, 3, 4, 5)

A saída da execução do código acima será a soma dos valores 1, 2, 3, 4 e 5, que é 15.

Também podemos usar o * em frente ao nome de sequências (como tuplas, listas,) para fazer com que os valores que compõem tais sequências sejam desmembrados e passados como argumentos para funções. Considere e função func() abaixo:

def func(a,b,c,d):
    print a,b

Ela possui 4 parâmetros (aqui representados pelas variáveis a, b, c, d) e imprime os valores de a e de b. Certo. Imagine agora que você possui uma lista com 4 valores, que deseja passar para a função func(). Poderia fazer o seguinte:

l = [1, 2, 3, 4]
func(l[0], l[1], l[2], l[3])

Correto? Sim. Mas dá pra fazer melhor. E se eu quisesse passar a lista toda para a função, sem precisar acessar os elementos individuais.

l = [1, 2, 3, 4]
func(l)

Errado! Afinal, a função espera 4 valores e estamos passando somente um, uma lista. É aí que podemos usar o *. Veja:

l = [1, 2, 3, 4]
func(*l)
Legal, não? Em um próximo post, vou falar de dicionários com argumentos nomeados.

maketrans – Tabela de tradução de Strings

Já ouviu falar do alfabeto Leet (ou l337)? É um alfabeto empregado principalmente na internet e usado para comunicação entre pessoas, onde algumas letras do alfabeto latino são substituídas por símbolos graficamente parecidos. Utilizando alfabeto leet, a palavra STREET ficaria 57r337, por exemplo. Ou seja, o número 5 substitui a letra S, o número 7 substitui a letra T, 3 substitui a letra E.

Vamos criar uma ferramenta que traduza texto escrito usando o alfabeto tradicional para texto usando alfabeto leet. Uma forma tosca de fazermos isso é substituir todas as ocorrências de determinada letra do alfabeto tradicional por seu correspondente leet. Exemplo:

s = raw_input('Digite uma frase:')
s = s.replace('a', '4')
s = s.replace('t', '7')
s = s.replace('e', '3')
s = s.replace('s', '5')
...
print s

Porém, existe outra forma mais simples e mais elegante, usando uma Tabela de Tradução. Considere a tabela abaixo. Nela temos duas colunas, uma chamada de Entrada e outra chamada de Saída. Usando a tabela de tradução, cada letra da coluna Entrada encontrada no texto será substituída pela sua correspondente na coluna Saída.

Entrada Saída
A 4
B 8
T 7
E 3
S 5
I 1
O 0
Z 2

No módulo string, fornecido juntamente com a biblioteca padrão do Python, temos um método chamado maketrans, que, dadas duas entradas, cria uma tabela de tradução. Por exemplo, para criar a tabela de tradução apresentada acima, utilizamos o código abaixo:

from string import maketrans
entrada = 'ABTESIOZ'
saida   = '48735102'
tabela = maketrans(entrada, saida)
s = raw_input('Digite uma frase para ser convertida para leet:')
print s.translate(tabela)

As variáveis entrada e saida são strings que irão representar as colunas da tabela de tradução. Para cada ocorrência de um caractere da string entrada em uma string que será traduzida, tal caractere será substituído pelo elemento correspondente na variável saida. Por exemplo, todo caractere ‘A’ em uma string a ser traduzida será substituído pelo caractere ‘4’, e assim por diante.

Usando a mesma idéia, podemos escrever um programinha que cifre uma mensagem usando a Cifra de Caesar. Nesse tipo de cifra, cada letra de uma frase é substituída por outra letra, de acordo com um deslocamento do alfabeto tradicional. Considere como exemplo os dois alfabetos abaixo:

a b c d e f g h i j k l m n o p q r s t u v w x y z
c d e f g h i j k l m n o p q r s t u v w x y z a b

O alfabeto da segunda linha possui um deslocamento de 2 em seus caracteres. Agora, podemos cifrar textos de acordo com tais alfabetos. Por exemplo:

hello world  ---->  jgnnq yqtnf

Cada caractere da string original é substituído pelo correspondente da tabela de tradução. ‘h’ é substituído por ‘j’, e assim por diante. Quem vê a mensagem  jgnnq yqtnf  não consegue descobrir qual o significado desta, a não ser que conheça ou descubra o algoritmo utilizado para cifrá-la. É claro que, nesse caso, é muito simples descobrir. Outro exemplo de cifragem usando os mesmos dois alfabetos:

estou saindo ---->  guvqw uckpfq

Como implementar uma Cifra de Caesar simples, com deslocamento 2, em Python?

from string import maketrans

entrada = 'abcdefghijklmnopqrstuvwxyz'
saida   = 'cdefghijklmnopqrstuvwxyzbc'

def cifra_de_caesar(texto):
    tabela = maketrans(entrada, saida)
    return texto.translate(tabela)

print cifra('hello world')

Como vimos nos exemplos anteriores, o método maketrans, combinado com o translate, nos facilita muito a vida na hora de fazer a cifragem/decifragem de uma mensagem. Poderíamos fazer o mesmo com o método replace da string, mas o código ficaria muito maior e difícil de manter.

Maiores informações sobre maketrans: http://docs.python.org/library/string.html#string-functions

Retorno de múltiplos valores em funções – com tuplas

Quando aprendemos o conceito de funções no estudo de programação, nos é ensinado que uma função é um trecho de código ao qual damos um nome, que realiza uma tarefa específica, e que pode receber várias entradas de uma vez e retornar apenas um valor por vez. Até aí, tudo certo. Mas, quando estamos programando em Python, poderemos nos deparar com códigos semelhantes ao seguinte:

def func(x, y):
    return x*y, x+y

mult, soma = func(2, 3)

Pera aí! Uma função que retorna 2 valores de uma só vez? O valor da multiplicação das entradas será retornado e atribuído à variável mult, e o valor da soma das entradas atribuído à variável soma. Outro exemplo:

def func(x, y):
    return x+y, x-y, x*y, x/y, x%y, "hello"

Uma função com seis valores de retorno ao mesmo tempo? Quem, num momento de desespero e extrema gambiarra, nunca pensou em definir uma struct só para agrupar dois ou três valores que desejava que fossem retornados todos de uma vez por uma única função? Pois é, em Python é possível retornar mais de um valor em uma função. Porém, nem tudo é mágica. Para entender, vamos começar por um exemplo. Abra um shell Python e crie uma variável da seguinte forma:

>>> x = 2, 4, 10

Você deve estar pensando: “Isso não vai funcionar! Como posso eu atribuir 3 valores para apenas uma variável?”. O que está esperando para testar? Viu? Funcionou. Mas como? Calma, o próximo passo é vermos o conteúdo de x após a atribuição:

>>> print x
(2, 4, 10)

Agora, vamos verificar qual é o tipo de dados da variável x:

>>> type(x)
<type 'tuple'>

Opa! Então, quer dizer que quando eu faço return x*y, x+y, estou na verdade retornando uma TUPLA? Isso mesmo. Mais legal que isso somente o fato de Python suportar a seguinte forma de atribuição de elementos de tuplas:

x = 1 , 2, 10
a, b, c = x

Após as atribuições da segunda linha, o valor de a será 1, de b será 2 e de c será 10. Legal né? É exatamente isso que acontece quando criamos uma função com “mais de um valor de retorno”. Na realidade, nossa função possui apenas um valor de retorno, que é uma tupla. E os elementos dessa tupla são atribuídos à variáveis individuais no chamador da função.

Mais sobre tuplas: http://www.franciscosouza.com.br/aprendacompy/capitulo_09.html

Leitura de arquivos de configuração .ini em Python

Embora não seja a melhor forma possível de descrevermos a configuração de algo, os arquivos .INI ainda existem em grande quantidade. Quem nunca viu ou precisou que seu programa lesse um arquivo parecido com o seguinte?

[section1]
config1=100
config2=0
[section2]
confign=-1

Por mais que queiramos evitar tais arquivos, por serem até considerados um formado obsoleto, às vezes é necessário ler um arquivo desses para obter informações para nosso programa. Como fazer isso em Python? Ler o arquivo linha por linha? Não! Vamos usar o módulo ConfigParser.

Primeiramente, devemos importar o módulo, instanciar um objeto ConfigParser e realizar a leitura do arquivo de configuração desejado (no nosso caso, config.ini):

import ConfigParser
cfg = ConfigParser.ConfigParser()
cfg.read('config.ini')

Feito isso, o arquivo de configuração já está lido, agora basta que obtenhamos os valores que queremos extrair do arquivo. Para obter um valor do arquivo, é preciso especificar a seção e a propriedade que queremos obter. Por exemplo, o código abaixo obtém o valor da propriedade confign da seção section2 e o armazena na variável x:

x = cfg.getint('section2', 'confign')

Veja que utilizamos um método chamado getint( ) para fazer a leitura de um valor inteiro do arquivo .INI. Caso os dados a serem lidos fossem de outro tipo, poderíamos usar um dos seguintes métodos: getboolean( ), getfloat( ), ou simplesmente get( ) no caso de strings.

Com o mesmo módulo, também é possível realizarmos a escrita de arquivos .ini. Veja mais em: http://docs.python.org/library/configparser.html

Argumentos da linha de comando

Como é comum em programas de linha de comando em sistemas baseados em UNIX, é muito útil que nossos programas possuam uma “interface” de linha de comando, de forma que o usuário possa fornecer entradas ao programa diretamente pela linha de comando. Tais entradas são comumente chamadas de argumentos e são muito mais práticas para o usuário do que encher o programa de input() e raw_input(). Vejamos um exemplo de passagem de argumentos em um famoso programa para Linux:

$ ifconfig eth0 address 192.168.0.10

No exemplo acima, ifconfig é o nome do programa, e o restante (eth0 address 192.168.0.10) são os argumentos para esse programa. Por que isso é mais prático do que o seguinte?

$ ifconfig
Digite a interface de rede que deseja configurar: eth0
Digite o endereço para a interface de rede: 192.168.0.10
$

Simples. O primeiro exemplo permite que seja criado um script com entradas dinâmicas, baseadas no conteúdo de variáveis, permite fácil repetição da execução dos comandos (juntamente com as entradas) através das funções de histórico do shell, sem falar que possibilita comunicação entre programas de forma facilitada. OK, mas como fazemos para obter as entradas do usuário vindas da linha de comando em Python?

Existe mais de uma resposta para essa pergunta. Vamos à mais comum:

import sys
print sys.argv[0]
print sys.argv[1]
print sys.argv[2]

O exemplo acima mostra o uso do atributo argv do módulo sys. Através desse atributo, podemos acessar as entradas passadas pelo usuário. Considere que o código acima é o conteúdo de um arquivo chamado args.py. Se esse arquivo for executado da seguinte forma:

$ python args.py Hello world

Produzirá a seguinte saída:

args.py
Hello
world

Isso mesmo, como podemos ver, o primeiro argumento para o programa é o próprio nome do programa, e é acessado através de sys.argv[0]. A variável argv nada mais é do que uma lista. Sendo assim, seus elementos podem ser acessados via índice. E o que acontece se chamarmos o programa acima sem argumentos?

$ python args.py
args.py
Traceback (most recent call last):
  File "args.py", line 5, in <module>
    print sys.argv[1]
IndexError: list index out of range

Temos um erro de execução, pois nosso código tenta acessar elementos inexistentes na lista argv. O que fazer? Antes de acessar um elemento, devemos checar se a lista o contém.

import sys
if len(sys.argv) >= 3:
    print sys.argv[0]
 print sys.argv[1]
 print sys.argv[2]

Mas e aqueles programas que possuem diversas opções pela linha de comando, sendo algumas obrigatórias, outras opcionais, como fazem? Tratam opção por opção?

Até podem fazer isso de forma manual, mas existem ferramentas para manipulação de argumentos vindos da linha de comando que facilitam bastante o trabalho. Uma delas será o tema do próximo post: a argparse.

Até breve.

Fatiamento (slicing) de Strings em Python

Antes de falarmos de slicing, vamos ver rapidamente o que são as strings em Python. Strings em Python são objetos como outros quaisquer. Podem ser construídos com uma atribuição simples:

>>> s = "hello, world!"

Tendo feito isso, o objeto s possui disponíveis vários métodos:

>>> print s.upper()
HELLO, WORLD!
>>> print s.split(",")
['hello', ' world!']
>>> print s.split(",")[0]
'hello'
>>> print s.replace("world", "dude")
hello, dude!

Acima, são mostrados apenas alguns dos métodos disponíveis para as strings, nativamente em Python. Assim como em outras linguagens, elementos individuais de uma string podem ser acessados via índice:

>>> print s[0]
h
>>> print s[2]
l
>>> print s[12]
!

Para acessar o último elemento de uma string, podemos proceder de duas formas:

>>> print s[len(s)]
!
>>> print s[-1]
!

Uma operação muito interessante que Python fornece para manipulação de strings é o fatiamento (slicing). Fatiamento significa extrair apenas uma parte da string, ou seja, uma substring. Com essa operação, podemos delimitar os limites inferior e superior do pedaço da string que queremos acessar. Por exemplo, se quisermos acessar a substring da posição 0 até a posição 4 na string s original, podemos fazer o seguinte:

>>> print s[0:5]
'hello'
>>> print s[:5]
'hello'
>>> print s[2:4]
ll
>>> print s[7:13]
'world!'
>>> print s[7:]
'world!'
>>> print s[:]
'hello, world!'

Repare que o elemento que reside na posição do limite superior não é retornado juntamente com a string. Veja a primeira linha do código acima, s[0:5] retorna os elementos que residem entre as posições 0 e 5, incluindo a primeira e excluindo a segunda. Como mostrado acima, também podemos omitir um dos limites, ou ambos, quando queremos algo do início até posição x, ou da posição x até o fim.

OK, mas pra que serve isso? Vamos a um exemplo bem simples: extrair o protocolo de uma URL (formato: protocolo://servidor:porta/caminho/para/recurso).

url = "http://localhost:8000/arquivo.iso"
protocolo = url[0:url.index(':')]

Esse é apenas um simples exemplo do poder que essa operação tem. Cabe ressaltar aqui, que o fatiamento cria uma nova string contendo o conteúdo solicitado na operação. Ou seja, a cada operação de fatiamento, uma nova string é criada.

É isso. Faça bom proveito desse recurso que torna o código muito mais limpo e de fácil entendimento.

Ferramenta web para visualização de execução

Muitas vezes procurei por uma ferramenta legal para que os alunos pudessem visualizar a execução passo a passo de um programa escrito em Python. Nunca encontrava nada, a não ser os tradicionais depuradores. Hoje me deparei com a seguinte ferramenta: http://people.csail.mit.edu/pgbovine/python, desenvolvida no MIT, que cria uma representação visual do programa em execução, mostrando de forma didática as variáveis alocadas, bem como a execução do programa.

 

Sugiro a todos que estão iniciando seus estudos em programação que dêem uma olhada nesse projeto.

http://people.csail.mit.edu/pgbovine/python