Nice work on this post. It was easy to follow your line of exploration and thinking. May I suggest a couple of simplifications.
The generator expression in the sum() just applies a one-argument function so it can be replaced with map().
And instead of computing a sign at each step, it is simpler and faster to negate the roots at the outset.
There actually is a combinations(..., 0) in Python, and it returns [()] which is consistent with math.comb(4, 0) returning 1. Also, prod([]) returns 1. Putting those facts together helps eliminate special case at the end point.
Here's the simplified code:
from itertools import combinations
from operator import neg
from math import prod
def coeffs_from_roots(roots):
roots = list(map(neg, roots))
return [
sum(map(prod, combinations(roots, k)))
for k in range(len(roots) + 1)
]
I'm not sure why I missed the combinations(..., 0), I'm pretty certain to have tested in a previous iteration (maybe because I wasn't using math.prod() initially); code and text adjusted.
For the map yeah I thought it wasn't recommended by Python style but I personally prefer it so I'm adopting that change as well.
For the sign change, your initial comment (before your edits) was proposing the (1, -1)[i & 1] trick, which I actually like better than negating all the roots. At least for clarity in understanding the blog post (which mentions the sign juggling), I think it's better to keep it that way. I added a link to your comment for anyone looking for further improvement.
The generator expression in the sum() just applies a one-argument function so it can be replaced with map(). And instead of computing a sign at each step, it is simpler and faster to negate the roots at the outset.
There actually is a combinations(..., 0) in Python, and it returns [()] which is consistent with math.comb(4, 0) returning 1. Also, prod([]) returns 1. Putting those facts together helps eliminate special case at the end point.
Here's the simplified code: