15
\$\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\$
  • 10
    \$\begingroup\$ Note that it outputs a, a, a, for [a, a, a]. \$\endgroup\$ – Eric Duminil Aug 28 at 10:50
71
\$\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\$
  • 7
    \$\begingroup\$ wow that is some serious science for a list glueing task \$\endgroup\$ – Oct18 is day of silence on SE Aug 28 at 17:37
  • 2
    \$\begingroup\$ Wouldn't print(', '.join(flavours), end='.\n') work slightly better, since it wouldn't need to make 2 long strings? \$\endgroup\$ – Maarten Fabré Aug 29 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 Aug 29 at 14:45
  • \$\begingroup\$ Great answer. stackoverflow.com/help/how-to-answer should redirect here. Every answer should have an "How does this work?" section. \$\endgroup\$ – voices Aug 29 at 15:16
  • \$\begingroup\$ @tjt263 That's a Stack Overflow link, this is Code Review ;-) \$\endgroup\$ – Mast Aug 30 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\$
10
\$\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\$
  • 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\$ – jeremy radcliff Aug 28 at 2:21
  • 6
    \$\begingroup\$ Correct, but computer scientists instead call that O(n²). \$\endgroup\$ – Reinderien Aug 28 at 2:32
  • \$\begingroup\$ Between both solutions you proposed, it's possible to use a loop without if. Here's an example. \$\endgroup\$ – Eric Duminil Aug 28 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 Aug 28 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\$ – Eric Duminil Aug 28 at 13:44
3
\$\begingroup\$

Using f-strings, which are pretty fast:

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

print(f'{", ".join(flavors)}.')
\$\endgroup\$
  • 1
    \$\begingroup\$ With those error margins it's hard to say. \$\endgroup\$ – G. Sliepen Aug 28 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 Aug 28 at 19:58
  • 3
    \$\begingroup\$ Yeah @Andy you should make graphs like AJNeufeld. Be more like him. \$\endgroup\$ – user14492 Aug 29 at 13:32
  • 3
    \$\begingroup\$ I've added your answer to my time-plots. Performance seems effectively identical. \$\endgroup\$ – AJNeufeld Aug 29 at 14:48
  • 5
    \$\begingroup\$ @user14492 are you... are you one of my coworkers? \$\endgroup\$ – Andy Aug 29 at 14:58
1
\$\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\$
  • \$\begingroup\$ Interesting. Do you have any performance numbers to back up this claim, like here or here? \$\endgroup\$ – AJNeufeld Sep 1 at 15:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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