4
\$\begingroup\$

Inspired by a recent question that caught my interest (now deleted), I wrote a function in Python 3 to rearrange the letters of a given string to create a (any!) palindrome:

  1. Count the occurrences of each letter in the input
  2. Iterate over the resulting (letter, occurrences) tuples only once:
    • If the occurences are even, we remember them to add them around the center later
    • A valid palindrome can contain only 0 or 1 letter(s) with an odd number of occurrences, so if we've already found the center, we raise an exception. Otherwise, we save the new-found center for later
  3. Finally, we add the sides around the center and join it to create the resulting palindrome string.

I'm looking for feedback on every aspect you can think of, including

  • readability (including docstrings),
  • how appropriate the data structures are,
  • if the algorithm can be expressed in simpler terms (or replaced altogether) and
  • the quality of my tests.

Implementation: palindromes.py

from collections import deque, Counter
def palindrome_from(letters):
 """
 Forms a palindrome by rearranging :letters: if possible,
 throwing a :ValueError: otherwise.
 :param letters: a suitable iterable, usually a string
 :return: a string containing a palindrome
 """
 counter = Counter(letters)
 sides = []
 center = deque()
 for letter, occurrences in counter.items():
 repetitions, odd_count = divmod(occurrences, 2)
 if not odd_count:
 sides.append(letter * repetitions)
 continue
 if center:
 raise ValueError("no palindrome exists for '{}'".format(letters))
 center.append(letter * occurrences)
 center.extendleft(sides)
 center.extend(sides)
 return ''.join(center)

Unit tests: test_palindromes.py (using py.test)

def test_empty_string_is_palindrome():
 assert palindrome_from('') == ''
def test_whitespace_string_is_palindrome():
 whitespace = ' ' * 5
 assert palindrome_from(whitespace) == whitespace
def test_rearranges_letters_to_palindrome():
 assert palindrome_from('aabbb') == 'abbba'
def test_raises_exception_for_incompatible_input():
 with pytest.raises(ValueError) as error:
 palindrome_from('asdf')
 assert "no palindrome exists for 'asdf'" in error.value.args

Manual testing in the console

while True:
 try:
 word = input('Enter a word: ')
 print(palindrome_from(word))
 except ValueError as e:
 print(*e.args)
 except EOFError:
 break
asked May 1, 2013 at 12:39
\$\endgroup\$
1
  • \$\begingroup\$ Rather than checking if you already have a letter with odd frequency, you can just count how many you have total, and if it is not equal to mod2 of the length of the string, it can't be a palindrome. In other words, if(number_of_odd_frequencies == len(word) % 2): it is a palindrome \$\endgroup\$ Commented Aug 14, 2015 at 17:26

1 Answer 1

2
\$\begingroup\$
from collections import deque, Counter
def palindrome_from(letters):
 """
 Forms a palindrome by rearranging :letters: if possible,
 throwing a :ValueError: otherwise.
 :param letters: a suitable iterable, usually a string
 :return: a string containing a palindrome
 """
 counter = Counter(letters)
 sides = []
 center = deque()
 for letter, occurrences in counter.items():
 repetitions, odd_count = divmod(occurrences, 2)

odd_count is a bit of a strange name, because its just whether its odd or even, not really an odd_count

 if not odd_count:
 sides.append(letter * repetitions)
 continue

avoid using continue, favor putting the rest of the loop in an else block. Its easier to follow that way

 if center:
 raise ValueError("no palindrome exists for '{}'".format(letters))
 center.append(letter * occurrences)
 center.extendleft(sides)
 center.extend(sides)

Avoid reusing variables for something different. Changing center to be the whole phrase isn't all that good an idea. I suggest using itertools.chain(sides, center, reversed(sides)) and then joining that.

 return ''.join(center)
answered May 1, 2013 at 13:10
\$\endgroup\$
2
  • \$\begingroup\$ Interesting! Using itertools.chain eliminates the need to use a deque I guess. You need to do reversed(sides) for the last argument though (the deque used to do that by itself for some reason). \$\endgroup\$ Commented May 1, 2013 at 13:13
  • \$\begingroup\$ @codesparkle, ah I suppose extendleft probably extends in the backwards order. \$\endgroup\$ Commented May 1, 2013 at 13:19

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.