In this lab you will gain practice writing functions by implementing Julius Caesar’s cipher to encode and decode secret 4-letter strings. For example, the string "code"
encodes to "dpef"
. In a later exercise you will improve your functions to support encoding and decoding str
values of any length. Soon you’ll be able to send your crush secret love messages on zoom.
Before beginning, be sure you have completed Lesson 8 on Functions, Lesson 9 on Constants, and handed-in their respective questions on Gradescope.
The Caesar Cipher is one of the simplest ways to encode a message. It’s easily cracked. The idea is each letter is replaced by a letter some offset away from it in the alphabet. For our purposes, we will assume an offset shift of 1.
Thus, the mappings are:
a -> b
b -> c
c -> d
....
y -> z
z -> a
Notice when z
is encoded it “wraps around” to a
.
ord
functionUnder the hood our computer uses numeric codes to represent characters, or str
s of length one. The standard encoding is ASCII (“American Standard Code for Information Exchange”) which uses 7 binary digits (0s and 1s) to reresent each character. You’ll learn about this more in later classes, so no worries about the details here. The table of codes can be found here: https://en.wikipedia.org/wiki/ASCII#Printable_characters. In the table, the Dec
column is the int
value of each character and the Glyph
column contains the character representation. Scroll down to Dec
97
to see the lowercase range.
For this exercise, you will make use of the built-in ord
. The ord
function is short for “ordinal” and it gives the numerical int
ordering of a single-character str
in ASCII. For example, the character n
has corresponds to the int
value 110
in ASCII per the table linked above. You can confirm this with the following demo code:
>>> character: str = "n"
>>> ascii_code: int = ord(character)
>>> print(ascii_code)
110
chr
function.According to our cipher mapping, we want to encode each character with the letter following it in the alphabet. We can do this by performing integer arithmetic on the ascii_code
.
Building on the previous example, we will encode the character
variable, whose value is n
, and store the result in encoded_character
, whose value we expect to be o
.
The built-in chr
function, as first demonstrated in lesson 3, can be given an int
ASCII code and return a str
made of its corresponding character.
>>> character: str = "n"
>>> ascii_code: int = ord(character)
>>> encoded_ascii_code: int = ascii_code + 1
>>> encoded_character: str = chr(encoded_ascii_code)
>>> print(encoded_character)
o
Try running this code yourself in a REPL to make sure you are confident about what is going on!
What about if we have to wrap around? In the case of encoding z
, we can’t just add 1 to get back to a
. The character z
’s ASCII code is 122
while a
is 97
.
Some clever arithmetic, in conjunction with our good friend the remainder operator %
, can help us achieve this! Since there are 26 lowercase letters, we can take the remainder of dividing any number by 26 and are guaranteed for it to result in a value of 0
through 25
. This would be great if a
was 0
and z
was 25
, because then (25 + 1) % 26
would be 0
which is a
! The problem is, lowercase ASCII codes begin at 97
with a
and end at 122
with z
. For a fun challenge, pause here and see if you can figure out how we’ll get around this conundrum.
Are you ready for the spoiler? Ok, here goes! An oft-used trick in situations like this, (this comes up in 3D graphics, too!), is we’ll momentarily normalize our value so that a
is 0
and z
is 25
by subtracting a
’s ASCII value which is 97
. Then, we’ll encode by adding 1 and performing the remainder operation, finally we’ll denormalize by adding a
’s ASCII value back so that we once again have a number in the correct range for lower case letters. This modification still works for all letters in the alphabet, not just z
.
>>> character: str = "z"
>>> ascii_code: int = ord(character)
>>> normalized_code: int = ascii_code - 97
>>> encoded_code: int = (normalized_code + 1) % 26 + 97
>>> encoded_character: str = chr(encoded_code)
>>> print(encoded_character)
'a'
Give it a try in a REPL to convice yourself these steps work for any lowercase character
. No worries if this still seems a bit fuzzy or magical – we are more worried about the function writing in the next two sections.
Now that you have a sense for the steps to encode a single character string using a caeser cipher, let’s define a function to abstract away the details of all those steps into functions which are easier to use.
Open the course workspace in VS Code (File > Open Recent > comp110-workspace) and open the File Explorer pane. Expand exercises
.
Right click on the word exercises
and select “New Folder”.
Name the folder exactly: ex02
cipher.py
EncodingRight click on the ex02
folder you setup above and select “New File”, name the file cipher.py
. In this part of the exercise, we will be walking through how to encode a str
.
encode_char
functionNow that you’ve experimented with the process of encoding a single-length str
, it is your turn to write a function which carries out those same steps.
Define a function named encode_char
that given a single-length str
parameter returns the encoded version of that character.
Once you’ve defined your encode_char
function, save your work and try importing the function definition into a REPL to try out the function calls shown below.
>>> from exercises.ex02.cipher import encode_char
>>> encode_char("c")
'd'
>>> encode_char("z")
'a'
Wow! Behold the power of process abstraction! Once you have correctly implemented your function, notice how much easier it is to encode a single character using the function than moving through all those earlier steps!
encode_char
encode_char
str
that can be assumed to be single-lengthstr
str
should be the parameter given shifted one letter to the right in the alphabet, per the mapping shown above.ord
and chr
built-in functions.lower
method:
some_variable_name: str = parameter_name.lower()
. The lower
method is built-in to Python str
objects and converts any uppercase characters in a string to lowercase.encode_str
function to encode 4-letter stringsNow that your encode_char
function is complete, you can use it to implement a function for encoding str
s of lengths greater than 1. For the purposes of this exercise, we will assume length 4. In a future exercise, you will rewrite this function to work with a str
of any length, but we need to learn a few more concepts first.
Define a new function called encode_str
that takes in a str
parameter and returns the encoded version of that str
.
Here’s an example of how you should be able to use the function, once implemented:
>>> from exercises.ex02.cipher import encode_str
>>>> encode_str("abcz")
'bcda'
encode_str
encode_str
str
that can be assumed to have a length of 4.str
of length 4.str
with each letter shifted one to the right.encode_char
inside this function for each letter of the parameter.str
indexing to get the individual characters without looping. Hint: you’ll want to use str
indexing.cipher.py
DecodingNow that we have learned how to encode a str
, we want undo this operation and decode
it. These next two functions will look VERY similar to the ones you wrote in Part 1. This is expected – the main goal of this exercise is to get comfortable with the function writing process.
decode_char
functionDefine a function named decode_char
that given a single-length str
parameter returns the originial, unencoded version of that character.
Example function call:
decode_char("c") --> returns "b"
decode_char
decode_char
str
that can be assumed to be single-lengthstr
str
should be the parameter given shifted one letter to the left in the alphabet, per the mapping shown above.ord
and chr
built-in functions.some_variable_name = <parameter_name>.lower()
. The lower
function is built into python and converts any string into lowercase.decode_str
function to decode 4-letter stringsWe can now use our decode_char
function to help us decode str
s of length 4. Define a new function called decode_str
that takes in a str
parameter and returns the original, unencoded version of that str
.
Example function call:
decode_str("bdef") --> returns "acde"
decode_str
decode_str
str
that can be assumed to have a length of 4.str
of length 4.str
with each letter shifted one to the left.decode_char
inside this function for each letter of the parameter.str
indexing to get the individual characters without looping. Hint: you’ll want to use str
indexing.To check that your functions work as expected, you can load your cipher
file into a REPL by opening up a new REPL
, and then running from exercises.ex02.cipher import encode_char, encode_str, decode_char, decode_str
. From here you can practice calling your functions and seeing if the results match what you expected. Once you have completed parts 1 and 2, you should be able to try combining function calls to decode and encode.
For example, decode_char
and encode_char
are inverses of each other. This means if you apply encode_char
to something followed by decode_char
, you should end up with what you started with.
decode_char(encode_char("a")) --> returns "a"
This same property holds for decode_str
and encode_str
. You should see the following behavior:
decode_str(encode_str("flex")) --> returns "flex"
For the both parts of the exercise, we will manually grade your code and are looking for good choices of meaningful variable names. Your variable names should be descriptive of their purposes. We will also manually grade to check that you declared your variables with explicit types.
Once your program is working, add a docstring at the top of your file with a one-sentence description of your program’s purpose.
Then, after the docstring but before your program’s code, add your __author__
variable assigned to your name and e-mail address.
Lastly, there should be no magic numbers in your code. Make any magic numbers into named constants. Hint: In the program snippets above there are 3 magic numbers. More info on this can be found here.