18
\$\begingroup\$

Is there a better way to do this? Assuming very long lists, is this slow and wasteful?

flavors = ['chocolate', 'vanilla', 'caramel', 'strawberry', 'coffee']

for flavor in flavors:
    print(flavor, end = ', ' if (flavors.index(flavor) != len(flavors) -1) else '.')

Output:

chocolate, vanilla, caramel, strawberry, coffee.

\$\endgroup\$
1
  • 10
    \$\begingroup\$ Note that it outputs a, a, a, for [a, a, a]. \$\endgroup\$ Commented Aug 28, 2019 at 10:50

5 Answers 5

75
\$\begingroup\$

TL;DR:

Despite this being the currently accepted & highest voted answer, these methods from this answer and variations (1, 2) of it:

print(", ".join(flavours) + ".")         # Peilonrayz
print(", ".join(flavours), end=".\n")    # Maarten Fabré
print(f'{", ".join(flavors)}.')          # Andy

are all faster than the solution originally proposed in this answer:

print(*flavours, sep=', ', end='.\n')

Original Answer, Plus Explanation & Timing Analysis:

Consider also:

flavours = ['chocolate', 'vanilla', 'caramel', 'strawberry', 'coffee']
print(*flavours, sep=', ', end='.\n')

This does not perform any unnecessary string concatenation, nor does it require a loop variable to test for the final index.


How does this work?

The print function takes a variable number of arguments, and so would be defined something like:

def print(*args, sep=' ', end='\n', file=sys.stdout, flush=False):
    # ...

except it is a built-in function.

The *args parameter consumes all of the unnamed arguments into one list for processing by the function, allowing the function to take a variable number of arguments.

In the statement,

print(*flavours, sep=', ', end='.\n')

The "splat operator" (*) takes the iterable flavours and expands it into a list of arguments for the function, allowing the caller to pass a variable number of arguments to a function, taken from the contents of a container (list, tuple, etc).

The Python interpreter could match the *flavours splat operator with the *args variable argument list of the print function, and simply pass the flavours list into the args.

But does it? I got worried. Perhaps, because a list is given, and the variable argument list (in CPython) is passed as a tuple, the list actually must be copied. How much time does it take.

After creating a class Null output stream, to speed up the printing, I began passing variable sized lists to the various answers, and profiling the results. While my solution is one of the least amounts of code, it turns out that @Peilonrayz's solution of ", ".join(flavours) seems to be the fastest.

profile results

Using tuples or lists doesn't seem to affect the performance much, so any thought that splatting a tuple instead of a list, to be collected into a *args variable argument tuple may be optimized to a no-op, seems to have been squashed.


Since print will automatically convert objects to strings for printing, the above will work for all object types in a list. The ", ".join(flavours) will only work for strings; it would have to be modified to convert non-strings to strings to be truly equivalent:

print(", ".join(map(str, flavours)) + ".")
\$\endgroup\$
8
  • 9
    \$\begingroup\$ wow that is some serious science for a list glueing task \$\endgroup\$ Commented Aug 28, 2019 at 17:37
  • 3
    \$\begingroup\$ Wouldn't print(', '.join(flavours), end='.\n') work slightly better, since it wouldn't need to make 2 long strings? \$\endgroup\$ Commented Aug 29, 2019 at 11:35
  • \$\begingroup\$ @MaartenFabré Yes, that would be slightly better. But that comment should be on Peilonrayz's answer. When they update their answer to include your improvement, I'll update the last statement of my answer to keep it equivalent to theirs. \$\endgroup\$
    – AJNeufeld
    Commented Aug 29, 2019 at 14:45
  • 1
    \$\begingroup\$ Great answer. stackoverflow.com/help/how-to-answer should redirect here. Every answer should have an "How does this work?" section. \$\endgroup\$
    – voices
    Commented Aug 29, 2019 at 15:16
  • \$\begingroup\$ @tjt263 That's a Stack Overflow link, this is Code Review ;-) \$\endgroup\$
    – Mast
    Commented Aug 30, 2019 at 10:17
42
\$\begingroup\$

The best solution is to use str.join(), using ', ' as the joining string.

def display(flavours):
    print(', '.join(flavours) + '.')

Outputting:

>>> display(['chocolate', 'vanilla', 'caramel', 'strawberry', 'coffee'])
chocolate, vanilla, caramel, strawberry, coffee.

Comparing this to the two adaptions of my approach, and against AJNeufeld in one of the graphs:

image

image

NOTE: Code to plot the graphs, complete changes.

\$\endgroup\$
11
\$\begingroup\$

There is indeed a better way.

flavours = ('chocolate', 'vanilla', 'caramel', 'strawberry', 'coffee')

for i, flavour in enumerate(flavours):
   if i == len(flavours) - 1:
       end = '.'
   else:
       end = ', '
   print(flavour, end=end)

Even better:

print(', '.join(flavours) + '.')

The most expensive part of your solution is the call to index. That needs to be avoided because it does a search for the current item on each iteration.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ So index has to do a linear search through the entire list every time, such that the number of operations would be n/2 * (n+1) ? \$\endgroup\$ Commented Aug 28, 2019 at 2:21
  • 7
    \$\begingroup\$ Correct, but computer scientists instead call that O(n²). \$\endgroup\$
    – Reinderien
    Commented Aug 28, 2019 at 2:32
  • \$\begingroup\$ Between both solutions you proposed, it's possible to use a loop without if. Here's an example. \$\endgroup\$ Commented Aug 28, 2019 at 10:54
  • \$\begingroup\$ @EricDuminil Yum, \$O(n)\$ memory, doesn't support lists of nothing and it doesn't support iterators! For \$O(1)\$ memory, no if and to support iterators you can do it this way. You can get rid of the if outside the loop with a try if you think that still counts. \$\endgroup\$
    – Peilonrayz
    Commented Aug 28, 2019 at 13:27
  • 1
    \$\begingroup\$ @Peilonrayz: Not my proudest piece of code, I'll give you that :D. It has the merit of being simple and short, and not asking at every iteration "Are we there yet?". I'd never use it though, for the reasons you mentioned. \$\endgroup\$ Commented Aug 28, 2019 at 13:44
5
\$\begingroup\$

Using f-strings, which are pretty fast:

flavors = ['chocolate', 'vanilla', 'caramel', 'strawberry', 'coffee']

print(f'{", ".join(flavors)}.')
\$\endgroup\$
9
  • 1
    \$\begingroup\$ With those error margins it's hard to say. \$\endgroup\$
    – G. Sliepen
    Commented Aug 28, 2019 at 18:47
  • 2
    \$\begingroup\$ I do not trust a single timeit call, please look at AJNeufeld's edits to see why (those peaks that mean nothing). You should make a graph like AJNeufeld. \$\endgroup\$
    – Peilonrayz
    Commented Aug 28, 2019 at 19:58
  • 3
    \$\begingroup\$ Yeah @Andy you should make graphs like AJNeufeld. Be more like him. \$\endgroup\$
    – user14492
    Commented Aug 29, 2019 at 13:32
  • 3
    \$\begingroup\$ I've added your answer to my time-plots. Performance seems effectively identical. \$\endgroup\$
    – AJNeufeld
    Commented Aug 29, 2019 at 14:48
  • 5
    \$\begingroup\$ @user14492 are you... are you one of my coworkers? \$\endgroup\$
    – Andy
    Commented Aug 29, 2019 at 14:58
3
\$\begingroup\$

The built-in print function (in Python 3+) has a sep (separator) argument. Performance is not as good as with str.join, but I just thought I'd mention it. In combination with * argument unpacking:

print(*flavours, sep=', ', end='.\n')

\$\endgroup\$
1
  • \$\begingroup\$ Interesting. Do you have any performance numbers to back up this claim, like here or here? \$\endgroup\$
    – AJNeufeld
    Commented Sep 1, 2019 at 15:04

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.