Simplifying Vigenere cipher program in Python - python

I have the program below, which is passed on to another function which simply prints out the original and encrypted messages. I want to know how I can simplify this program, specifically the "match = zip" and "change = (reduce(lambda" lines. If possible to do this without using lambda, how can I?
from itertools import cycle
alphabet = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"]
def vigenereencrypt(message,keyword):
output = ""
match = zip(message.lower(),cycle(keyword.lower()))
for i in match:
change = (reduce(lambda x, y: alphabet.index(x) + alphabet.index(y), i)) % 26
output = output + alphabet[change]
return output.lower()

Two things:
You dont need to have a local variable match, just loop zip
Your can split up your two indices x and y in your for loop definition rather than using reduce; reduce is normally used for larger iterables and since you only have 2 items in i, it's adding unnecessary complexity.
ie, you can change your for loop definition to:
for x, y in zip(...):
and your definition of change to:
change = (alphabet.index(x) + alphabet.index(y)) % 26

Starting with what R Nar said:
def vigenereencrypt(message,keyword):
output = ""
for x, y in zip(message.lower(), cycle(keyword.lower())):
change = (alphabet.index(x) + alphabet.index(y)) % 26
output = output + alphabet[change]
return output.lower()
We can be more efficient by using a list and then joining it, instead of adding to a string, and also by noticing that the output is already lowercase:
def vigenereencrypt(message,keyword):
output = []
for x, y in zip(message.lower(), cycle(keyword.lower())):
change = (alphabet.index(x) + alphabet.index(y)) % 26
output.append(alphabet[change])
return "".join(output)
Then we can reduce the body of the loop to one line..
def vigenereencrypt(message,keyword):
output = []
for x, y in zip(message.lower(), cycle(keyword.lower())):
output.append(alphabet[(alphabet.index(x) + alphabet.index(y)) % 26])
return "".join(output)
... so we can turn it into a list comprehension:
def vigenereencrypt(message,keyword):
output = (
alphabet[(alphabet.index(x) + alphabet.index(y)) % 26]
for x, y in zip(message.lower(), cycle(keyword.lower()))
)
return "".join(output)
I feel like there's something we could do with map(alphabet.index, ...) but I can't think of a way that's any better than the list comprehension.

you could do it with a bunch of indexing instead of zip...
alphabet = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"]
alphaSort = {k:n for n,k in enumerate(alphabet)}
alphaDex = {n:k for n,k in enumerate(alphabet)}
def vigenereencrypt(message,keyword):
output = ""
#match = zip(message.lower(),cycle(keyword.lower())) # zip(a,cycle(b)) Creates [(a[n],b[n%len(b)]) for k in range(len(a)) ]
op = "" # So lets start with for k in range(len(a))
for k in range(len(message)):
op += alphaDex[(alphaSort[message.lower()[k]]+alphaSort[keyword.lower()[k%len(keyword)]])%len(alphabet)]
return(op)

Related

Python difference between string formatting

long story short. Can someone explain it to me how is it that be, simple line of code can do the same as all of the for loop written by me?
def z1(code1, code2):
parse = lambda x: int(x.replace('-', ''))
code1, code2 = parse(code1), parse(code2)
print(code1, code2)
return ["%02d-%03d" % divmod(x, 1000) for x in range(code1, code2+1)] <---
here's my solution:
def z2(code1, code2):
codes = []
parse = lambda x: int(x.replace('-', ''))
code1, code2= parse(code1), parse(code2)
for x in range(code1, code2+1):
x = str(x)
a = x[0:2]
b = x[2:6]
c = a+"-"+b
codes.append(f"{c}")
return codes
["%02d-%03d" % divmod(x, 1000) for x in range(code1, code2+1)]
This is called list comprehension, allow writing more concise code than creating empty list and then append-ing in loop. Consider following example
squares = []
for i in range(5):
squares.append(i**2)
is equivalent to
squares = [i**2 for i in range(5)]

Format a large integer with commas without using .format()

I'm trying to format any number by inserting ',' every 3 numbers from the end by not using format()
123456789 becomes 123,456,789
1000000 becomes 1,000,000
What I have so far only seems to go from the start, I've tried different ideas to get it to reverse but they seem to not work as I hoped.
def format_number(number):
s = [x for x in str(number)]
for a in s[::3]:
if s.index(a) is not 0:
s.insert(s.index(a), ',')
return ''.join(s)
print(format_number(1123456789))
>> 112,345,678,9
But obviously what I want is 1,123,456,789
I tried reversing the range [:-1:3] but I get 112,345,6789
Clarification: I don't want to use format to structure the number, I'd prefer to understand how to do it myself just for self-study's sake.
Here is a solution for you, without using built-in functions:
def format_number(number):
s = list(str(number))[::-1]
o = ''
for a in range(len(s)):
if a and a % 3 == 0:
o += ','
o += s[a]
return o[::-1]
print(format_number(1123456789))
And here is the same solution using built-in functions:
def format_number(number):
return '{:,}'.format(number)
print(format_number(1123456789))
I hope this helps. :D
One way to do it without built-in functions at all...
def format_number(number):
i = 0
r = ""
while True:
r = "0123456789"[number % 10] + r
number //= 10
if number == 0:
return r
i += 1
if i % 3 == 0:
r = "," + r
Here's a version that's almost free of built-in functions or methods (it does still have to use str)
def format_number(number):
i = 0
r = ""
for character in str(number)[::-1]:
if i > 0 and i % 3 == 0:
r = "," + r
r = character + r
i += 1
return r
Another way to do it without format but with other built-ins is to reverse the number, split it into chunks of 3, join them with a comma, and reverse it again.
def format_number(number):
backward = str(number)[::-1]
r = ",".join(backward[i:i+3] for i in range(0, len(backward), 3))
return r[::-1]
Your current approach has following drawbacks
checking for equality/inequality in most cases (especially for int) should be made using ==/!= operators, not is/is not ones,
using list.index returns first occurence from the left end (so s.index('1') will be always 0 in your example), we can iterate over range if indices instead (using range built-in).
we can have something like
def format_number(number):
s = [x for x in str(number)]
for index in range(len(s) - 3, 0, -3):
s.insert(index, ',')
return ''.join(s)
Test
>>> format_number(1123456789)
'1,123,456,789'
>>> format_number(6789)
'6,789'
>>> format_number(135)
'135'
If range, list.insert and str.join are not allowed
We can replace
range with while loop,
list.insert using slicing and concatenation,
str.join with concatenation,
like
def format_number(number):
s = [x for x in str(number)]
index = len(s) - 3
while index > 0:
s = s[:index] + [','] + s[index:]
index -= 3
result = ''
for character in s:
result += character
return result
Using str.format
Finally, following docs
The ',' option signals the use of a comma for a thousands separator. For a locale aware separator, use the 'n' integer presentation type instead.
your function can be simplified to
def format_number(number):
return '{:,}'.format(number)
and it will even work for floats.

Repeating characters results in wrong repetition counts

My function looks like this:
def accum(s):
a = []
for i in s:
b = s.index(i)
a.append(i * (b+1))
x = "-".join(a)
return x.title()
with the expected input of:
'abcd'
the output should be and is:
'A-Bb-Ccc-Dddd'
but if the input has a recurring character:
'abccba'
it returns:
'A-Bb-Ccc-Ccc-Bb-A'
instead of:
'A-Bb-Ccc-Cccc-Bbbbb-Aaaaaa'
how can I fix this?
Don't use str.index(), it'll return the first match. Since c and b and a appear early in the string you get 2, 1 and 0 back regardless of the position of the current letter.
Use the enumerate() function to give you position counter instead:
for i, letter in enumerate(s, 1):
a.append(i * letter)
The second argument is the starting value; setting this to 1 means you can avoid having to + 1 later on. See What does enumerate mean? if you need more details on what enumerate() does.
You can use a list comprehension here rather than use list.append() calls:
def accum(s):
a = [i * letter for i, letter in enumerate(s, 1)]
x = "-".join(a)
return x.title()
which could, at a pinch, be turned into a one-liner:
def accum(s):
a = '-'.join([i * c for i, c in enumerate(s, 1)]).title()
This is because s.index(a) returns the first index of the character. You can use enumerate to pair elements to their indices:
Here is a Pythonic solution:
def accum(s):
return "-".join(c*(i+1) for i, c in enumerate(s)).title()
simple:
def accum(s):
a = []
for i in range(len(s)):
a.append(s[i]*(i+1))
x = "-".join(a)
return x.title()

How do I append a list of multiple objects to the end of a file path?

I have created a for loop to create a list containing the string 'size_i' so I have size_01, size_02, size_03 etc all the way to size_84. Like:
def size():
x = list()
for i in range(0,84):
x = x.append('size' + str(i))
return x
This works fine. Now I want to save each 'size' in that list to the end of a filepath:
d_path = /home/Jake/Documents/sizes/size_01
= /home/Jake/Documents/sizes/size_02
etc...
But for some reason I can't get it to work and I have no idea where I am going wrong! Here is my full attempt:
import os
def size():
d_path = '/home/Jake/Documents/sizes'
x = list()
for i in range(0,84):
x = x.append('size_' + str(i))
p = os.path.join(d_path,x)
return p
From what I understand of the question, you are writing your function wrong. x.append('size_' + str(i)) returns None, so basically you were replacing x with None in x = x.append('size_' + str(i)) which was giving an error when you were trying to join.
Your current function is returning the last name that you are trying to create ie '/home/Jake/Documents/sizes/size_83', i believe you want to return a list containing all the names.
This will work.
import os
def size():
d_path = '/home/Jake/Documents/sizes'
x = list()
for i in range(0,84):
tmp = 'size_' + str(i)
p = os.path.join(d_path,tmp)
x.append(p)
return x
print size()
Does this work for you?
import os
def size():
d_path = '/home/Jake/Documents/sizes'
x = []
p = []
for i in range(0,84):
z = 'size_' + str(i)
x.append(z)
p.append(d_path+z)
return p
Try this one
import os
def size():
d_path = '/home/Jake/Documents/sizes'
x = list()
for i in range(0,84):
x.append(os.path.join(d_path,'size_' + str(i)))
return x
The function append in class list doesn't return a
significant value.
Try this:
import os
def size():
d_path = '/home/Jake/Documents/sizes'
x = []
p=[]
for i in range(0,84):
x.append('size_{:02}'.format(i))
p.append(os.path.join(d_path,x[i]))
return p
print size()
Some of these answers are unnecessarily long and complex, but may more be insightful about what's going on than the following snippets:
Clear Approach
path_root = "/home/Jake/Documents/sizes"
sizes = ["size_%d" % i for i in range(84)]
paths = [os.path.join(path_root, s) for s in sizes]
print(paths)
Alternative Approach
path_root = "/home/Jake/Documents/sizes"
paths = [os.path.join(path_root, "size_%d" % i) for i in range(84)]
print(paths)
Some notes:
Both approaches use list comprehensions
When the lower bound of range is zero, it can be omitted
Both approaches use the % sign printf-style string formatting operator
If you want leading zeros for single digit numbers like your comments (but not your code) suggest, you can replace "size_%d" with "size_%02d"

Parsing string with Python correct way

I have some problems with parsing the correct way. I want to split the complete string in two seperate strings. And then remove the "="-signs frome the first string and the ","-sign from the 2nd string. From my output I can conclude that I did something wrong, but I do not seem to get where the problem lies. I want the first part to convert to integers, and I've already tried it with map(int, split()).
If anyone has a tip, I would appreciate that.
This is my output:
('5=20=22=10=2=0=0=1=0=1', 'Vincent Appel,Johannes Mondriaan')
This is my program:
mystring = "5=20=22=10=2=0=0=1=0=1;Vincent Appel,Johannes Mondriaan"
def split_string(mystring):
strings = mystring.split(";")
x = strings[0]
y = strings[-1]
print(x,y)
def split_scores(x):
scores = x.split("=")
score = scores[0]
names = scores[-1]
stnames(names)
print score
def stnames(y):
studentname = y.split(",")
name = studentname[1]
print name
split_string(mystring)
split_string(mystring) runs the 1st function, producing the tuple with 2 strings. But nothing runs the other functions which are intended to perform further splitting.
try:
x, y = split_string(mystring)
x1 = split_scores(x)
y1 = stnames(y)
(x1, y1)
oops, your functions print the results, don't return them. So you also need:
def split_string(mystring):
# split mystring on ";"
strings = mystring.split(";")
return strings[0],strings[1]
def split_string(mystring):
# this version raises an error if mystring does not have 2 parts
x, y = mystring.split(";")
return x,y
def split_scores(x):
# return a list with all the scores
return x.split("=")
def stnames(y):
# return a list with all names
return y.split(",")
def lastname(y):
# return the last name (, delimited string)
return y.split(",")[-1]
If you are going to split the task among functions, it is better to have them return the results rather than print them. That way they can be used in various combinations. print within a function only for debugging purposes.
Or a compact, script version:
x, y = mystring.split(';')
x = x.split('=')
y = y.split(',')[-1]
print y, x
If you want the scores as numbers, add:
x = [int(x) for x in x]
to the processing.
Try this:
def split_string(mystring):
strings = mystring.split(";")
x = int(strings[0].replace("=",""))
y = strings[-1].replace(","," ")
print x,y
My two cents.
If I understood what you want to achieve, this code could help:
mystring = "5=20=22=10=2=0=0=1=0=1;Vincent Appel,Johannes Mondriaan"
def assignGradesToStudents(grades_and_indexes, students):
list_length = len(grades_and_indexes)
if list_length%2 == 0:
grades = grades_and_indexes[:list_length/2]
indexes = grades_and_indexes[list_length/2:]
return zip([students[int(x)] for x in indexes], grades)
grades_and_indexes, students = mystring.split(';')
students = students.split(',')
grades_and_indexes = grades_and_indexes.split('=')
results = assignGradesToStudents(grades_and_indexes, students)
for result in results:
print "[*] {} got a {}".format(result[0], result[1])
Output:
[*] Vincent Appel got a 5
[*] Vincent Appel got a 20
[*] Johannes Mondriaan got a 22
[*] Vincent Appel got a 10
[*] Johannes Mondriaan got a 2

Categories

Resources