7
\$\begingroup\$

I have written a palindrome program. And I was just wondering if it correct? I checked using the words racecar and madam, and it says they are palindromes. I have also tested nonpalindrome words and it worked correctly.

Is my implementation correct? If it's not could you correct it for me. It it works correct, is there an easier way to implement this?

def is_palindrome(x):

    for i in range(0,len(x)):
        x = x.lower()
        if x[i::1] == x[::-1]:
            return("%s Is a palindrome" %(x))
        else:
            return("%s Is not a palindrome" %(x))

print(is_palindrome('racecar'))
\$\endgroup\$
0

2 Answers 2

9
\$\begingroup\$

I would expect a function that has a "predicate" name (is_x()) to return a boolean value rather than a string. The boolean is much more useful to the caller.

We should add a main-guard to top-level code, so the file could be useful as a module.

Consider adding some unit tests. A convenient way to do this is using the doctest module, which finds tests in the function's docstring; any line beginning with >>> is a test, and is immediately followed by the expected result.

Building on the version shown by C.Nivs, that would give this improved version:

def is_palindrome(x):
    '''
    True if the letters of x are the same when
    reversed, ignoring differences of case.
    >>> is_palindrome('')
    True
    >>> is_palindrome('a')
    True
    >>> is_palindrome('a0')
    False
    >>> is_palindrome('ab')
    False
    >>> is_palindrome('ab a')
    False
    >>> is_palindrome('Aba')
    True
    >>> is_palindrome('Àbà')
    True
    '''
    x = x.lower()
    # Alternative (letters only):
    #     x = ''.join(filter(str.isalpha, x.lower()))
    # Alternative (letters and digits only):
    #     x = ''.join(filter(str.isalnum x.lower()))
    return x[::-1] == x

if __name__ == '__main__':
    import sys
    for string in sys.argv[1:]:
        # check each command-line argument
        message = 'is a palindrome' if is_palindrome(string) else 'is not a palindrome'
        print(f'{string} {message}')
    else:
        # no arguments, so run self-test
        import doctest
        doctest.testmod()

It may well make sense to separate the pre-processing from the palindrome checking, so that is_palindrome() simply returns x[::-1] == x, and the caller is responsible for deciding which characters are significant. We could call it with a specific canonicalisation:

is_palindrome(string.lower())

More advanced would be to pass a normalisation function and/or a character filter, as optional arguments:

def is_palindrome(s, normalise=None, char_filter=None):
    '''
    True if the letters of x are the same when
    reversed, ignoring differences of case.
    >>> is_palindrome('')
    True
    >>> is_palindrome('a')
    True
    >>> is_palindrome('a0')
    False
    >>> is_palindrome('a0', char_filter=str.isalpha)
    True
    >>> is_palindrome('ab')
    False
    >>> is_palindrome('ab a')
    False
    >>> is_palindrome('Aba')
    False
    >>> is_palindrome('Aba', normalise=str.lower)
    True
    >>> is_palindrome('Àbà', normalise=str.lower)
    True
    '''
    if normalise:
        s = normalise(s)
    if char_filter:
        s = ''.join(filter(char_filter, s))
    return s == ''.join(reversed(s))

Usage of these optional arguments should be apparent from the extra tests I've added.


Bug

Whether we use [::-1] or reversed() to reverse the input string, we have a bug when the input contains characters that are formed using combining accents, as demonstrated by these additional tests:

>>> is_palindrome("o̅o") # o+overline, o
False
>>> is_palindrome("o̅o̅") # o+overline, o+overline
True

Both of these fail, because we reverse the string by treating each Unicode character independently, rather than considering them as (possibly multi-character) letters. To fix that, we'll need to find or create a combining-character-aware function to reverse a string - I'm not aware of such a thing in the Python standard library.

\$\endgroup\$
1
  • \$\begingroup\$ Yeah im a beginner if you can't already tell lol but I mean the program is still working and putting out positive results but I guess I need to make it more clearer and organized \$\endgroup\$ Commented Jan 30, 2020 at 1:37
8
\$\begingroup\$

You don't need the loop. Once i > 0, the if statement will always be False:

x = 'abba'
x[0::1] == x[::-1]
True

x[1::1] == x[::-1]
False

x[1::1]
'bba'

So drop the iteration. Besides, the immediate return won't allow for iteration anyways, a second iteration will never be reached:

def is_palindrome(x):
    x = x.lower()

    if x[::-1] == x:
        return f'{x} is a palindrome'
    else:
        return f'{x} is not a palindrome'

You don't need the parentheses with return, and I'd use f-strings instead of % formatting if you're on python 3.5+.

If you are adding the possibilities of spaces/punctuation, you could change x to only include alphabet chars:

def is_palindrome(x):
    # this will produce only alphabet lowercase chars
    # and join them into a string, though I'm not sure if
    # this is a use case for you
    x = ''.join(filter(str.isalpha, x.lower()))

    if x[::-1] == x:
        return f'{x} is a palindrome'
    else:
        return f'{x} is not a palindrome'

To show what that join statement does:

# maybe this should be a palindrome if you ignore the
# non-alpha chars including spaces
x = "ABC1234CB a."

y = ''.join(filter(str.isalpha, x.lower()))
'abccba'

Edit

To address concerns in the comments, if you wanted to offer options into what kind of filtering you want to provide, you could use a dictionary to act as a mapping:

from functools import partial

def is_palindrome(input_str, filter_type='nofilter'):
   """
   Parameter 'filter_type' defaults to pass-through, but you can
   provide options such as 'alphanum', 'nospace' (to just get rid of spaces), and 'alpha'
   """
   filters = {
       'alpha': partial(filter, str.isalpha),
       'alphanum': partial(filter, str.isalnum),
       'nospace': partial(filter, lambda char: not char.isspace()),
       'nofilter': partial(map, lambda char: char) # this is just a pass-through 
   }

   # raise this exception just so the use is more clear to the user
   # what is expected
   try:
       f = filters[filter_type]
   except KeyError as e:
       raise ValueError(
           f"Invalid filter_type, choose one of {'\n'.join(filters)}"
       ) from e

   x = ''.join(filter_type(input_str.lower()))

   # you can just check the first half of the string
   # against the last half reversed, rather than comparing the entire string 
   midpoint = len(x) // 2
   if len(x) % 2: # even length
       a = b = midpoint
   else:
       a, b = midpoint + 1, midpoint

   return x[:a] == x[b::-1]

partial will bind arguments to a function and return a new callable. As a small example:

def f(a):
    return a

g = partial(f, 1)
f(2)
2

g()
1

Which is helpful for taking a function that takes many arguments and returning one that takes fewer arguments.

Credit to Toby Speight for returning bool type, @Baldrickk for midpoint slice, and @Peilonrayz for concern over input filtering.

\$\endgroup\$
13
  • \$\begingroup\$ Why have you added str.isalpha? 0w0 is a palindrome, not because you mutilate it to become w. \$\endgroup\$
    – Peilonrayz
    Commented Jan 30, 2020 at 14:00
  • \$\begingroup\$ @Peilonrayz sure, that didn't appear to be OP's use case. I didn't see any clarification from OP that this needed to be the case, and I don't see how it makes the review wrong. I also mentioned that OP could have this option, but it's not necessary \$\endgroup\$
    – C.Nivs
    Commented Jan 30, 2020 at 14:49
  • 1
    \$\begingroup\$ @Peilonrayz it could easily be made to be alphanumeric, which would make sense, but it does show the method by which would be done, so I wouldn't fault the answer for this. \$\endgroup\$
    – Baldrickk
    Commented Jan 30, 2020 at 14:53
  • 1
    \$\begingroup\$ An additional note - you don't need to compare the full string x with its reverse - you need only compare the first half with the reverse of the second half. \$\endgroup\$
    – Baldrickk
    Commented Jan 30, 2020 at 14:55
  • \$\begingroup\$ @Baldrickk And why o' why would I ever want my is_palindrome to mangle my input? Not only does it violate SRP, it makes absolutely no sense for it to be there. If you want to check if only the alpha is a palindrome, then make a filter_to_alpha function and use is_palindrome(filter_to_alpha(value)). \$\endgroup\$
    – Peilonrayz
    Commented Jan 30, 2020 at 15:55

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.