Python - Neaten up Code - Loops and Recursive features - python

I have the below code that works fine. However Whilst i'm learning im trying to find ways to continually improve my code.
Would it be possible to make the below code neater in any way?
def isValidWord(word, hand, wordList):
"""
Returns True if word is in the wordList and is entirely
composed of letters in the hand. Otherwise, returns False.
Does not mutate hand or wordList.
word: string
hand: dictionary (string -> int)
wordList: list of lowercase strings
"""
x=''
if word in wordList:
x=True
else:
x=False
#dont mutate the hand so copy
hand2=hand.copy()
for letter in word:
if letter in hand2.keys():
hand2[letter]-=1
if hand2[letter]<0:
x=False
break
else:
x = False
return x
To run the code:
hand = {'r': 1, 'a': 3, 'p': 2, 'e': 1, 't': 1, 'u':1}
word = "rapture"
wordList = ['rapture','alex']
isValidWord(word, hand, wordList)

With all and a Counter:
from collections import Counter
def isValidWord(word, hand, wordList):
wc = Counter(word)
return word in wordList and all(wc[c] <= hand.get(c, -1) for c in wc)
If you have very large lists of words, consider using a set instead for the O(1) lookup time.

To handle counts properly:
from collections import Counter
def isValidWord(word, hand, wordList):
# Check for membership first and early out if fails
# Otherwise, see if the count of any letter in word exceeds that of the hand
return word in wordList and all(hand.get(let, -1) >= cnt for let, cnt in Counter(word).items())

Related

Duplicate characters in a string

Hello I a beginner in python. I am building a small program that can find any duplicate characters in a string. However there's something i don't understand.
Code:
def is_isogram(string):
dict = {}
for letter in string:
dict[letter] = 1
if letter in dict:
dict[letter] += 1
return dict
print(is_isogram("Dermatoglyphics"))
OUTPUT
{'D': 1, 'e': 1, 'r': 1, 'm': 1, 'a': 1, 't': 1, 'o': 1, 'g': 1, 'l': 1, 'y': 1, 'p': 1, 'h': 1, 'i': 1, 'c': 1, 's': 2}
I set an empty dictionary.
I then used a for loop to iterate over the string, and then in each iteration it should assign 1 to a dictionary key, "letter"
Then used "if...in" to check if letter has already appeared, and if it has then the the "letter" key should be incremented by 1.
I tried it on a word, Dermatoglyphics, but each time the last key value pair is always 2, even though this word only contains 1 of each letter. Does anyone know why?
if statement applies after finishing for loop, so that it adds 1 only in last character. Its a problem of indentation. Even if you write if condition inside loop, it won't be right because of your logic. You assign dict[letter] = 1 for every letter. Then check if letter in dict, so that it will add 1 two times. Use else condition instead.
def is_isogram(string):
dict = {}
for letter in string:
if letter in dict:
dict[letter] += 1
else:
dict[letter] = 1
return dict
print(is_isogram("Dermatoglyphics"))
Or you can use count function like this
def is_isogram(string):
dict = {}
for letter in string:
dict[letter] = string.count(letter)
return dict
print(is_isogram("Dermatoglyphics"))
You are setting 1 for each, and then you increment the last letter. I think you meant to put if inside for block.
Here is a working version:
def is_isogram(string):
dct = {}
for letter in string:
if letter in dct:
dct[letter] += 1
else:
dct[letter] = 1
return dct
print(is_isogram("Dermatoglyphics"))
The logic behind: If the letter already exists, increment counter. Otherwise initialize it with counter=1.
Edit: Changed dict to dct as dict is a python built-in name as #Michael suggested.
As your function is named is_isogram() it should return a boolean.
Either the string is an isogram either it isn't.
A big benefit is that you stop iterating as soon as you find a duplicate.
You don't need to use a dict.
It's not a bad idea but to detect an isogram you don't have the need to count occurrences of each letter.
You just have to test the membership.
A set is better suited. Like a dict but without the values.
def is_isogram(word: str) -> bool:
used_letters = set()
for letter in word:
if letter in used_letters:
return False
else:
used_letters.add(letter)
return True
is_isogram("Dermatoglyphics") # True
is_isogram("DDermatoglyphics") # False
Your code works exactly as it is expected it assigns 1 to every letter then since your if condition is out of the loop, it increments the last character (letter) by one.
I made some changes to your code.
def is_isogram(string):
dict = {}
for letter in string:
dict[letter] = 0
for letter in string:
dict[letter] += 1
return dict
print(is_isogram("telegram"))
What I have done is that first it adds all the letters to the dictionary then uses another scan to count each letter.
This function has a complexity of O(n) which is faster than other answers I think
Here is a timed execution of both
This answer: https://onlinegdb.com/lMC-Qn76D
Other answers: https://onlinegdb.com/eeV0IFN5J
Please correct me if I am wrong

Counting number of occurrences in a string

I need to return a dictionary that counts the number of times each letter in a predetermined list occurs. The problem is that I need to count both Upper and Lower case letters as the same, so I can't use .lower or .upper.
So, for example, "This is a Python string" should return {'t':3} if "t" is the letter being searched for.
Here is what I have so far...
def countLetters(fullText, letters):
countDict = {i:0 for i in letters}
lowerString = fullText.lower()
for i in lowerString:
if i in letters:
countDict[i] += 1
return countDict
Where 'letters' is the condition and fullText is the string I am searching.
The obvious issue here is that if the test is "T" rather than "t", my code won't return anything Sorry for any errors in my terminology, I am pretty new to this. Any help would be much appreciated!
To ignore capitalization, you need to input =
input = input.lower ()
.Lists all characters of the input text using list operations.
It can also be used as a word counter if you scan the space character.
input = "Batuq batuq BatuQ" # Reads all inputs up to the EOF character
input = input.replace('-',' ')#Replace (-, + .etc) expressions with a space character.
input = input.replace('.','')
input = input.replace(',','')
input = input.replace("`",'')
input = input.replace("'",'')
#input= input.split(' ') #if you use it, it will sort by the most repetitive words
dictionary = dict()
count = 0
for word in input:
dictionary[word] = input.count(word)
print(dictionary)
#Writes the 5 most repetitive characters
for k in sorted(dictionary,key=dictionary.get,reverse=True)[:5]:
print(k,dictionary[k])
Would something like this work that handles both case sensitive letter counts and non case sensitive counts?
from typing import List
def count_letters(
input_str: str,
letters: List[str],
count_case_sensitive: bool=True
):
"""
count_letters consumes a list of letters and an input string
and returns a dictionary of counts by letter.
"""
if count_case_sensitive is False:
input_str = input_str.lower()
letters = list(set(map(lambda x: x.lower(), letters)))
# dict comprehension - build your dict in one line
# Tutorial on dict comprehensions: https://www.datacamp.com/community/tutorials/python-dictionary-comprehension
counts = {letter: input_str.count(letter) for letter in letters}
return counts
# define function inputs
letters = ['t', 'a', 'z', 'T']
string = 'this is an example with sTrings and zebras and Zoos'
# case sensitive
count_letters(
string,
letters,
count_case_sensitive=True
)
# {'t': 2, 'a': 5, 'z': 1, 'T': 1}
# not case sensitive
count_letters(
string,
letters,
count_case_sensitive=False
)
# {'a': 5, 'z': 2, 't': 3} # notice input T is now just t in dictionary of counts
Try it - like this:
def count_letters(fullText, letters):
countDict = {i: 0 for i in letters}
lowerString = fullText.lower()
for i in lowerString:
if i in letters:
countDict[i] += 1
return countDict
test = "This is a Python string."
print(count_letters(test, 't')) #Output: 3
You're looping over the wrong string. You need to loop over lowerString, not fullString, so you ignore the case when counting.
It's also more efficient to do if i in countDict than if i in letter.
def countLetters(fullText, letters):
countDict = {i.lower():0 for i in letters}
lowerString = fullText.lower()
for i in lowerString:
if i in countDict:
countDict[i] += 1
return countDict
What you can do is simply duplicate the dict with both upper and lowercase like so:
def countLetters(fullText, letters):
countDict = {}
for i in letters:
countDict[i.upper()]=0
countDict[i.lower()]=0
lowerString = fullText.lower()
letters = letters.lower()
for i in lowerString:
if i in letters:
countDict[i] += 1
if (i!=i.upper()):
countDict[i.upper()] +=1
return countDict
print(countLetters("This is a Python string", "TxZY"))
Now some things you can also do is loop over the original string and change countDict[i] += 1 to countDict[i.lower()] +=1
Use the Counter from the collections module
from collections import Counter
input = "Batuq batuq BatuQ"
bow=input.split(' ')
results=Counter(bow)
print(results)
output:
Counter({'Batuq': 1, 'batuq': 1, 'BatuQ': 1})

Code not running properly

The code I have below is supposed to run the number of words that start with a certain letter, but when I run it, the counts are all 0, instead of what they should be: {'I': 2, 'b': 2, 't': 3, 'f': 1}. I appreciate any help. Thanks!
def initialLets(keyStr):
'''Return a dictionary in which each key is the initial letter of a word in t and the value is the number of words that begin with that letter. Upper
and lower case letters should be considered different letters.'''
inLets = {}
strList = keyStr.split()
firstLets = []
for words in strList:
if words[0] not in firstLets:
firstLets.append(words[0])
for lets in firstLets:
inLets[lets] = strList.count(lets)
return inLets
text = "I'm born to trouble I'm born to fate"
print(initialLets(text))
You can try this:
text = "I'm born to trouble I'm born to fate"
new_text = text.split()
final_counts = {i[0]:sum(b.startswith(i[0]) for b in new_text) for i in new_text}
Output:
{'I': 2, 'b': 2, 't': 3, 'f': 1}
You don't have a counter as you append the letter but not its number of occurrences.
So to simplify:
def initialLets(keyStr):
'''Return a dictionary in which each key is the initial letter of a word in t and the value is the number of words that begin with that letter. Upper
and lower case letters should be considered different letters.'''
strList = keyStr.split()
# We initiate the variable that gonna take our results
result = {}
for words in strList:
if words[0] not in result:
# if first letter not in result then we add it to result with counter = 1
result[words[0]] = 1
else:
# We increase the number of occurence
result[words[0]] += 1
return result
text = "I'm born to trouble I'm born to fate"
print(initialLets(text))
Firstly, you are checking if the first letter of the word is in the list before putting it in. That would just make the list comprise of only 1 of each letter. Secondly, your strList is a list of each word, instead of inLets[lets] = strList.count(lets), it should be inLets[lets] = firstLets.count(lets)... While your current code isn't the cleanest way to do it, this minor modification would have worked.
def initialLets(keyStr):
'''Return a dictionary in which each key is the initial letter of a word in t and the value is the number of words that begin with that letter. Upper
and lower case letters should be considered different letters.'''
inLets = {}
strList = keyStr.split()
firstLets = []
for words in strList:
firstLets.append(words[0])
for lets in firstLets:
inLets[lets] = firstLets.count(lets)
return inLets
text = "I'm born to trouble I'm born to fate"
print(initialLets(text))

How can I tell what value my function is returning in Python?

I'm trying to debug this program I wrote. How can I tell if, for a given word, hand, and word_list, it returns True or False? I tried initializing a variable failure and then modifying it and printing it's value. It isn't printing, so I don't know if it is behaving like it's supposed to. Any help is appreciated.
I have a function load_words() that returns a list of words. I know word is in word_list (I checked), so just trying to see if word is composed entirely of letters from the keys in the dictionary hand, which in this case it isn't, so it should return False.
Also, what is the difference between .keys() and .iterrkeys(), and is there a better way of looping through hand, perhaps with letter, value in hand.iteritems()?
word = 'axel'
hand2 = {'b':1, 'x':2, 'l':3, 'e':1}
def is_valid_word(word, hand, word_list):
"""
Returns True if word is in the word_list and is entirely
composed of letters in the hand. Otherwise, returns False.
Does not mutate hand or word_list.
word: string
hand: dictionary (string -> int)
word_list: list of lowercase strings
"""
failure = False
if word in word_list:
print hand
print [list(i) for i in word.split('\n')][0]
for letter in [list(i) for i in word.split('\n')][0]:
print letter
if letter in hand.keys():
print letter
return True
failure = True
print failure
else:
return False
failure = False
print failure
else:
return False
failure = False
print failure
is_valid_word(word,hand2,load_words())
UPDATE I wish to use this function in my function, but it gives a key error, even though it works fine on its own.
def update_hand(hand, word):
"""
Assumes that 'hand' has all the letters in word.
In other words, this assumes that however many times
a letter appears in 'word', 'hand' has at least as
many of that letter in it.
Updates the hand: uses up the letters in the given word
and returns the new hand, without those letters in it.
Has no side effects: does not modify hand.
word: string
hand: dictionary (string -> int)
returns: dictionary (string -> int)
"""
for letter in [list(i) for i in word.split('\n')][0]:
if letter in hand.keys():
hand[letter] = hand[letter]-1
if hand[letter] <= 0:
del hand[letter]
display_hand(hand)
return hand
The reason why it is not printing out is because you are returning the function before it prints. This means that the program stops before it reaches the print statement. For example:
def foo(x):
return x
print x
foo("asdf")
Will return nothing while:
def foo(x):
print x
return x
foo("asdf")
Will print:
asdf
So, all your statements before return. If not, it will not execute.
For your second clarification, this post already has your answer https://stackoverflow.com/a/3617008:
In Python 2, iter(d.keys()) and d.iterkeys() are not quite equivalent, although they will behave the same. In the first, keys() will return a copy of the dictionary's list of keys and iter will then return an iterator object over this list, with the second a copy of the full list of keys is never built.
Note that Python 3 does not have .iterkeys() too. Python 3 uses the previous .iterkeys() as the new .keys().
Lastly, I will review what is generally wrong with your code and what you want to achieve in descending order of severity.
Your code only checks one letter
[list(i) for i in word.split('\n')][0] is not how you get all the letters from a word.
You should make short code return first so that you would not have big indent blocks.
Your code only checks one letter
In your for loop, you return True immediately after the first word is checked. You should return True after the loop is completed instead.
for letter in word:
if letter not in hand.keys():
return False
return True
List comprehension
Your list comprehension is not needed (I'll tell you why later) and need not be so complex just to get the letters from a word. E.g.
[list(i) for i in word.split('\n')][0]
Actually only does this:
list(word)
In fact, you should just iterate through the word directly (as I did above), it will return the letters one by one:
for letter in word:
# code...
Make short code return first
Usually I dislike big chunks of highly indented code. What you can do is make the short code return first. For example:
if word in word_list:
for letter in word:
if letter in hand.keys():
return True
else:
return False
else:
return False
Can be simply be written as:
if word not in word_list:
return False
for letter in word:
if letter in hand.keys():
return True
else:
return False
However, this is just my opinion. Some others may prefer the else statement so that they know when the code is executed.
Your final code would look like:
def is_valid_word(word, hand, word_list):
if word not in word_list:
return False
for letter in word:
if letter not in hand.keys():
return False
return True
Clean right? However, I assume that you are making something like a scrabble game, so you would count if the words in your hand can for the word you chose. What you can add is something to count if the number of letters in the word is less than or equal to the number of letters in your hand:
def is_valid_word(word, hand, word_list):
if word not in word_list:
return False
# This makes the word into a "unique list"
letters = set(word)
for letter in letters:
if hand[letter] < word.count(letter):
return False
return True
EDIT
There was a problem with the code. It does not check if the letter is in hand in the if statement: if hand[letter] < word.count(letter):.
def is_valid_word(word, hand, word_list):
if word not in word_list and word not in hand.keys():
return False
letters = set(word)
for letter in letters:
# Add this extra clause
if letter in hand.keys() or hand[letter] < word.count(letter):
return False
return True
You can print the result directly print is_valid_word(word,hand2,load_words())
You have some indentation issues, and doing something after a return statement is futile.
You don't need to use keys or iterkeys the in operator will check for you, and will work with lists, set, dicts (keys), tuples, strings, ...
The in operator invokes __contains__ which is supported by most python collections.
Also have look at https://docs.python.org/2/reference/expressions.html#membership-test-details.
He is a minimized example of what you want to do with 3 tests.
def is_valid_word(word, hand, word_list):
"""
Returns True if word is in the word_list and is entirely composed
of letters in the hand. Otherwise, returns False. Does not mutate
hand or word_list.
word: string
hand: dictionary (string -> int)
word_list: list of lowercase strings
"""
if word not in word_list:
return False
for letter in word:
if letter not in hand:
return False
return True
print(is_valid_word('bxel',
{'b': 1, 'x': 2, 'l': 3, 'e': 1},
['foo', 'bar', 'bxel']))
print(is_valid_word('axel',
{'b': 1, 'x': 2, 'l': 3, 'e': 1},
['foo', 'bar', 'axel']))
print(is_valid_word('axel',
{'a': 1, 'x': 2, 'l': 3, 'e': 1},
['foo', 'bar', 'axel']))

Function variables: Whats the range of scope of a function

Recently I have been working on a word game in python, nonetheless I am having problems with a little detail.The idea of the game is to create words with letters in a hand given randomly. The function that coordinates the game asks the user to input a letter and according to it, lets him/her to play a new hand, play the same hand again or exit the game. The problem is that I cannot make the game to return to the previous hand and let the user play it again. Instead the code returns a modified version of the previous hand, so that if you inputted a word correctly the letters in that word won't be in it. I thought this could not be possible since the function that "updates the hand" lies on a different scope. I am working in python 3.2.1
I posted the entire code so you could test it and see what it does. The problem, however, lies in the functions: play_game, play_hand and update_hand.
import random
import string
VOWELS = 'aeiou'
CONSONANTS = 'bcdfghjklmnpqrstvwxyz'
HAND_SIZE = 7
SCRABBLE_LETTER_VALUES = {
'a': 1, 'b': 3, 'c': 3, 'd': 2, 'e': 1, 'f': 4, 'g': 2, 'h': 4, 'i': 1, 'j': 8, 'k': 5, 'l': 1, 'm': 3, 'n': 1, 'o': 1, 'p': 3, 'q': 10, 'r': 1, 's': 1, 't': 1, 'u': 1, 'v': 4, 'w': 4, 'x': 8, 'y': 4, 'z': 10
}
WORDLIST_FILENAME = "words.txt"
def load_words():
"""
Returns a list of valid words. Words are strings of lowercase letters.
Depending on the size of the word list, this function may
take a while to finish.
"""
print ("Loading word list from file...")
# inFile: file
inFile = open(WORDLIST_FILENAME, 'r')
# wordlist: list of strings
wordlist = []
for line in inFile:
wordlist.append(line.strip().lower())
print (" ", len(wordlist), "words loaded.")
return wordlist
def get_frequency_dict(sequence):
"""
Returns a dictionary where the keys are elements of the sequence
and the values are integer counts, for the number of times that
an element is repeated in the sequence.
sequence: string or list
return: dictionary
"""
# freqs: dictionary (element_type -> int)
freq = {}
for x in sequence:
freq[x] = freq.get(x,0) + 1
return freq
def get_word_score(word, n):
"""
Returns the score for a word. Assumes the word is a
valid word.
The score for a word is the sum of the points for letters
in the word multiplied by the length of the word, plus 50
points if all n letters are used on the first go.
Letters are scored as in Scrabble; A is worth 1, B is
worth 3, C is worth 3, D is worth 2, E is worth 1, and so on.
word: string (lowercase letters)
returns: int >= 0
"""
score = 0
for character in word:
score = score + (SCRABBLE_LETTER_VALUES[character])
score = score * len(word)
if len(word) == 7:
score = score + 50
return score
def display_hand(hand):
"""
Displays the letters currently in the hand.
For example:
display_hand({'a':1, 'x':2, 'l':3, 'e':1})
Should print out something like:
a x x l l l e
The order of the letters is unimportant.
hand: dictionary (string -> int)
"""
for letter in hand.keys():
for j in range(hand[letter]):
print (letter,)
def deal_hand(n):
"""
Returns a random hand containing n lowercase letters.
At least n/3 the letters in the hand should be VOWELS.
Hands are represented as dictionaries. The keys are
letters and the values are the number of times the
particular letter is repeated in that hand.
n: int >= 0
returns: dictionary (string -> int)
"""
hand={}
num_vowels = int(n / 3)
for i in range(num_vowels):
x = VOWELS[random.randrange(0,len(VOWELS))]
hand[x] = hand.get(x, 0) + 1
for i in range(num_vowels, n):
x = CONSONANTS[random.randrange(0,len(CONSONANTS))]
hand[x] = hand.get(x, 0) + 1
return hand
def update_hand(hand, word):
"""
Assumes that 'hand' has all the letters in word.
In other words, this assumes that however many times
a letter appears in 'word', 'hand' has at least as
many of that letter in it.
Updates the hand: uses up the letters in the given word
and returns the new hand, without those letters in it.
Has no side effects: does not modify hand.
word: string
hand: dictionary (string -> int)
returns: dictionary (string -> int)
"""
for i in word:
new_hand = hand
new_VOWELS = VOWELS
new_CONSONANTS = CONSONANTS
if i in VOWELS:
new_VOWELS = new_VOWELS.replace("i", "")
new_hand[i] = new_hand.get(i, 0) - 1
if new_hand[i] <= 0:
new_hand.pop(i, None)
else:
new_CONSONANTS = new_CONSONANTS.replace("i", "")
new_hand[i] = new_hand.get(i, 0) - 1
if new_hand[i] <= 0:
new_hand.pop(i, None)
return (new_hand)
def is_valid_word(word, hand, word_list):
"""
Returns True if word is in the word_list and is entirely
composed of letters in the hand. Otherwise, returns False.
Does not mutate hand or word_list.
word: string
hand: dictionary (string -> int)
word_list: list of lowercase strings
"""
for character in word:
x = 0
if character in hand:
x = x + 1
if (x==(len(word)-1)) and (word in word_list):
return (True)
else:
return (False)
def calculate_handlen(hand):
handlen = 0
for v in hand.values():
handlen += v
return handlen
def play_hand(hand, word_list):
"""
Allows the user to play the given hand, as follows:
* The hand is displayed.
* The user may input a word.
* An invalid word is rejected, and a message is displayed asking
the user to choose another word.
* When a valid word is entered, it uses up letters from the hand.
* After every valid word: the score for that word is displayed,
the remaining letters in the hand are displayed, and the user
is asked to input another word.
* The sum of the word scores is displayed when the hand finishes.
* The hand finishes when there are no more unused letters.
The user can also finish playing the hand by inputing a single
period (the string '.') instead of a word.
hand: dictionary (string -> int)
word_list: list of lowercase strings
"""
score = 0
han = hand
while True:
print ("Score= ", score)
word = str(input("Enter your word: "))
value = is_valid_word(word, han, word_list)
if value == True:
print ("Congratulations, that word is worth", get_word_score(word, 7), "points")
print ("Please input another word")
han = update_hand(han, word)
print ("Current hand=")
display_hand(han)
score = score + get_word_score(word, 7)
elif word == "." or len(hand)==0:
break
else:
print ("Current hand=")
display_hand(han)
print ("Sorry, that word is not valid")
print ("Please choose another word")
def play_game(word_list):
"""
Allow the user to play an arbitrary number of hands.
* Asks the user to input 'n' or 'r' or 'e'.
* If the user inputs 'n', let the user play a new (random) hand.
When done playing the hand, ask the 'n' or 'e' question again.
* If the user inputs 'r', let the user play the last hand again.
* If the user inputs 'e', exit the game.
* If the user inputs anything else, ask them again.
"""
global handy
handy = deal_hand(7)
while True:
print ("Use 'n' to play a new hand, 'r' to play the last hand again or 'e' to exit game")
inp = str(input("Enter a letter:'n' or 'r' or 'e': ="))
if inp == 'n':
hand = deal_hand(7)
handy = hand
print("Current hand =")
display_hand(hand)
play_hand(hand, word_list)
elif inp == 'r':
print("Current hand =")
display_hand(handy)
play_hand(handy, word_list)
elif inp == 'e':
exit(play_game)
else:
print ("Please input a valid letter")
if __name__ == '__main__':
word_list = load_words()
play_game(word_list)
Writing y = x makes y refer to the same thing as x. It does NOT copy x. With "immutable" objects, this looks like a copy because you have to make a new object in order for any "change" to be evident. With "mutable" objects like lists, because x and y both refer to the same object, changes to one name appear in the other.
You probably want to copy the list:
new_hand = hand[:] # this funny slicing notation produces a shallow copy of the original list

Categories