24
\$\begingroup\$

I'm currently learning Python using the book mentioned in the title. I just finished reading about lists, tuples, functions and methods, and one of the projects following this chapter is to create a program which takes in a list as an input and outputs a corresponding string, as the following example showcases:

Input: ['apples', 'bananas', 'tofu', 'cats']

Output: apples, bananas, tofu, and cats

This should of course work for any list. So I've come up with the following code:

def listToString(someList):
    for i in range(len(someList)):
        newString = str(someList[i])

        if i == (len(someList)-1):
            print('and ' + str(someList[i]))
        else:
            print(newString, end=', ')

someList = ['apples','bananas','tofu','cats']
listToString(someList)

I feel like I haven't used everything (for example, some methods) which were taught in the chapter to solve the problem. Is there a more efficient way to code this project?

\$\endgroup\$
  • 6
    \$\begingroup\$ There's no reason to use range(len(_)) in Python. Just iterate over the collection itself. If you need the indices as well, use enumerate. \$\endgroup\$ – SwiftsNamesake Oct 7 '17 at 19:36
  • 1
    \$\begingroup\$ This is actually quite a tricky exercise due to all the special cases for short lists. (It appears to want output in standard written English, but in that case "and" is appropriate only for lists of 2 or more items and the Oxford comma should appear only in lists of 3 or more items. That's a relatively complicated specification for an introductory exercise, unless the point was how to deal with complex specifications.) \$\endgroup\$ – David K Oct 8 '17 at 19:59
24
\$\begingroup\$

You could have used various methods with a bit of list slicing.

I don't know if you had learned the .join() and .format() methods in your book, but those I will use.

A few remarks:

  • Functions and variables should be snake_case instead of CamelCase
  • List-to-string should return a string instead of printing it because that is what the name says

def list_to_string(a_list):
    return '{0} and {1}'.format(', '.join(a_list[:-1]), a_list[-1])

some_list = ['apples','bananas','tofu','cats']
print(list_to_string(some_list))

How does it work?

With .format() you can make clean string concatenations. The {0} is replaced by the ', '.join(a_list[:-1]) which joins the list until the last item [:-1] together with a comma and a space. The {1} is replaced by the last item in the list a_list[-1].

\$\endgroup\$
  • \$\begingroup\$ Thanks for the tips, I really need to get used to the naming convention. I do know about the .format() function! .join() I'm not aware of. Basically, the .join() function takes the elements from a list and joins them, until the given index? Very clever with the format() function btw, this just did not occur to me! Thanks for your time! \$\endgroup\$ – sar91 Oct 7 '17 at 14:17
  • 15
    \$\begingroup\$ This has three disadvantages, though: 1. It only works with lists of strings 2. For lists of length 1, like ["foo"], it returns " and foo" (the OP's code has the same problem) 3. Empty lists raise an IndexError. \$\endgroup\$ – Graipher Oct 7 '17 at 15:24
  • 2
    \$\begingroup\$ Worth mentioning that modern Python has formatted string literals (ie f'{', '.join(a_list[:-1])} and {a_list[-1]}') \$\endgroup\$ – gntskn Oct 7 '17 at 22:59
  • 1
    \$\begingroup\$ Slight nitpick: you need a comma after {0}, because the example includes the Oxford comma. \$\endgroup\$ – Mego Oct 8 '17 at 9:57
  • 2
    \$\begingroup\$ @gntskn Don't you mean f"{', '.join(a_list[:-1])} and {a_list[-1]}"? \$\endgroup\$ – wizzwizz4 Oct 8 '17 at 19:26
25
\$\begingroup\$

Ludisposed's answer is already a great improvement, however it fails to take into account some cornercases:

  1. It only works with lists of strings.
  2. For lists of length 1, like ["foo"], it returns " and foo" (your code has the same problem).
  3. It raises an IndexError on an empty list.

This code avoids these problems:

def list_to_string(a_list):
    if not a_list:
        return ""
    elif len(a_list) == 1:
        return str(a_list[0])
    else:
        body = ", ".join(map(str, a_list[:-1]))
        return '{0} and {1}'.format(body, a_list[-1])

This does not currently have the Oxford comma, like your code. But you can easily add it as another special case (don't add it when len(a_list) == 2 and add it otherwise). Thanks to @DavidK for pointing this out in the comments.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the great insights! Your second point is absolutely correct, I hadn't tested my code with just one element in the list. Most of your code I understood, apart from two things: (i) the first if statement, I'm a bit confused as to what it is checking? 'if not a_list' - does this check whether the a_list is empty? (ii) the 'map' statement in the .join() function. I'll google it now, but any insights about it would be appreciated! Thanks a lot for your time. \$\endgroup\$ – sar91 Oct 7 '17 at 19:22
  • 3
    \$\begingroup\$ Empty lists are considered Falsey in python. The map(str, list) converts the entire list to strings. \$\endgroup\$ – Ludisposed Oct 7 '17 at 19:31
  • 1
    \$\begingroup\$ yes, if checks the following statement for truthiness. An empty list is falsey, a non-empty list truthy and the not inverts this, of course. map takes a function (here str) and applies it to each element of an iterable (like a list). It returns a map object in Python, which is iterable again. In Python 2 it simply returned a list \$\endgroup\$ – Graipher Oct 7 '17 at 19:32
  • 1
    \$\begingroup\$ @sar91 This is the case for most objects in Python. Empty lists, strings, tuples, sets, dictionaries... Are all falsey. In general, any object with len(obj) == 0 is falsey (unless you specify it differently, which you can). \$\endgroup\$ – Graipher Oct 8 '17 at 8:48
  • 1
    \$\begingroup\$ In the question and all the answers here (so far), there is the question of the usage of the Oxford comma, which is a comma that appears in lists of 3 or more items (missing from a couple of answers, including this one) but not in lists of 2 items (but inserted by the OP and another answer). The reason I like this answer is that it is easily extended to differentiate lists of 2 from lists of 3 and to handle the comma appropriately in each case. \$\endgroup\$ – David K Oct 8 '17 at 19:55
18
\$\begingroup\$

Fundamental Problems

Considering you're learning, I would like to comment on what I see are fundamental problems with the function, not just how concise you can make the code.

No return value

A very big part of programming is making your functions modular, and we usually do this by breaking down the task into pieces which can be repeated. Using print statements like you did will print out to the console and might even make sense for your current problem, but what happens when you want to use this elsewhere? What if you want to save the fancy string to disk?

The answer to this is to return the string as the other answers have done. Then when you have the string you can do what you want with it like printing it or writing it to disk.

You should get into the habit of returning values from your functions.

Misleading Names

The function name implies that it returns a string from a list. Imagine you come back to your code four months from now and try to use this function to write to a file you would be justified into writing something like this:

fancy_string = list_to_string(groceries)
with open('groceries.txt', 'w') as groceries_file:
    groceries_file.write(fancy_string)

However, this will not work, because there is no return value here.

Naming is a seriously hard thing to do in programming, so consider your names carefully.

Python Style

Now into more minor things I'd like to point out to make the code more pythonic.

enumerate is your friend

This loop fragment:

for i in range(len(someList)):
    newString = str(someList[i])

can be replaced with:

for i, newString in enumerate(someList):

Also now replace instances of someList[i] with newString:

def listToString(someList):
    for i, newString in enumerate(someList):
        if i == (len(someList)-1):
            print('and ' + str(newString))
        else:
            print(newString, end=', ')

Even though in this case you should be using answers more along the line of the other posts it's good to know about enumerate for cases where you need the index along with the element of a list. I encourage you to use the Python for-loop construct without indexing things as much as possible.

PEP 8

As others have pointed out, the code is not to the Python standard PEP 8. While I'm not aware of code that strictly follows PEP 8 you should take a look at it to see what it looks like.

\$\endgroup\$
  • \$\begingroup\$ The enumerate() function is pretty cool. I will definitely keep it in mind. Thank you for all of the other great tips as well, very much appreciated!! Very good point about making functions modular btw - makes the necessity of a return value very apparent! \$\endgroup\$ – sar91 Oct 8 '17 at 7:12
2
\$\begingroup\$

Some of the other answers give great instruction but the code examples introduce some readability issues that I would not recommend.

Compare this example to some of the others:

def list_to_string(list_object):
    # corner case 1: not a list object
    if not isinstance(list_object, list):
        raise ValueError('function parameter is not a list')

    # corner case 2: list object has 0 element
    if not list_object:
        raise ValueError('list is empty aka evaluated as false')

    # corner case 3: list object has 1 element
    if len(list_object)==1:
        return str(list_object[0])

    # actual use case
    else:
        # convert list elements to string
        str_list = [str(x) for x in list_object]
        bulk, last = str_list[:-1], str_list[-1]
        bulk_str = ', '.join(bulk)
        new_string = bulk_str + ' and ' + last
        return new_string

This covers most of the corner cases mentioned above.

alist = ['apples', 'bananas', 'tofus', 'cats']

list_to_string(alist)
# 'apples, bananas, tofus, and cats'

list_to_string(['apples'])
# 'apples'

list_to_string(10)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-30-8eea34c27d99> in <module>()
----> 1 list_to_string(10)

<ipython-input-27-37b4b95abce9> in list_to_string(list_object)
      3 def list_to_string(list_object):
      4     if not isinstance(list_object, list):
----> 5         raise ValueError('function parameter is not a list')
      6     if len(list_object)==1:
      7         return list_object[0]

ValueError: function parameter is not a list

blist = [2, 4, 5, 6]
list_to_string(blist)
# '2, 4, 5 and 6'

clist = []
list_to_string(clist)
# ---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-27-149a45f1b560> in <module>()
      1 clist = []
----> 2 list_to_string(clist)

<ipython-input-24-70c66e3078ce> in list_to_string(list_object)
      6     # corner case 2: list object has 0 element
      7     if not list_object:
----> 8         raise ValueError('list is empty aka evaluated as false')
      9 
     10     # corner case 3: list object has 1 element

ValueError: list is empty aka evaluated as false
\$\endgroup\$
  • \$\begingroup\$ I might add that f strings or .format() is cleaner than string concatenation. Secondly this fails for a list of int as handled by @Grapiher \$\endgroup\$ – Ludisposed Oct 9 '17 at 15:22
  • \$\begingroup\$ Also you could return the string in the else in one go instead of first assigning it to a variable. Like return f'{body} and {last}' \$\endgroup\$ – Ludisposed Oct 9 '17 at 15:27
  • \$\begingroup\$ added code to handle more corner cases. This is not the most concise code but I believe the clarity is emphasized. \$\endgroup\$ – BCR Oct 9 '17 at 15:48
2
\$\begingroup\$

You could have simply broken this problem in two parts, where first part would be printing the list items just before where and starts and then print last two items of the list (and included).

spam = ['apples', 'bananas', 'tofu', 'cats', 'mewoeto']
def andInsertedList(spamList):
    for i in range(len(spamList)-1):
        print(spamList[i], end=', ')  #First Part
    print('and ' + spamList[len(spamList)-1])  #Second Part  

andInsertedList(spam)
\$\endgroup\$
  • \$\begingroup\$ The last line: "spamList[len(spamList)-1]" doesn't need to calculate the index value of the last item "len(spamList)-1" to retrieve it. You can simply use "spamList[-1]" \$\endgroup\$ – DataWriter May 17 at 18:16

protected by Community Dec 27 '18 at 7:47

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

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