I am just beginning to learn Python, this is my own take on a Caesar cipher just looking at Python docs. I would like to know how I could have made it better and how would you rate this. Is it any good?
#caeser cipher
import string
message = str(input("Enter the Message you want to encrypt: "))
shift = int(input("Enter the number of letters for cipher shift: "))
def cipher(message,shift):
encList = []
messageLst = []
alphabet = list(string.ascii_lowercase * 2)
punct = list(string.punctuation)
for i in message:
messageLst.append(i)
for i in messageLst :
for j in alphabet:
if i == j:
replaceChar = alphabet[alphabet.index(j)+(shift)]
encList.append(replaceChar)
break
elif i == " ":
encList.append(i)
break
elif i in punct:
encList.append(i)
break
encMessage = ""
encMessage = encMessage.join(encList)
#print(alphabet)
#print(messageLst)
#print(encList)
print("Encrypted Message is : ", encMessage)
cipher(message,shift)
2 Answers 2
Alphabet list
Alphabet doesn't need to be a list
, you can just use the ascii_lowercase
string:
# just a copy
alphabet = string.ascii_lowercase
# two copies
alphabet = string.ascii_lowercase * 2
# alternatively
from string import ascii_lowercase as alphabet
Using the Modulo Operator
Instead of using two copies of ascii_lowercase
to cover index overflows, you can use the modulo operation as shown by your linked Wikipedia entry:
alphabet = string.ascii_lowercase
shift = 5
# index is in range(0, 25)
alphabet[(6 + shift) % 26]
'l'
# index would be > 25, so it loops back around
alphabet[(21 + shift) % 26]
'a'
Cipher Lookup with a Dictionary
Now, what you could do is use a dictionary to build the lookups ahead of time so that you don't have to use alphabet.index
:
cipher_lookup = {char: alphabet[(i + shift) % 26] for i, char in enumerate(alphabet)}
Now, there's no more need to track indices:
def cipher(message, shift):
cipher_lookup = {char: alphabet[(i + shift) % 26] for i, char in enumerate(alphabet)}
encrypted = []
for letter in message:
# simply look it up in the dictionary
encrypted.append(cipher[letter])
Checking for Punctuation
Use a set
here, rather than a list. Membership testing for a set
/dict
is a time-constant operation, rather than O(N), where N is the length of the list:
punct = set(string.punctuation)
'a' in punct
False
'.' in punct
True
However, you don't actually have to test for punctuation. You could instead use dict.get
to return the value from the dictionary if it's there, and return the letter if it isn't:
# say shift is 5
cipher_lookup.get('a', 'a')
'f'
# punctuation is not in the cipher
# so we just return that character
cipher_lookup.get('.', '.')
'.'
Predefining encMessage
You can just use:
enc_message = ''.join(enc_list)
Variable Naming
Variable and function names are to be snake_case
with all lowercase letters
Function params
Separate your parameters in your function definitions with a space:
def cipher(message, shift):
input
You don't need to cast message
to str
, since input
only outputs strings
if __name__ == "__main__"
Guard
This will allow you to import this function without executing the program if you wanted to reuse it:
# This goes at the very bottom
if __name__ == "__main__":
message = input("Enter the Message you want to encrypt: ")
shift = int(input("Enter the number of letters for cipher shift: "))
print(cipher(message, shift))
You can put your message
and shift
prompts here as well so that they don't execute unless you are running the program.
Refactored
from string import ascii_lowercase as alphabet
def cipher(message, shift):
cipher_lookup = {char: alphabet[(i + shift) % 26] for i, char in enumerate(alphabet)}
encrypted_message = ""
for letter in message:
encrypted_message += cipher_lookup.get(letter, letter)
return encrypted_message
if __name__ == "__main__":
message = input("Enter the Message you want to encrypt: ")
shift = int(input("Enter the number of letters for cipher shift: "))
print(cipher(message, shift))
String concatenation
It's not usually best practice to use string concatenation, but I think this is a good start to demonstrate the concepts of what's going on in the program. A better practice than the for
loop would be to use a generator expression:
encrypted_message = ''.join((cipher_lookup.get(letter, letter) for letter in message))
C.Nivs has made excellent points about:
- not needing to convert
alphabet
into a list - using the Modulo Operator (
%
) - storing the translations in a dictionary
- using
set
/dict
andin
for membership tests - naming, spaces, unnecessary casts and initializations
__main__
guards
I won't repeat those here, but please study those and follow their advice on those points.
Additional Code Review Points
Loop Invariants
You wrote this double for
loop:
for i in messageLst :
for j in alphabet:
if i == j:
replaceChar = alphabet[alphabet.index(j)+(shift)]
encList.append(replaceChar)
break
elif i == " ":
encList.append(i)
break
elif i in punct:
encList.append(i)
break
The tests i == " "
and i in punct
are nested inside the inner loop, which loops for j in alphabet
. Notice that neither those tests nor the code executed based on passing either of those tests depends on j
. This is inefficient.
Consider a messageLst
which contains ['z', 'e', 'b', 'r', 'a']
. The first iteration of the outer loop
- assigns
'z'
to the variablei
, and then - executes the body of that loop, which is the inner loop. That loop:
- assign
j
to the first letter ofalphabet
(an'a'
), and sincei == j
is false, goes on to check ifi
is a space, and if not, ifi
is a punctuation character. Since both were false, the loop continues and, - assign
j
to the second letter ofalphabet
(a'b'
), and sincei == j
is false, goes on to check ifi
is a space, and if not, ifi
is a punctuation character. Since both were false, the loop continues and, - assign
j
to the third letter ofalphabet
(a'c'
), and sincei == j
is false, goes on to check ifi
is a space, and if not, ifi
is a punctuation character. Since both were false, the loop continues and,
- assign
Notice those checks for i == ' '
and i in punct
are redundantly executed ... 26 times in case of the letter 'z'
!
If those tests are moved out of the loop, we can get a more efficient implementation:
for i in messageLst :
if i == " ":
encList.append(i)
elif i in punct:
encList.append(i)
else:
for j in alphabet:
if i == j:
replaceChar = alphabet[alphabet.index(j)+(shift)]
encList.append(replaceChar)
break
Unnecessary looping
Consider just the inner loop:
for j in alphabet:
if i == j:
...
break
This searches alphabet
for values j
that equals i
, and (due to the break
) stops at the first occurrence. When that occurrence is found, j
will equal i
.
This is a complicated way of writing:
if i in alphabet:
j = i
...
In fact, you don't even need the extra j
variable. You can simply use i
in the ...
code.
if i in alphabet:
replaceChar = alphabet[alphabet.index(i) + shift]
encList.append(replaceChar)
Improved code
Fixing the loop invariant and removing the unnecessary looping (but without applying improvements from the other answer), we get:
for i in messageLst :
if i in alphabet:
replaceChar = alphabet[alphabet.index(i) + shift]
encList.append(replaceChar)
elif i == " ":
encList.append(i)
elif i in punct:
encList.append(i)
... which is certainly an improvement over the original code.
Batteries Included
C.Nivs also gave you improvements on string concatenation, culminating with a generator expression to produce the encrypted message. We can do even better...
Python comes with a str.translate
function, which does a letter-by-letter substitution on a string ... which is exactly what the Caesar cipher is doing.
from string import ascii_lowercase as alphabet
def cipher(message: str, shift: int) -> str:
"""
Encode a message using a Caesar Cipher with a user-defined shift on
a 26 letter lowercase alphabet.
"""
shift %= len(alphabet)
code = str.maketrans(alphabet, alphabet[shift:] + alphabet[:shift])
return message.translate(code)
if __name__ == "__main__":
message = input("Enter the Message you want to encrypt: ")
shift = int(input("Enter the number of letters for cipher shift: "))
print(cipher(message, shift))
Note: I've added a """docstring"""
and type-hints to the cipher
function. Both are extremely useful in Python development. Make sure you study them.
-
1\$\begingroup\$ Great answer! Minor nitpicks, just reading the code the part
code = ...
is a bit terse, addingshifted_alphabet = alphabet[shift:] + alphabet[:shift]
above would make it clearer.len
of alphabet is called each time a new cipher is called. Maybe make it a constantALPHABET_LEN = len(alphabet)
. Some minor checking on the inputs would be nice. What happens ifshift
is not an int? Also some very briefdoctests
would makecipher
clearer as it is a bit terse.code
is better namedtranslation_table
orcipher
=). \$\endgroup\$N3buchadnezzar– N3buchadnezzar2021年09月09日 20:54:22 +00:00Commented Sep 9, 2021 at 20:54 -
1\$\begingroup\$ As a last comment =)
str
is imho not helping much. Typing hints should ideally hint at what is going on. I find it clearer addingfrom typing import Annotated
and then do something likePlaintext = Annotated[str, "a plaintext to be ciphered"]
,Ciphertext = Annotated[str, "an encrypted plaintext using a cipher"]
and thendef cipher(message: Plaintext, shift: int) -> Ciphertext:
. \$\endgroup\$N3buchadnezzar– N3buchadnezzar2021年09月09日 21:09:37 +00:00Commented Sep 9, 2021 at 21:09
Explore related questions
See similar questions with these tags.