For reference, here is your original code:
#!/usr/bin/env python
def encrypt (raw_string, shift):
encrypted_string = ""
for letter in raw_string:
zeroed_ord = ord(letter) - 96
shifted_ord = (zeroed_ord + int(shift)) % 26
if shifted_ord == 0:
shifted_ord += 26
encrypted_string += chr(shifted_ord + 96)
return encrypted_string
def decrypt(encrypted_string, shift):
return encrypt(encrypted_string, -int(shift))
if __name__ == "__main__":
while True:
choice = raw_input("This is a Caesar cipher encrypter/decrypter. Enter 'e' to encrypt, 'd' to decrypt, and 'q' to quit: ")
if choice.lower() == 'e':
raw_string = raw_input("Enter string to be encrypted. Letters only please: ")
shift = raw_input("Enter right shift: ")
print "Your encrypted string is {0}".format(encrypt(raw_string, shift))
elif choice.lower() == 'd':
encrypted_string = raw_input("Enter string to be decrypted. Letters only please: ")
shift = raw_input("Enter right shift: ")
print "Your decrypted string is {0}".format(decrypt(encrypted_string, shift))
elif choice.lower() == 'q':
break
else:
print "Invalid choice. Try again... "
Let's start by taking a look at your encrypt
method -- specifically, this line:
zeroed_ord = ord(letter) - 96
I'm not sure if this is intentional, or if this was a bug, but this isn't actually a 'zero'. Running ord('a')
actually produces 97
, not 96
. Therefore, zeroed_ord
is actually indexed starting from 1, not from zero.
As a partial consequence of that, you needed to include an if statement in order to properly handle the shifting when you do modulus.
If you adjust that 96
to 97
, then you do correctly start at zero, and you can omit the entire if statement.
Here's your adjusted encrypt
function:
def encrypt (raw_string, shift):
encrypted_string = ""
for letter in raw_string:
zeroed_ord = ord(letter) - 97
shifted_ord = (zeroed_ord + int(shift)) % 26
encrypted_string += chr(shifted_ord + 97)
return encrypted_string
In your encrypt
function, there are two sources of inefficiency. The first is that you're converting shift
into a number with each iteration of the loop. We can fix this by converting
The second is how you're building up the final string. Remember, in Python, strings are immutable -- they can never change. Consequently, when you do something like "abc" + "def"
, you're actually copying the contents of both strings over and concatenating them.
This turns out to be fairly inefficient, and leading to O(n^2)
performance. (Though in practice, Python does apply several optimizations that alleviates this problem).
So, the generally accepted way to concatenate many strings in Python is to first append them all to a list, and then join them at the end.
def encrypt(raw_string, shift):
encrypted_string = []
shift = int(shift)
for letter in raw_string:
zeroed_ord = ord(letter) - 97
shifted_ord = (zeroed_ord + shift) % 26
encrypted_string.append(chr(shifted_ord + 97))
return ''.join(encrypted_string)
Not only is this more efficient, but it also paves the path to the next transformation.
I think now is a good point to introduce the concept of "higher-order programming". In essence, your encrypt
function suffers from the following flaw: it mixes together implementation with intent.
If you think about it, what your encrypt
function is doing is taking in a sequence of some items (the original sentence), transforming it, and returning the transformed sequence. In order to make this work, you need to do three separate things: iterate through the string, transform the characters, and build up a new list.
This is a very common pattern in coding. You have an input sequence, you transform, and you return the output. A common habit many beginning programmers have is that they don't recognize this common pattern, and keep re-implementing it (writing for loops, while loops, etc.)
While at first, their code works, it becomes increasingly messier and messier as their loop starts getting more and more complex. For example, what if you need to transform one list into another, but only if some condition is true, then take that output and transform it again, then aggregate it together, etc...
In this case, it may seem difficult to find a way to abstract this out. How do you abstract the idea of a for
loop? Of a transformation? Most beginning coders are used to the idea of abstracting data (using lists, dicts, objects), but not so much with abstracting the idea of control flow.
We can take our cues from functional programming to try and find a way to abstract this.
The first thing I'll do is move the code to shift a single letter into its own function:
def shift_letter(letter, shift):
zeroed_ord = ord(letter) - 97
shifted_ord = (zeroed_ord + shift) % 26
return chr(shifted_ord + 97)
def encrypt(raw_string, shift):
shift = int(shift)
encrypted_string = []
for letter in raw_string:
encrypted_string.append(shift_letter(letter, shift))
return ''.join(encrypted_string)
Now, if you notice, it's currently taking us exactly three lines to perform the next steps: looping and creating a new list. What we can do is use something called a list comprehension -- a construct Python offers to generalize this pattern. Here's what it looks like:
def shift_letter(letter, shift):
zeroed_ord = ord(letter) - 97
shifted_ord = (zeroed_ord + shift) % 26
return chr(shifted_ord + 97)
def encrypt(raw_string, shift):
shift = int(shift)
encrypted_string = [shift_letter(letter, shift) for letter in raw_string]
return ''.join(encrypted_string)
This fragment of code is exactly identical to the one up above, but makes our intent more clear by not entangling the looping and the transforming. When we use a list comprehension, it implicitly states that the primary goal of the function is to transform some sequence, and that there are no additional side effects.
Granted, this is not the best example of functional-style programming. Pedagogically, this isn't a good example to really introduce the concept because the current example introduces several complications that makes it hard to immediately show all the nifty things you can do.
Regardless, I'll re-introduce the topic in future feedback writeups, so just keep the idea percolating in the back of your mind (and look up list comprehensions
for a much better introduction to the subject).
There were a few minor things you could fix.
- You convert the shift to an int in decrypt, yet pass in a string when literally calling encrypt. It'd be better to convert the shift to an integer immediately after recieving it so that we pass in a uniform data type to the encrypt function.
- You call
choice.lower()
in multiple places -- it'd be better to do it once.