Skip to content

[Sum of Multiples] Add Approaches - #3375

Closed
MatthijsBlom wants to merge 4 commits into
exercism:mainfrom
MatthijsBlom:approach-sum-of-multiples
Closed

[Sum of Multiples] Add Approaches#3375
MatthijsBlom wants to merge 4 commits into
exercism:mainfrom
MatthijsBlom:approach-sum-of-multiples

Conversation

@MatthijsBlom

Copy link
Copy Markdown
Contributor

No description provided.

@MatthijsBlom
MatthijsBlomforce-pushed the approach-sum-of-multiples branch from c2db761 to a4bb5cdCompareMarch 30, 2023 16:54
@@ -0,0 +1,3 @@
def sum_of_multiples(limit, factors):
is_multiple = lambda n: any([n % f == 0 for f in factors if f != 0])
return sum(filter(is_multiple, range(limit)))

@BethanyGBethanyGApr 2, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is problematic, as it binds a name to a lambda, so needs re-work.

I think this is also referenced in the introduction. We'll need to remove all of them.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…I now see I also left a temporarily inserted list comprehension in there.

Would you rather have

defsum_of_multiples(limit, factors):
returnsum(filter(
lambdan: any(n%f==0forfinfactorsiff!=0),
range(limit)
))

and keep the explanation of lambda, or

defsum_of_multiples(limit, factors):
defis_multiple (n):
returnany(n%f==0forfinfactorsiff!=0)
returnsum(filter(is_multiple, range(limit)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either work for me. The first might be preferable, considering that you've already done a good explanation of lambda. The second one would need an explanation of nested functions. Absolutely not opposed to doing that - but it is extra work for you.

lambda can sometimes be slower in filter or map, since it opens another stack frame. But that's sorta irrelevant to this particular exercise, methinks.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lambda can sometimes be slower in filter or map, since it opens another stack frame.

Can you elaborate or link to a source on this? I had no idea.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TL;DR is that lambda has all the overhead/execution of any other function call (here's a link to python tutor for the two examples above), so in any situation where you are using a lambda over a built-in, or in place of a generator expression that does the same/similar operation, you will incur the 'extra' overhead of the function call. Trivial in small/medium cases -- but it can add up for larger data sets.

And where you're converting that filter + lambda into a list or other 'realized' structure, it will be a lot slower than the corresponding comprehension or generator, due to the overhead of calling an additional function for every item in the list.

But this varies widely (since python 3.x returns iterators instead of lists) - if you don't need to realize the values and can consume them lazily (like in a call to sum()), then filter/map/reduce outperform comprehensions, and are mostly even for generators. But again, a generator that doesn't call an extra function will be faster than filter + lambda (because of the lambda function call).

Here are some articles - but many of them assume that values need to be realized, and so aren't really comparing apples to apples. The finxter blog (apologies for the aggressive ads there!) does do the comparisons for both realized and un-realized data - and you can really see the difference.

And I'll provide this for completeness, although its really mostly a rant about personal preference with readability, and not on performance: Trey Hunner: Stop Writing Lambda Expressions

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I haven't read all the links yet, but the gist certainly makes sense. I thought previously you meant that

deff(x): returnf_body(x)
map(f, xs)
# be faster thanmap(lambdax: f_body(x), xs)

That in-lined functions in comprehensions are faster than mapped functions I already expected.

# LIGHTNING
```

An iterator is a bit like a cursor that can move only to the right.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this explanation. Both from a cursor for typing ... and a cursor from a DB. Both can only be gone through once, and you can't back up. 😄

@MatthijsBlom

Copy link
Copy Markdown
ContributorAuthor

U-oh, yet another approach:

fromheapqimportheapify, heapreplacefromitertoolsimporttakewhiledefsum_of_multiples(limit, factors):
defmultiples():
queue= [(_, _) for_infactorsif_!=0]
heapify(queue)
previous_multiple=0whilequeue:
multiple, factor=queue[0]
ifmultiple!=previous_multiple:
yieldmultipleprevious_multiple=multipleheapreplace(queue, (multiple+factor, factor))
returnsum(takewhile(lambdan: n<limit, multiples()))

@BethanyG

BethanyG commented Apr 11, 2023

Copy link
Copy Markdown
Member

WOOT! I was going to mention that one when you were posting in the forum, and then got distracted and wandered away. Glad you remembered it! 😄

@MatthijsBlom

MatthijsBlom commented Apr 11, 2023

Copy link
Copy Markdown
ContributorAuthor

Yesterday or this morning I remembered Eppstein's/Martelli's primes generator (improvements on StackOverflow); the above solution immediately followed.

Yet another approach:

fromitertoolsimportcombinationsfrommathimportlcmdefsum_of_multiples(limit, factors):
factors= [_for_infactorsif_!=0]
defrange_sum(d):
# `sum(range(0, limit, d))` but in constant timeq= (limit-1) //dreturnd*q* (q+1) //2returnsum(
# inclusion/exclusion
(-1) ** (r-1) *range_sum(lcm(*fs))
forr, _inenumerate(factors, start=1)
forfsincombinations(factors, r)
)

@MatthijsBlom

MatthijsBlom commented Apr 12, 2023

Copy link
Copy Markdown
ContributorAuthor

@BethanyG Do you still wish for these lambda assignments to be avoided?

My own preference is to revert most of 5dbe577. (I'd like to keep the admonitions.) I could add some more prose on the matter, or lift the deleted paragraph into a exercism/caution, or something.

@BethanyG

Copy link
Copy Markdown
Member

@MatthijsBlom - Any interest in continuing work on this? It's been open since March of 2023, and since we're in the process of upgrading to Python 3.13, it may need some re-working. If I don't hear from you by next week (1/5/2026), I'll close it. Just let me know - Thanks!

@BethanyG

Copy link
Copy Markdown
Member

Closing this as it needs a re-read and rework for Python 3.13.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@MatthijsBlom@BethanyG@IsaacG