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.
5 Answers 5
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.
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)) + ".")
-
9\$\begingroup\$ wow that is some serious science for a list glueing task \$\endgroup\$aaaaa says reinstate Monica– aaaaa says reinstate Monica2019年08月28日 17:37:05 +00:00Commented 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\$Maarten Fabré– Maarten Fabré2019年08月29日 11:35:47 +00:00Commented 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– AJNeufeld2019年08月29日 14:45:58 +00:00Commented 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– voices2019年08月29日 15:16:38 +00:00Commented Aug 29, 2019 at 15:16
-
\$\begingroup\$ @tjt263 That's a Stack Overflow link, this is Code Review ;-) \$\endgroup\$2019年08月30日 10:17:31 +00:00Commented Aug 30, 2019 at 10:17
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.
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.
-
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– jeremy radcliff2019年08月28日 02:21:32 +00:00Commented Aug 28, 2019 at 2:21
-
7\$\begingroup\$ Correct, but computer scientists instead call that O(n²). \$\endgroup\$Reinderien– Reinderien2019年08月28日 02:32:44 +00:00Commented 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\$Eric Duminil– Eric Duminil2019年08月28日 10:54:53 +00:00Commented 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\$2019年08月28日 13:27:02 +00:00Commented 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\$Eric Duminil– Eric Duminil2019年08月28日 13:44:22 +00:00Commented Aug 28, 2019 at 13:44
Using f-strings, which are pretty fast:
flavors = ['chocolate', 'vanilla', 'caramel', 'strawberry', 'coffee']
print(f'{", ".join(flavors)}.')
-
1\$\begingroup\$ With those error margins it's hard to say. \$\endgroup\$G. Sliepen– G. Sliepen2019年08月28日 18:47:40 +00:00Commented 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\$2019年08月28日 19:58:19 +00:00Commented Aug 28, 2019 at 19:58
-
3\$\begingroup\$ Yeah @Andy you should make graphs like AJNeufeld. Be more like him. \$\endgroup\$user14492– user144922019年08月29日 13:32:14 +00:00Commented Aug 29, 2019 at 13:32
-
3\$\begingroup\$ I've added your answer to my time-plots. Performance seems effectively identical. \$\endgroup\$AJNeufeld– AJNeufeld2019年08月29日 14:48:17 +00:00Commented Aug 29, 2019 at 14:48
-
5\$\begingroup\$ @user14492 are you... are you one of my coworkers? \$\endgroup\$Andy– Andy2019年08月29日 14:58:16 +00:00Commented Aug 29, 2019 at 14:58
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')
a, a, a,
for[a, a, a]
. \$\endgroup\$