Desafio da Cifra de César
Para explicar um pouco mais sobre strings e alguns outros assuntos, iniciei um projeto sobre criptografia no canal Python Café. O objetivo é tornar uma mensagem de texto incompreensível utilizando a cifra de César. Você pode conferir o primeiro vídeo deste projeto a seguir:
Neste vídeo, além de implementar uma versão da cifra de César, discutimos algumas decisões de projeto que são válidas de serem tomadas, levando a programas diferentes, mas que cumprem o mesmo objetivo. Ao final, deixei como desafio satisfazer uma dessas decisões. Sugeri adaptar o programa para cifrar letras em letras, de modo que após o Z maiúsculo tenhamos o A maiúsculo e após o z minúsculo, tenhamos o a minúsculo. Além disso, também sugeri que espaços, pontuações e demais caracteres fossem preservados, de modo que apenas as letras fossem cifradas, tudo isso usando a tabela ASCII como fonte dos códigos dos caracteres. Apresentarei uma resposta para este desafio neste artigo.
Em busca da Solução
Vimos que para obter o valor numérico de um caracter da tabela ASCII podemos usar a função ord(). Com esta informação, percebemos que as letras maiúsculas estão entre ord(‘A’) e ord(‘Z’), enquanto as minúsculas, entre ord(‘a’) e ord(‘z’). Logo, se um caracter estiver fora de um desses dois intervalos, ele não é uma letra e não deve ser cifrado. Vamos implementar o teste dessa condição e também mudar o nome da variável ‘letra’ para ‘caracter’ de modo a refletir essa nova abordagem.
for caracter in mensagem:
ind = ord(caracter)
if ord('A') <= ind <= ord('Z')or ord('a') <= ind <=ord('z'):
nova_letra = chr((ind + chave)%n)
# substituir na mensagem a letra pela nova_letra
cifrada = cifrada + nova_letra
Para evitar ficar portando a notação ord(‘A’), ord(‘Z’), ord(‘a’) e ord(‘z’) ao longo do programa, iremos atribuir esses valores a variáveis e utilizá-las em seu lugar. Este estilo de escrita é menos propenso a erros e também vai nos poupar um pouco de processamento.
nA = ord('A')
nZ = ord('Z')
na = ord('a')
nZ = ord('z')
Em regra geral, queremos que o novo índice seja igual ao índice do caracter observado adicionado à chave de criptografia. Contudo, no caso dessa soma ser superior ao índice de ord(‘Z’), queremos que ela passe a contar a partir do índice de ord(‘A’). Para implementar esta ordem circular em um intervalo específico dentro da tabela ASCII, vamos precisar escrever uma expressão um pouco mais complicada. Se ultrapassarmos ord(‘Z’), vamos retornar a contagem ao começo da tabela com a operação módulo e, após isso, vamos adicionar o valor do índice ord(‘A’) ao resultado para “empurrá-lo” de volta ao intervalo desejado. Seria algo nessa linha:
if (ind + chave) <= nZ:
nova_letra = chr(ind + chave)
else:
nova_letra = (ind + chave)%(nZ+1) + nA
Ou seja, quando tivermos o valor de ind + chave igual a nZ+1, o resultado de (ind + chave) %(nZ+1) será 0 (zero) e nós jogaremos esse resultado de volta para o intervalo das letras maiúsculas somando nA. Assim, o primeiro índice após nZ será nA; o segundo, 1 + nA, que é o índice correspondente a ‘B’, e assim sucessivamente. Podemos colocar essas linhas de código em uma única expressão, removendo o if e o else:
nova_letra = chr((ind + chave)%(nZ+1) + ((ind + chave)//(nZ+1))*nA)
Assim, se (ind + chave) <= nZ, temos que o resto da divisão inteira dessa expressão por nZ+1 é nulo e, portanto a parcela ((ind + chave)//(nZ+1))*nA) é igual a 0 e a operação módulo não altera o valor de (ind + chave). Caso (ind + chave) seja maior que nZ, teremos (ind + chave)//(nZ+1) igual a 1 e a expressão ficará igual àquela que colocamos no else. Note que, teoricamente, a expressão (ind + chave)//(nZ+1) poderia assumir valores superiores a 1 se a chave fosse muito grande, mas nós podemos limitar isso no programa.
Infelizmente, nessa nova abordagem, não vai ser tão simples manter as instruções referentes às letras maiúsculas e minúsculas juntas. Será mais fácil separarmos essa condição em 2 partes:
for caracter in mensagem:
ind = ord(caracter)
if nA <= ind <= nZ:
nova_letra = chr((ind + chave)%(nZ+1) + ((ind + chave)//(nZ+1))*nA)
# substituir na mensagem a letra pela nova_letra
cifrada = cifrada + nova_letra
elif na <= ind <= nz:
nova_letra = chr((ind + chave)%(nz+1) + ((ind + chave)//(nz+1))*na)
cifrada = cifrada + nova_letra
Ao invés de usarmos desigualdades para verificarmos se o índice se encontra no intervalo que queremos, podemos usar a função range(), que gera intervalos de números inteiros da forma [inicio, fim). Vou substituir a condição no elif para que você observe e se lembre disso:
elif ind in range(na, nz+1):
nova_letra = chr((ind + chave)%(nz+1) + ((ind + chave)//(nz+1))*nA)
cifrada = cifrada + nova_letra
Precisamos cobrir também o caso em que o caracter não é uma letra. Neste caso, queremos que a mensagem crifrada apenas incorpore o caracter original sem alterá-lo. Isso completa o nosso código:
chave = 3
mensagem = "Em noite de lua cheia, enquanto o Canguçu esturra, o Guatipuru sobe na Carapanaúba?"
nA = ord('A')
nZ = ord('Z')
na = ord('a')
nz = ord('z')
for caracter in mensagem:
ind = ord(caracter)
if nA <= ind <= nZ:
nova_letra = chr((ind + chave)%(nZ+1) + ((ind + chave)//(nZ+1))*nA)
# substituir na mensagem a letra pela nova_letra
cifrada = cifrada + nova_letra
elif ind in range(na, nz+1):
nova_letra = chr((ind + chave)%(nZ+1) + ((ind + chave)//(nZ+1))*nA)
cifrada = cifrada + nova_letra
else:
cifrada = cifrada + caracter
print("Original: ", mensagem)
print("Cifrada: ", cifrada)
Observe o resultado final:
Original: Em noite de lua cheia, enquanto o Canguçu esturra, o Guatipuru sobe na Carapanaúba?
Cifrada: Hp qrlwh gh oxd fkhld, hqtxdqwr r Fdqjxçx hvwxuud, r Jxdwlsxux vreh qd Fdudsdqdúed?
Como você pode observar, letras maiúsculas foram cifradas em letras maiúsculas, assim como as letras minúsculas foram cifradas em letras minúsculas. Os demais caracteres, incluindo letras acentuadas, foram mantidos inalterados. Você pode encontrar o código completo neste link. Se você tiver uma solução diferente, fique à vontade para compartilhá-la conosco!