Python Forum
Caesar cipher - Printable Version

+- Python Forum (https://python-forum.io)
+-- Forum: Python Coding (https://python-forum.io/forum-7.html)
+--- Forum: Homework (https://python-forum.io/forum-9.html)
+--- Thread: Caesar cipher (/thread-13676.html)

Pages: 1 2


RE: Caesar cipher - nilamo - Nov-02-2018

>>> for index, value in enumerate(["spam", "cat", "fish", "bar"]):
...   print(f"{index} => {value}")
...
0 => spam
1 => cat
2 => fish
3 => bar
What you're calling variable is actually the index in shifted that the char is located. And because it's an index, it's an int, so it's the same as...
>>> x = 5
>>> x["a"] = []
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object does not support item assignment
With that context, hopefully the error you're getting makes more sense.

I could stop here, but there's a couple other things which will trip you up almost right after that...
Quote:
    text = list(text) # Convert text to string
    print(text) # Confirmation of conversion operation
    scrambled_text =[] # Initializing output variable 
    for index in text:

list(some_thing) will convert some_thing into a list, not a string. Your text is already a string, you're converting it into a list (though it doesn't actually matter, since both lists and strings are iterable and indexable, so you should be able to just comment that line out).

for index in text:, no, if you loop over a string or list, the iteration variable will be each value of what you're iterating over. If you want the index, that's when you use enumerate(). Here's an example:

>>> for character in "foobarspam":
...   print(character)
...
f
o
o
b
a
r
s
p
a
m
And with all that said, you're very close to being done. :)


RE: Caesar cipher - Drone4four - Nov-04-2018

(Nov-02-2018, 05:35 PM)nilamo Wrote:
>>> for index, value in enumerate(["spam", "cat", "fish", "bar"]):
...   print(f"{index} => {value}")
...
0 => spam
1 => cat
2 => fish
3 => bar
What you're calling variable is actually the index in shifted that the char is located. And because it's an index, it's an int, so it's the same as...
>>> x = 5
>>> x["a"] = []
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object does not support item assignment

I think I see where you are going. When referring to an index, the slicing call must be an integer whereas in my case I was slicing using a string. So I tried a few different integers in place of char at line 21 (like I separately tried, 0, 1,and -1). That got rid of the 'int' object TypeError but now I've got a str error, described at the end of this forum post.

Quote:list(some_thing) will convert some_thing into a list, not a string. Your text is already a string, you're converting it into a list (though it doesn't actually matter, since both lists and strings are iterable and indexable, so you should be able to just comment that line out).

I understand what you are saying about how it might not be necessary to cast my string into a list however strings are immutable. So I need to convert the string into a list so I can swap out the letters. The plan was to then at the end of these operations, join each item in the list of scrambled characters back together into string formatting.

I have played around with about 4 or 5 different combinations of index, char and enumerate(text/shifted) and here I am mashing my keyboard still unable to figure this out.

Here is the closest I’ve come:

from collections import deque
import string
import copy
 
def encrypt(text,shift_variance):
    '''
    INPUT: text as a string and an integer for the shift value.
    OUTPUT: The shifted text after being run through the Caesar cipher.
    ''' 
    original = string.ascii_lowercase # Initializing alphabet variable
    original = deque(list(original)) # Turning the original alphabet into a list
    shifted = original.copy() # Assigning new variable to copy of original alphabet
    shifted.rotate(shift_variance) # Rotating new shifted alphabet 
 
    # BEGIN for loop:
    # text = list(text) # Convert text to string
    print(text) # Confirmation of conversion operation
    # scrambled_text =[] # Initializing output variable 
    for index, value in enumerate(shifted):
        # for variable, char in enumerate(shifted):
        print(f"{index} => {value}")   
    for index, character in enumerate(text):
        character[0] = the corresponding character in shifted_alphabet
        print(character)
    pass
As you can see I’ve commented out the line where I cast the text string into a list (even though I think it is still necessary).

Lines 19-21 are there to demonstrate the point you made about enumeration except I’ve put the variables in the context of the variables and semantics in my script.

I feel like line 22 is the way it should be. At line 23 instead of slicing using the character (a string) I use an integer 0. This does away with the Int object TypeError. But now Anaconda throws: SyntaxError: invalid syntax. I get that line 23 is completely wrong. I can't for the life of me figure out how to re-write this pseudo code in Python code.

Do I need to add a nested loop at line 23?

Thank you @nilamo for your help so far and thank you for your continued patience as I take my first baby steps in writing my first Python program.


RE: Caesar cipher - stullis - Nov-04-2018

On line 23, you cannot slice character. Character is a single alpha character. By iterating over text, you are already retrieving individual characters from text. Line 23 can be rewritten as:

character = the corresponding character in shifted_alphabet
I don't think enumerate() is the best way to go about this. Since you have both original and shifted, you could use zip() instead to directly connect the two:

original = string.ascii_lowercase
original = deque(list(original))
shifted = original.copy()
shifted.rotate(3)

for char1, char2 in zip(original, shifted):
    print(f"{char1} to {char2}")
With that in mind, I recommend using zip() to combine them and then converting the result into a dictionary. Then, you can iterate over the clear text and use each character to index the dictionary for the cipher character.


RE: Caesar cipher - nilamo - Nov-04-2018

(Nov-04-2018, 02:36 AM)Drone4four Wrote: I understand what you are saying about how it might not be necessary to cast my string into a list however strings are immutable.
Sure, that's one way to do it, by assigning a new value to the list's index, but because of this line...
(Nov-04-2018, 02:36 AM)Drone4four Wrote:
scrambled_text =[] # Initializing output variable
...I had assumed you were building a new list that contained the shifted characters, without modifying the original list.

Quote:
character[0] = the corresponding character in shifted_alphabet
You have the character you want to rotate. So the next step would be to find where that character is in the original. Once you know the index of that, you use the same index to look up what the shifted character is in shifted. Something like
for index, character in enumerate(text):
    lookup_index = original.find(character)
    new_character = shifted[lookup_index]

    # and then either...
    text[index] = new_character
    # ...or
    scrambled_text.append(new_character)
So if you keep the text = list(text) line, you can assign a new value like so: text[index] = new_character. Index the list itself, not a particular character within that list (which is a syntax/unsubscriptible error).

Way earlier in the thread, I mentioned how we'd look at a "better" way to do this once you got it working. That better way is a translation table.
# original and shifted are from your code
shifted = ''.join(shifted) # convert to a string
# build a translation table, mapping one character to it's shifted version
translation = str.maketrans(original, shifted)
# now translate the text
translated_text = str.translate("some text", original, shifted)
print(translated_text)
...though your teacher probably wouldn't want to see that, lol


RE: Caesar cipher - Drone4four - Nov-06-2018

(Nov-04-2018, 12:04 PM)stullis Wrote: With that in mind, I recommend using zip() to combine them and then converting the result into a dictionary. Then, you can iterate over the clear text and use each character to index the dictionary for the cipher character.

@stullis: I really like the idea of zipping two lists into a dictionary instead of working with lists and enumerate. I’ll get back to a potential zip/dictionary algorithm later. For now I am going to run with @nilamo’s advice.

(Nov-04-2018, 09:44 PM)nilamo Wrote: Earlier in the thread, I mentioned how we'd look at a "better" way to do this once you got it working. That better way is a translation table.
# original and shifted are from your code
shifted = ''.join(shifted) # convert to a string
# build a translation table, mapping one character to it's shifted version
translation = str.maketrans(original, shifted)
# now translate the text
translated_text = str.translate("some text", original, shifted)
print(translated_text)
...though your teacher probably wouldn't want to see that, lol

This translation table solution is elegant. In the Udemy course I am taking and in the particular module I am working on calls students to practice with functions and loops. You seem to have completed the exercise using casting methods, which is pretty awesome and terrific for my reference. But for now I am going to explore your initial advice to use a for loop with enumerate(). You are probably right that the Udemy instructor wouldn't want to see this. I know what you mean. ;)

My new encrypt functions now looks like this:
from collections import deque
import string
import copy
 
def encrypt(text,shift_variance):
    '''
    INPUT: text as a string and an integer for the shift value.
    OUTPUT: The shifted text after being run through the Caesar cipher.
    ''' 
    original = string.ascii_lowercase # Initializing alphabet variable
    original = deque(list(original)) # Turning the original alphabet into a list
    shifted = original.copy() # Assigning new variable to copy of original alphabet
    shifted.rotate(shift_variance) # Rotating new shifted alphabet 
    text = list(text) # Convert text to string
    print(text) # Confirmation of conversion operation
    scrambled_text =[] # Initializing output variable 
    for index, character in enumerate(text):
        lookup_index = original.find(character)
        new_character = shifted[lookup_index]
        scrambled_text.append(new_character)
    print(scrambled_text)
When I call the function with encrypt("Hello World",3), it shows this traceback:

Quote:['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!', ' ', 'F', 'e', 'e', 'd', ' ', 't', 'h', 'e', ' ', 'b', 'i', 'r', 'd', 's', ' ', 'a', 'n', 'd', ' ', 'r', 'e', 'l', 'e', 'a', 's', 'e', ' ', 't', 'h', 'e', ' ', 'p', 'i', 'g', 'g', 'i', 'e', 's', '!']
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-2-b5941c11deb0> in <module>()
----> 1 encrypt("Hello World! Feed the birds and release the piggies!",1)

<ipython-input-1-b1a366fa3185> in encrypt(text, shift_variance)
16 scrambled_text =[] # Initializing output variable
17 for index, character in enumerate(text):
---> 18 lookup_index = original.find(character)
19 new_character = shifted[lookup_index]
20 scrambled_text.append(new_character)

AttributeError: 'collections.deque' object has no attribute 'find'

I initially thought that the find method here indicated that it needs to be imported from a library, but I learned that it's built in. So there is no need to import anything. find() should just work. The AttributeError also points to collections.deque which was the function that I invoked at line 11. I figured this might be referring to how original is a list whereas find()only works on strings. So I re-added:

Quote: original = ''.join(original)
shifted = ''.join(shifted)

This re-concatenates the two lists. When I add these two lines before the for loop, the AttributeError disappears, which is awesome. So here is my code again, but this time with these two lines added:

from collections import deque
import string
import copy
 
def encrypt(text,shift_variance):
    '''
    INPUT: text as a string and an integer for the shift value.
    OUTPUT: The shifted text after being run through the Caesar cipher.
    ''' 
    original = string.ascii_lowercase # Initializing alphabet variable
    original = deque(list(original)) # Turning the original alphabet into a list
    shifted = original.copy() # Assigning new variable to copy of original alphabet
    shifted.rotate(shift_variance) # Rotating new shifted alphabet 
    original = ''.join(original) # Re-concatenating split list (alphabet)
    shifted = ''.join(shifted)
    text = list(text) # Convert text to string
    scrambled_text =[] # Initializing output variable 
    for index, character in enumerate(text):
        lookup_index = original.find(character)
        new_character = shifted[lookup_index]
        scrambled_text.append(new_character)
    text = ''.join(text)
    scrambled_text = ''.join(scrambled_text)
    print(text)
    print(scrambled_text)
Now when I call the encrypt function with encrypt("Hello World",3) - - Eureka! - - I get this output:
Quote:Hello World
wbiilwwloia

It’s a scrambled Caesar Cipher! I am so thrilled! Thank you. Although I am not done yet. It’s not quite perfect. There should be a space between “wbill” and “wloia” rather than an extra ‘w’ in the middle, right?

I suppose the issue is more clearly present when passing a function both with a longer sentence as a string and with a single shift key variance:
encrypt("Hello World! Feed the birds and release the piggies!",1)
Quote:Hello World! Feed the birds and release the piggies!
ydkknyynqkcyyyddcysgdyahqcryzmcyqdkdzrdysgdyohffhdry

So my two remaining questions are:
  1. How do you account for why the Python interpreter is producing output without spaces between words? Could any of you provide a clue as to what I could try next to better control spacing between words? I'm thinking I could use a conditional some how.
  2. With a shift variance of 1, each letter is moved one position to the right. But there is way more than 1 character difference between H and y. How would you account for this discrepancy?

Thanks to you both @stullis and @nilamo for your insight so far. I apologize for the multi-day gap in between my forum posts. I look forward to reading your next replies (and from other members on this forum) in this drawn out thread. Thanks again for your continued patience with my novice questions.


RE: Caesar cipher - stullis - Nov-06-2018

You're getting the extra "w"s because of an interaction between the output of str.find() and str[index]. The "original" string does not include a space as a character, therefore, str.find() returns a -1 per the documentation. If you index an iterable with a -1, the interpreter returns the last item in the iterable:

x = "abc"
print(x[-1]) # Prints "c"
So, every time your loop encounters a space...

  1. it searches original for the character
  2. fails to find it and returns a -1
  3. indexes shifted with a -1 to return the last character ("w" in this case)
  4. appends that character in place of the space

To correct this, you'll either have to control for a -1 result from line 19 or rewrite the loop to avoid the issue. You could split "text" by spaces and then join it again later after encrypting.


RE: Caesar cipher - Drone4four - Nov-09-2018

I only partially understand.

@stullis: As you say say:

(Nov-06-2018, 02:50 AM)stullis Wrote: You're getting the extra "w"s because of an interaction between the output of str.find() and str[index]. The "original" string does not include a space as a character, therefore, str.find() returns a -1 per the documentation. If you index an iterable with a -1, the interpreter returns the last item in the iterable:
x = "abc"
print(x[-1]) # Prints "c"

I understand this clearly. The last character in a string or the last item in a list can be referred to by adding a -1 in square brackets next to the variable that the list or string is assigned to.

@stullis, you continue:

Quote:So, every time your loop encounters a space...
  1. it searches original for the character
  2. fails to find it and returns a -1
  3. indexes shifted with a -1 to return the last character ("w" in this case)
  4. appends that character in place of the space

The builtin find() method attempts to locate the character. When it encounters a ’ ‘, it just returns the character at position -1 in the list (as in my example, ‘w’, as you pointed out and as detailed in the official Python docs). That what is going on in the loops (lines 18-21) in my script. I get it.

Then @stullis says:

Quote: To correct this, you'll either have to control for a -1 result from line 19 or rewrite the loop to avoid the issue. You could split "text" by spaces and then join it again later after encrypting.

I’ve resolved to split the text into a list of words, instead of a list of characters. text is processed in the loop like this:

Quote:['Hello', 'World!', 'Feed', 'the', 'birds', 'and', 'release', 'the', 'piggies!']

Instead of this:

Quote:['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!', ' ', 'F', 'e', 'e', 'd', ' ', 't', 'h', 'e', ' ', 'b', 'i', 'r', 'd', 's', ' ', 'a', 'n', 'd', ' ', 'r', 'e', 'l', 'e', 'a', 's', 'e', ' ', 't', 'h', 'e', ' ', 'p', 'i', 'g', 'g', 'i', 'e', 's', '!']

To account for this change to the structure of my sentence, I’ve added [:][:] to the reference of text inside the enumerate function. This way, enumerate() will cycle over each word and then cycle of each character in those words. That’s my intention. Here is my script now:

from collections import deque
import string
import copy
 
def encrypt(text,shift_variance):
    '''
    INPUT: text as a string and an integer for the shift value.
    OUTPUT: The shifted text after being run through the Caesar cipher.
    ''' 
    original = string.ascii_lowercase # Initializing alphabet variable
    original = deque(list(original)) # Turning the original alphabet into a list
    shifted = original.copy() # Assigning new variable to copy of original alphabet
    shifted.rotate(shift_variance) # Rotating new shifted alphabet 
    original = ''.join(original) # Re-concatenating split list (alphabet)
    shifted = ''.join(shifted)
    text = text.split() # Convert text to list of words
    scrambled_text =[] # Initializing output variable 
    for index, character in enumerate(text[:][:]):
        lookup_index = original.find(character)
        new_character = shifted[lookup_index]
        scrambled_text.append(new_character)
    text = ''.join(text)
    scrambled_text = ' '.join(scrambled_text)
    print(text)
    print(scrambled_text)    
My expected result is the scrambled ciphered sentence. But my actual output is:

Quote:HelloWorld!Feedthebirdsandreleasethepiggies!
y y y y y y y y y

I’ve failed. I don’t know what to try next. The remainder of this post is kinda like a rant. For the tl:dr and for my final series of questions, see the final paragraph.

What I don’t understand is how to implement a conditional to control for a -1 result at line 19. I understand enough of Python to be able to generally read and explain the logic, syntax and semantics of various examples of loops, functions, classes, methods, conditionals and other operations. But being able to independently take that knowledge and assemble a loop for a Caesar cipher from scratch on my own (like the way @nilamo had to do for me on the previous page of this thread) doesn’t come naturally to me yet. It’s frustrating.

I’ve attempted to learn Python (and other languages) many, many times over the last 12 years. It’s just one false start after another. I have given up way too easily.

I’ve achieved more progress in the past year with learning Python than I have in any year previously. I have indeed made significant progress, thanks to my commitment and persistence that I have invested like never before.

But I am at a point today where I feel helpless as I struggle to rewrite a for loop from scratch. This time I’d like to try using the zip() function in a for loop and use a dictionary. Venturing to use this technique, what I struggle with is how to “iterate over the clear text and use each character to index the dictionary for the cipher character,” also as @stullis suggested earlier.

In summer 2011 I bought a laptop so I could participate in a week-long Python Camp when the instructor toured my home city at the University of Toronto. After the first 6 hours of classes, I was completely confused by functions and “FizzBuzz” for loops. I today am able to model FizzBuzz control flow on my own without help from anyone else on this forum. On GitHub, here is the original Jupyter-Notebook from the Udemy instructor and here is all the work I completed on my own with help only from Google and Stackoverflow, with my FizzBuzz practice task included. I completed it successfully. See? I did it! I’ve worked on many other practice Jupyter Notebooks from my Udemy instructor. I am a capable (beginner) Python programmer. I understand the basics of Python. But writing a Caesar cipher is too much for me right now.

I want to complete this project but writing a for loop on my own using a dictionary as @stullis explained escapes me. There is a part of me which just wants to see the solution performed by someone else on the forum. But then it’s not coming from me. Then I am not learning. So what am I to do? Is there an intermediate leap I can take as a stepping stone between “Point A” (understanding the basics of Python) and “Point C” (writing a Caesar cipher on my own)? What should be my “Point B”? What would you people suggest?

Thanks for your attention.


RE: Caesar cipher - nilamo - Nov-09-2018

I'm going to write this in two parts. The first part, will be about this new issue, and the second will be about the code in general.

(Nov-09-2018, 07:22 PM)Drone4four Wrote:
    for index, character in enumerate(text[:][:]):
        lookup_index = original.find(character)
        new_character = shifted[lookup_index]
        scrambled_text.append(new_character)

The error here is actually the same as the space becoming w before (though this should also have been a "w", maybe you used a different shift_variance this time?). I think the issue boils down to a misunderstanding of what text[:][:] does (which is fine, I didn't catch it myself until I actually tried it). Observe:
>>> spam = "hello there".split()
>>> print(spam)
['hello', 'there']
>>> spam[:]
['hello', 'there']
>>> spam[:][:]
['hello', 'there']
It's clear from your code that your intention is to have a single list, with each element being a different character, such as ['h', 'e', 'l', 'l', 'o', 't', 'h', 'e', 'r', 'e']. But since "hello" obviously isn't in the original un-shifted alphabet, str.find is returning -1, which then means you end up with whatever the last character of the shifted alphabet is, for every word. Your code might work fine if you instead replace text[:][:] with "".join(text).


Now for the second part. Your function worked fine for encrypting words. It didn't handle spaces correctly, but I'm not sure that's actually an issue. encrypt(a_single_word) was beautiful. Maybe it's just personal preference, but I would have stopped there, and handled multiple words outside of the encryption function. Maybe something like:
def encrypt_phrase(text, shift_variance):
    words = text.split()
    encrypted = []
    for word in words:
        encrypted.append(encrypt(word, shift_variance))
    return " ".join(encrypted)
And for the third part of this post (I guess I lied when I said there were two parts)...
I don't think you should give up. A cipher isn't the easiest thing in the world to wrap your mind around, so you shouldn't be discouraged that you didn't get it right the first time you tried. Actually, if you embrace failure, and expect that something bad will happen the first couple times you try, you become really good at navigating the builtin help system, or searching online.

Programming is failure, over and over and over again, until it isn't.


RE: Caesar cipher - Drone4four - Nov-10-2018

(Nov-09-2018, 07:50 PM)nilamo Wrote: The error here is actually the same as the space becoming w before (though this should also have been a "w", maybe you used a different shift_variance this time?). I think the issue boils down to a misunderstanding of what text[:][:] does (which is fine, I didn't catch it myself until I actually tried it). Observe:
>>> spam = "hello there".split()
>>> print(spam)
['hello', 'there']
>>> spam[:]
['hello', 'there']
>>> spam[:][:]
['hello', 'there']

This helps. I am glad this is clarified.

Quote:It's clear from your code that your intention is to have a single list, with each element being a different character, such as ['h', 'e', 'l', 'l', 'o', 't', 'h', 'e', 'r', 'e']. But since "hello" obviously isn't in the original un-shifted alphabet, str.find is returning -1, which then means you end up with whatever the last character of the shifted alphabet is, for every word. Your code might work fine if you instead replace text[:][:] with "".join(text).


Now for the second part. Your function worked fine for encrypting words. It didn't handle spaces correctly, but I'm not sure that's actually an issue. encrypt(a_single_word) was beautiful. Maybe it's just personal preference, but I would have stopped there, and handled multiple words outside of the encryption function. Maybe something like:
def encrypt_phrase(text, shift_variance):
    words = text.split()
    encrypted = []
    for word in words:
        encrypted.append(encrypt(word, shift_variance))
    return " ".join(encrypted)

Thanks for the advice. I played around with this code.

Quote:And for the third part of this post (I guess I lied when I said there were two parts)...
I don't think you should give up. A cipher isn't the easiest thing in the world to wrap your mind around, so you shouldn't be discouraged that you didn't get it right the first time you tried. Actually, if you embrace failure, and expect that something bad will happen the first couple times you try, you become really good at navigating the builtin help system, or searching online.

Programming is failure, over and over and over again, until it isn't.

I agree that this Caesar cipher project is a bit over my head for where I am currently at. I am now going to shift gears and complete all the remaining Jupyter Notebook practice exercises for these two Python Udemy courses I am taking. Then I'll try writing an online form using Django which prompts the user for a fake credit card number and replaces the middle 8 of the 16 digits with xxxx xxxx. At that point I will be ready to return to this Caesar cipher.

If I have any further questions or run into any more issues with my code along the way, I will be sure to return to this sub-forum. You ppl are awesome. Special thanks again goes out to @knackwurstbagel, @nilamo, @stullis, and @DeaD_EyE for all your help and advice so far.


RE: Caesar cipher - nilamo - Nov-11-2018

(Nov-10-2018, 12:20 AM)Drone4four Wrote: If I have any further questions or run into any more issues with my code along the way, I will be sure to return to this sub-forum. You ppl are awesome. Special thanks again goes out to @knackwurstbagel, @nilamo, @stullis, and @DeaD_EyE for all your help and advice so far.
Let us know how it goes, we're here to help :)