From a0c5c07f1aa0467a4a61f157c9e95eca83d5a36a Mon Sep 17 00:00:00 2001 From: Yrahcaz7 <74512479+Yrahcaz7@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:06:46 -0400 Subject: [PATCH] apply audit items also a few other minor formatting fixes, like fixing the alignment of the performance article table --- .../raindrops/.approaches/config.json | 24 ++++-- .../.approaches/dict-and-join/content.md | 12 +-- .../.approaches/functools-reduce/content.md | 14 +-- .../.approaches/functools-reduce/snippet.txt | 6 +- .../.approaches/if-statements/content.md | 16 ++-- .../raindrops/.approaches/introduction.md | 85 ++++++++++--------- .../.approaches/itertools-compress/content.md | 13 +-- .../itertools-compress/snippet.txt | 6 +- .../.approaches/loop-and-fstring/content.md | 24 ++++-- .../.approaches/loop-and-fstring/snippet.txt | 4 +- .../.approaches/sequence-with-join/content.md | 28 +++--- .../sequence-with-join/snippet.txt | 8 +- .../truthy-and-falsey-with-fstring/content.md | 13 +-- .../practice/raindrops/.articles/config.json | 5 +- .../.articles/performance/content.md | 42 ++++----- .../.articles/performance/snippet.md | 16 ++-- 16 files changed, 173 insertions(+), 143 deletions(-) diff --git a/exercises/practice/raindrops/.approaches/config.json b/exercises/practice/raindrops/.approaches/config.json index ec50a5f52dd..de1388abed3 100644 --- a/exercises/practice/raindrops/.approaches/config.json +++ b/exercises/practice/raindrops/.approaches/config.json @@ -1,6 +1,7 @@ { "introduction": { - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, "approaches": [ { @@ -8,49 +9,56 @@ "slug": "if-statements", "title": "if statements", "blurb": "Use a series of if statements and string concatenation.", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, { "uuid": "55b0a949-e84e-4772-8a2c-368b491d72e2", "slug": "loop-and-fstring", "title": "Loop and f-string", "blurb": "Loop through a tuple and assemble output via an f-string.", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, { "uuid": "9785ad3a-fc52-409d-bfa4-d97926a87d22", "slug": "sequence-with-join", "title": "Sequence(s) with str.join()", "blurb": "Use one or more sequences and str.join() with a generator-expression.", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, { "uuid": "b4c3608e-d4f4-4a50-8da7-66c655241950", "slug": "truthy-and-falsey-with-fstring", "title": "Truthy and Falsey Values with f-strings", "blurb": "Use ternaries to build words and an f-string to assemble the result.", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, { "uuid": "78392dda-921b-4308-bdc7-8993898987ba", "slug": "dict-and-join", "title": "Dict and str.join()", "blurb": "Use a dict to hold factors:values and str.join() with a generator-expression.", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, { "uuid": "7e485b55-6f47-4288-ac18-d9f57fbdc0c2", "slug": "itertools-compress", "title": "itertools.compress", "blurb": "Use itertools.compress() with a list mask.", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, { "uuid": "298fbc79-7946-4cb1-b820-e2e282b1f155", "slug": "functools-reduce", "title": "functools.reduce", "blurb": "Use functools.reduce() and zip().", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] } ] } diff --git a/exercises/practice/raindrops/.approaches/dict-and-join/content.md b/exercises/practice/raindrops/.approaches/dict-and-join/content.md index 94be03cb0a1..12ea169d9d7 100644 --- a/exercises/practice/raindrops/.approaches/dict-and-join/content.md +++ b/exercises/practice/raindrops/.approaches/dict-and-join/content.md @@ -35,6 +35,9 @@ def convert(number): The advantage of the generator expression is that no intermediary `list` is created in memory. This will definitely save memory, and might also be slightly faster than a "classic" loop that appends to a `list`. + +## Variation 1 + Finally, this could all be done as a 'one liner'. But this becomes both harder to read and harder to maintain: @@ -42,12 +45,11 @@ But this becomes both harder to read and harder to maintain: def convert(number): return ''.join(sound for divisor, sound in {3: 'Pling', 5: 'Plang', 7: 'Plong'}.items() - if (number % divisor == 0)) or str(number) + if number % divisor == 0) or str(number) ``` -[dict-view-object]: https://docs.python.org/3/library/stdtypes.html#dictionary-view-objects -[dict.keys()]: https://docs.python.org/3/library/stdtypes.html#dict.keys +[dict-view-object]: https://docs.python.org/3/builtins/stdtypes.html#dictionary-view-objects +[dict.keys()]: https://docs.python.org/3/builtins/stdtypes.html#dict.keys [dict]: https://docs.python.org/3/tutorial/datastructures.html#dictionaries [generator-expressions]: https://www.pythonmorsels.com/how-write-generator-expression/ -[str.join]: https://docs.python.org/3/library/stdtypes.html#str.join - +[str.join]: https://docs.python.org/3/builtins/stdtypes.html#str.join diff --git a/exercises/practice/raindrops/.approaches/functools-reduce/content.md b/exercises/practice/raindrops/.approaches/functools-reduce/content.md index 36dd2222fd7..2e7b06f534f 100644 --- a/exercises/practice/raindrops/.approaches/functools-reduce/content.md +++ b/exercises/practice/raindrops/.approaches/functools-reduce/content.md @@ -1,12 +1,12 @@ -# Use functools.reduce() +# Use `functools.reduce()` ```python from functools import reduce def convert(number): - sounds = ('Pling','Plang','Plong') - factors = ((number % factor) == 0 for factor in (3,5,7)) #1 if no remainder, 0 if a remainder. + sounds = ('Pling', 'Plang', 'Plong') + factors = (number % factor == 0 for factor in (3, 5, 7)) # 1 if no remainder, 0 if a remainder. result = reduce(lambda sound, item : sound + (item[0] * item[1]), zip(sounds, factors), '') return result or str(number) @@ -20,16 +20,16 @@ The result is then 'reduced' to a combined result string with an empty string as If the result is empty, the number as a string is returned instead. This is an interesting use of `functools.reduce()`, but not necessarily the most readable way to solve this exercise. -Importing `functools` adds overhead and separating the sounds from the factors risks errors as the factor list expands. +Importing `functools` adds overhead and separating the sounds from the factors risks errors if the factor list is expanded. The syntax of `reduce()` also obfuscates string creation/concatenation. Unless the team maintaining this code is comfortable with `functools.reduce()`, this might be better re-written using `join()`: ```python def convert(number): - sounds = ('Pling','Plang','Plong') - factors = ((number % factor) == 0 for factor in (3,5,7)) - result = ''.join((item[0] * item[1]) for item in zip(sounds, factors)) + sounds = ('Pling', 'Plang', 'Plong') + factors = (number % factor == 0 for factor in (3, 5, 7)) + result = ''.join(item[0] * item[1] for item in zip(sounds, factors)) return result or str(number) ``` diff --git a/exercises/practice/raindrops/.approaches/functools-reduce/snippet.txt b/exercises/practice/raindrops/.approaches/functools-reduce/snippet.txt index d3b48650131..855b5a2d3df 100644 --- a/exercises/practice/raindrops/.approaches/functools-reduce/snippet.txt +++ b/exercises/practice/raindrops/.approaches/functools-reduce/snippet.txt @@ -1,8 +1,8 @@ from functools import reduce def convert(number): - sounds = ('Pling','Plang','Plong') - factors = ((number % factor) == 0 for factor in (3,5,7)) + sounds = ('Pling', 'Plang', 'Plong') + factors = (number % factor == 0 for factor in (3, 5, 7)) result = reduce(lambda sound, item : sound + (item[0] * item[1]), zip(sounds, factors), '') - return result or str(number) \ No newline at end of file + return result or str(number) diff --git a/exercises/practice/raindrops/.approaches/if-statements/content.md b/exercises/practice/raindrops/.approaches/if-statements/content.md index 80fe84bd926..4d137f60645 100644 --- a/exercises/practice/raindrops/.approaches/if-statements/content.md +++ b/exercises/practice/raindrops/.approaches/if-statements/content.md @@ -12,20 +12,23 @@ def convert(num): ``` -This approach is the most straightforward or 'naive' - it replicates in code what the instructions say, using `if` statements to check the modulo for each factor. -If the number is evenly divisible by the factor (modulo == 0), the corresponding string is concatenated to _sounds_ via the `+` operator. +This approach is the most straightforward or 'naive' - it replicates in code what the instructions say, using `if` statements to check the [modulo][modulo] for each factor. +If the number is evenly divisible by the factor (`modulo == 0`), the corresponding string is concatenated to `sounds` via the `+` operator. Sounds is returned if it is not empty (_see [Truth Value Testing][truth-value-testing] for more info_). Otherwise, a `str` version of the input number is returned. -This, of course incurs the 'penalty' of string concatenation. +This, of course, incurs the 'penalty' of string concatenation. But since there are only three factors to check and the strings are small, the concatenation is at a minimum. In fact, this solution - and most others described in the approaches here - are `O(1)` time complexity. There are a constant number of factors to iterate through, and the work that is done never increases, even as the input numbers get bigger. This holds true for space complexity as well. + +## Variation 1 + The compact form for the `if` statements might be harder to read for some people. -These can be re-written to be nested, and the return can be re-written to use a ternary expression: +These can be re-written to be nested, and the return can be re-written to use a [ternary expression][conditional-expression] (also called a conditional expression): ```python def convert(num): @@ -41,7 +44,10 @@ def convert(num): return sounds if sounds else str(num) ``` -While this solution is nicely readable and to-the-point, it will grow in length and get harder to read if many more factors are added or business logic changes. +While this approach is nicely readable and to-the-point, it will grow in length and get harder to read if many more factors are added or business logic changes. Other solutions using data structures to hold factors might be a better option in 'high change' situations. + +[conditional-expression]: https://docs.python.org/3/reference/expressions.html#conditional-expressions +[modulo]: https://www.freecodecamp.org/news/the-python-modulo-operator-what-does-the-symbol-mean-in-python-solved/ [truth-value-testing]: https://docs.python.org/3/library/stdtypes.html#truth-value-testing diff --git a/exercises/practice/raindrops/.approaches/introduction.md b/exercises/practice/raindrops/.approaches/introduction.md index 50e83d706ee..5da71e92afe 100644 --- a/exercises/practice/raindrops/.approaches/introduction.md +++ b/exercises/practice/raindrops/.approaches/introduction.md @@ -5,27 +5,27 @@ The goal of the Raindrops exercise is to convert a number to a string of "raindr is divisible by any combination of 3, 5, or 7. Numbers not divisible by any of these factors are returned as a string. -There are many different ways to solve this exercise in Python, with many many variations. +There are many different ways to solve this exercise in Python, with many variations. Some strategies optimize for simplicity, others for efficiency or extendability. Others explore interesting features of Python. Here, we go over 7 approaches: -1. Using `if` statements and `+` to assemble a return string. -2. Exploiting "Truthy" and "Falsy" values in ternary checks and an `f-string` -3. Using one or more sequences, a `loop`, and an `f-string`. -4. Using one or more sequences and a `generator-expression` within `join()`. -5. Using a `dict` of `{factors : sounds}` for lookup and a `generator-expression` within `join()`. -6. Using `itertools.compress()` and a `generator expression` within `join()`. -7. Using `functools.reduce()` and `join()`. +1. Using `if` statements and `+` to assemble a return string. +2. Exploiting "Truthy" and "Falsy" values in ternary checks and an `f-string`. +3. Using one or more sequences, a `loop`, and an `f-string`. +4. Using one or more sequences and a `generator-expression` within `join()`. +5. Using a `dict` of `{factors : sounds}` for lookup and a `generator-expression` within `join()`. +6. Using `itertools.compress()` and a `generator expression` within `join()`. +7. Using `functools.reduce()` and `join()`. ## General Guidance -The goal of the Raindrops exercise is to return a string that represents which factors (`3`, `5` or `7`) evenly divide the input number. -Each factor has a _sound_ assigned ('Pling' (3), 'Plang' (5), or 'Plong'(7)). +The goal of the Raindrops exercise is to return a string that represents which factors (`3`, `5`, or `7`) evenly divide the input number. +Each factor has a _sound_ assigned ('Pling' (3), 'Plang' (5), or 'Plong' (7)). Determining which factors divide into the input number without a remainder can most easily be accomplished via the [modulo][modulo] `%` operator. @@ -35,7 +35,7 @@ Using [`math.remainder()`][remainder] is **not** recommended for this exercise. The challenge is to efficiently assemble a results string while also keeping in mind how the code might be maintained or extended. -How might you add additional factors without a complete rewrite of code? +How might you add additional factors without a complete rewrite of the code? How can you keep the code concise? Do you use string concatenation, or are there other options you can choose? What tradeoffs will you make between readability, space, and speed of processing? @@ -71,7 +71,7 @@ def convert(number): return f'{threes}{fives}{sevens}' or str(number) - ###OR### +###OR### def convert(number): threes = 'Pling' if not number % 3 else '' # Sound if there is NOT a remainder @@ -82,19 +82,19 @@ def convert(number): ``` This approach uses [ternary expressions][ternary expression] (_also known as 'conditional expressions'_) to build strings for each factor. -The result strings are combined via an `f-string`, avoiding the use of `join()`, or a `loop`. +The result strings are combined via an `f-string`, avoiding the use of `join()` or a `loop`. If the `f-string` is empty _(evaluating to False in a [boolean context][truth-value-testing]_), a `str` of the input number is returned instead. -For more information, see the [Truthy and Falsy with f-string][approach-truthy-and-falsey-with-fstring] approach. +For more information, see the [Truthy and Falsy with `f-string`s][approach-truthy-and-falsey-with-fstring] approach. -## Approach: Using a `loop`, and an `f-string` +## Approach: Using a `loop` and an `f-string` ```python def convert(number): sounds = '' - drops = ("i", 3), ("a", 5), ("o", 7) + drops = ('i', 3), ('a', 5), ('o', 7) for vowel, factor in drops: if number % factor == 0: @@ -105,33 +105,33 @@ def convert(number): This approach loops through the _drops_ `tuple` (_although any iterable sequence can be used_), unpacking each `vowel` and `factor`. If the input number is evenly divisible by the factor (_modulus == 0_), the corresponding vowel is inserted into the `f-string` for that factor. -The `f-string` is then concatenated to _sounds_ string via `+`. - _Sounds_ is returned if it is not empty. - Otherwise, a string version of the input number is returned. +The `f-string` is then concatenated to the `sounds` string via `+`. +`sounds` is returned if it is not empty. +Otherwise, a string version of the input number is returned. -For more details, see the [loop and f-string][approach-loop-and-fstring] approach. +For more details, see the [loop and `f-string`][approach-loop-and-fstring] approach. ## Approach: Using Sequence(s) with `join()` ```python def convert(number): - drops = ["Pling","Plang","Plong"] - factors = [3,5,7] + drops = ['Pling', 'Plang', 'Plong'] + factors = [3, 5, 7] sounds = ''.join(drops[index] for index, factor in - enumerate(factors) if (number % factor == 0)) + enumerate(factors) if number % factor == 0) return sounds or str(number) ``` -This approach is very similar to the [loop and f-string][approach-loop-and-fstring] approach, but uses two `lists` to hold factors and sounds. +This approach is very similar to the [loop and `f-string`][approach-loop-and-fstring] approach, but uses two `lists` to hold factors and sounds. It also converts the loop that calculates the remainders and sounds into a [`generator expression`][generator-expression] within `join()`, which assembles the 'sounds' string. -_Sounds_ is returned if it is not empty. + `sounds` is returned if it is not empty. Otherwise, a `str` version of the input number is returned. -For more information, see the [tuple with join][approach-sequence-with-join] approach. +For more information, see the [sequence with join][approach-sequence-with-join] approach. ## Approach: Using a `dict` and `join()` @@ -150,27 +150,27 @@ def convert(number): return results or str(number) ``` -This approach uses a dictionary to hold the factor -> sound mappings and a `generator-expression` within `join()` to assemble results. -If 'results' is empty, a string version of the input number is returned. +This approach uses a dictionary to hold the factor to sound mappings and a [`generator expression`][generator-expression] within `join()` to assemble results. +If `results` is empty, a string version of the input number is returned. -For more details, read the [`dict` and `join()`][approach-dict-and-join] approach. +For more details, read the [`dict` and `join()`][approach-dict-and-join] approach. -## Approach: Using `itertools.compress` and a `list` Mask +## Approach: Using `itertools.compress` and a `list` Mask ```python from itertools import compress def convert(number): - sounds =('Pling','Plang','Plong') - mask = ((number % factor) == 0 for factor in (3,5,7)) + sounds = ('Pling', 'Plang', 'Plong') + mask = (number % factor == 0 for factor in (3, 5, 7)) return ''.join(compress(sounds, mask)) or str(number) ``` This approach uses [`itertools.compress`][itertools-compress] to filter a list of sounds using a mask of `True` and `False` values. -The mask is formed by calculating `bool((input % factor) == 0)` for each factor (_which will return True or False_). +The mask is formed by calculating `bool(input % factor == 0)` for each factor (_which will return True or False_). If the result of `itertools.compress` is empty, a string version of the input number is returned instead. For more details, see the [itertools.compress][approach-itertools-compress] approach. @@ -183,8 +183,8 @@ For more details, see the [itertools.compress][approach-itertools-compress] appr from functools import reduce def convert(number): - sounds = ('Pling','Plang','Plong') - factors = ((number % factor) == 0 for factor in (3,5,7)) + sounds = ('Pling', 'Plang', 'Plong') + factors = (number % factor == 0 for factor in (3, 5, 7)) result = reduce(lambda sound, item : sound + (item[0] * item[1]), zip(sounds, factors), '') return result or str(number) @@ -202,13 +202,13 @@ For more information, read the [functools.reduce][approach-functools-reduce] app Besides these seven approaches, there are a multitude of possible variations using different data structures and joining methods. One can also use the new [structural pattern matching][structural-pattern-matching], although it is both more verbose and harder to read. -It (unnecessarily) lists out all of the mask variations from the [itertools compress][itertools-compress] approach and would be hard to extend without the potential of making a mistake with the factors, the sounds, or the masks: +It (unnecessarily) lists out all of the mask variations from the [itertools compress approach][approach-itertools-compress] and would be hard to extend without the potential of making a mistake with the factors, the sounds, or the masks: ```python def convert(number): - match [(number % factor) == 0 for factor in (3,5,7)]: + match [number % factor == 0 for factor in (3, 5, 7)]: case [True, True, True]: return 'PlingPlangPlong' case [True, True, False]: @@ -230,11 +230,12 @@ def convert(number): ## Which Approach to Use? -All approaches are idiomatic, and show multiple paradigms and possibilities. +All 7 approaches are idiomatic, and show multiple paradigms and possibilities. Some additional considerations include readability and maintainability. Approaches using separate data structures to hold sounds/factors are very helpful when additional data needs to be added, even if there is memory or performance overhead associated with them. No one wants to add to an ever-growing block of `if-statements`, or reap the consequences of troubleshooting a typo in some strange embedded set of parenthesis. + Approaches using `join()` or `loops` are fairly succinct, and might be more easily understood by others reading your code, so that they can adjust or add to the logic. Additionally, using an `f-string` can cut down on visual "noise" as you assemble the return string. @@ -242,7 +243,7 @@ So an approach that balances maintenance needs with succinctness is likely the b ```python def convert(number): - #This is clear and easily added to. Unless the factors get + # This is clear and easily added to. Unless the factors get # really long, this won't take up too much memory. sounds = {3: 'Pling', 5: 'Plang', @@ -259,12 +260,12 @@ def convert(number): This separates the code that calculates the results string from the factors themselves. If a factor needs to be added, only the dictionary needs to be touched. This code need only iterate over the items of the dictionary to do its calculation, making this `O(1)` in time complexity. -This does take `O(m)` space, where `m` is equal to the number of factor entries. +This does take `O(n)` space, where `n` is equal to the number of factor entries. Since the number of factors is fixed here, this is unlikely to create issues unless a great many more are added to the 'sounds' `dict`. To compare the performance of this and the other approaches, take a look at the [Performance article][article-performance]. -[approach-dict-and-join ]: https://exercism.org/tracks/python/exercises/raindrops/approaches/dict-and-join +[approach-dict-and-join]: https://exercism.org/tracks/python/exercises/raindrops/approaches/dict-and-join [approach-functools-reduce]: https://exercism.org/tracks/python/exercises/raindrops/approaches/functools-reduce [approach-if-statements]: https://exercism.org/tracks/python/exercises/raindrops/approaches/if-statements [approach-itertools-compress]: https://exercism.org/tracks/python/exercises/raindrops/approaches/itertools-compress @@ -283,4 +284,4 @@ To compare the performance of this and the other approaches, take a look at the [remainder]: https://docs.python.org/3/library/math.html#math.remainder [ternary expression]: https://docs.python.org/3/reference/expressions.html#conditional-expressions [truth-value-testing]: https://docs.python.org/3/library/stdtypes.html#truth-value-testing -[structural-pattern-matching]: https://peps.python.org/pep-0622/ +[structural-pattern-matching]: https://docs.python.org/3/reference/compound_stmts.html#match diff --git a/exercises/practice/raindrops/.approaches/itertools-compress/content.md b/exercises/practice/raindrops/.approaches/itertools-compress/content.md index 42e02c188e0..7da4f5e6cc9 100644 --- a/exercises/practice/raindrops/.approaches/itertools-compress/content.md +++ b/exercises/practice/raindrops/.approaches/itertools-compress/content.md @@ -5,20 +5,20 @@ from itertools import compress def convert(number): - sounds ='Pling','Plang','Plong' - mask = ((number % factor) == 0 for factor in (3,5,7)) + sounds = ('Pling', 'Plang', 'Plong') + mask = (number % factor == 0 for factor in (3, 5, 7)) return ''.join(compress(sounds, mask)) or str(number) ``` This approach uses both a `tuple` for storing sound strings and mask to filter them with. -For each factor, the `generator-expression` calculates `True` (_evenly divisible_) or `False` (_remainder left over_). +For each factor, the [`generator expression`][generator-expression] calculates `True` (_evenly divisible_) or `False` (_remainder left over_). This mask is used with [`itertools.compress()`][compress] to 'mask over' any unwanted values in 'sounds'. Finally, the returned string is created with [`str.join()`][join]. If the 'sounds' string is empty, a string version of the number is returned instead. This is very succinct code that avoids string concatenation. However, it does require the overhead of importing `compress()` from the [itertools][itertools] module. -The code is also harder to maintain should there be additional factors/sounds needed. +The code is also harder to maintain should additional factors/sounds be needed. Because the factors and sounds are separated, there is a chance mistakes could be made like forgetting a number or swapping which factor is paired with which sound. A better approach for maintenance might be to turn the 'sounds' `tuple` into a dictionary where the factors and sounds can be stored separate from the logic that does the calculations and string creation: @@ -31,18 +31,19 @@ def convert(number): 5 : 'Plang', 7 : 'Plong' } - mask = ((number % factor) == 0 for factor in sounds.keys()) + mask = (number % factor == 0 for factor in sounds.keys()) return ''.join(compress(sounds.values(), mask)) or str(number) ``` -In this rewrite, factors are keys with the sounds as values in a dictionary. +In this variation, factors are keys and the sounds are values in a dictionary. The mask then uses a [`dict.keys()`][dict-keys] [view][view objects] to iterate over the factors and calculate the `True`/`False` values. This mask is used with `compress()` to filter a `dict.values()` view that is used by `join()` to construct the return string. If the string is empty, a string version of the input number is returned instead. [compress]: https://docs.python.org/3/library/itertools.html#itertools.compress [dict-keys]: https://docs.python.org/3/library/stdtypes.html#dict.keys +[generator-expression]: https://peps.python.org/pep-0289/ [itertools]: https://docs.python.org/3/library/itertools.html [join]: https://docs.python.org/3/library/stdtypes.html#str.join [view objects]: https://docs.python.org/3/library/stdtypes.html#dictionary-view-objects diff --git a/exercises/practice/raindrops/.approaches/itertools-compress/snippet.txt b/exercises/practice/raindrops/.approaches/itertools-compress/snippet.txt index 5a58c913f95..b28116c7d75 100644 --- a/exercises/practice/raindrops/.approaches/itertools-compress/snippet.txt +++ b/exercises/practice/raindrops/.approaches/itertools-compress/snippet.txt @@ -1,6 +1,6 @@ from itertools import compress def convert(number): - sounds ='Pling','Plang','Plong' - mask = ((number % factor) == 0 for factor in (3,5,7)) - return ''.join(compress(sounds, mask)) or str(number) \ No newline at end of file + sounds = ('Pling', 'Plang', 'Plong') + mask = (number % factor == 0 for factor in (3, 5, 7)) + return ''.join(compress(sounds, mask)) or str(number) diff --git a/exercises/practice/raindrops/.approaches/loop-and-fstring/content.md b/exercises/practice/raindrops/.approaches/loop-and-fstring/content.md index 41d4ce4951a..26963273113 100644 --- a/exercises/practice/raindrops/.approaches/loop-and-fstring/content.md +++ b/exercises/practice/raindrops/.approaches/loop-and-fstring/content.md @@ -1,10 +1,10 @@ -# Sequence(s) with a Loop and f-string +# Sequence(s) with a Loop and `f-string` ```python def convert(number): sounds = '' - drops = ("i", 3), ("a", 5), ("o", 7) + drops = ('i', 3), ('a', 5), ('o', 7) for vowel, factor in drops: if number % factor == 0: @@ -15,10 +15,10 @@ def convert(number): This approach loops through the _drops_ `tuple` (_although any iterable sequence(s) can be used_), unpacking each `vowel` and `factor`. -If the input number is evenly divisible by the factor (_modulus == 0_), the corresponding vowel is inserted into the `f-string` for that factor. -The `f-string` is then concatenated to _sounds_ string via `+`. - _Sounds_ is returned if it is not empty. - Otherwise, a string version of the input number is returned. +If the input number is evenly divisible by the factor (_modulus == 0_), the corresponding vowel is inserted into the [`f-string`][f-strings] for that factor. +The `f-string` is then concatenated to the `sounds` string via `+`. +`sounds` is returned if it is not empty. +Otherwise, a string version of the input number is returned. This takes `O(1)` time and `O(1)` space. @@ -26,6 +26,9 @@ It is a very efficient and clever way of building up the return string, since on However, it might take a moment for others reading the code to understand what exactly is going on. It also (may) create maintenance difficulties should there be future factors and sounds that do not conform to the pattern of only changing the vowel in the sound. + +## Variation 1 + A much less exciting (_but perhaps easier to maintain_) rewrite would be to store the whole drop sound and build up the return string out of whole drops: @@ -35,10 +38,13 @@ def convert(number): output = '' for factor, sound in sounds: - if number % factor == 0: - output += sound + if number % factor == 0: + output += sound return output or str(number) ``` -This has the same time and space complexity as the first variation. +This has the same time and space complexity as the previous variation. + + +[f-strings]: https://docs.python.org/3/reference/lexical_analysis.html#f-strings diff --git a/exercises/practice/raindrops/.approaches/loop-and-fstring/snippet.txt b/exercises/practice/raindrops/.approaches/loop-and-fstring/snippet.txt index a97a7f70b53..2f25acc9a6f 100644 --- a/exercises/practice/raindrops/.approaches/loop-and-fstring/snippet.txt +++ b/exercises/practice/raindrops/.approaches/loop-and-fstring/snippet.txt @@ -1,8 +1,8 @@ def convert(number): sounds = '' - drops = ("i", 3), ("a", 5), ("o", 7) + drops = ('i', 3), ('a', 5), ('o', 7) for vowel, factor in drops: if number % factor == 0: sounds += f'Pl{vowel}ng' - return sounds or str(number) \ No newline at end of file + return sounds or str(number) diff --git a/exercises/practice/raindrops/.approaches/sequence-with-join/content.md b/exercises/practice/raindrops/.approaches/sequence-with-join/content.md index 6895052a61b..4ac60db7f97 100644 --- a/exercises/practice/raindrops/.approaches/sequence-with-join/content.md +++ b/exercises/practice/raindrops/.approaches/sequence-with-join/content.md @@ -2,36 +2,40 @@ ```python def convert(number): - drops = ["Pling","Plang","Plong"] - factors = [3,5,7] + drops = ['Pling', 'Plang', 'Plong'] + factors = [3, 5, 7] sounds = ''.join(drops[index] for index, factor in - enumerate(factors) if (number % factor == 0)) + enumerate(factors) if number % factor == 0) return sounds or str(number) ``` This approach is very similar to the [loop and f-string][approach-loop-and-fstring] approach, but uses two `lists` to hold factors and sounds and [`enumerate()`][enumerate] to extract an index. - It also converts the loop that calculates the remainders and sounds into a [`generator expression`][generator-expression] within `join()`. -_Sounds_ is returned if it is not empty. + It also converts the loop that calculates the remainders and sounds into a [`generator expression`][generator-expression] within [`join()`][str-join]. + `sounds` is returned if it is not empty. Otherwise, a `str` version of the input number is returned. - This, like most approaches described here is `O(1)` time and space complexity. + This, like most approaches described here, is `O(1)` time and space complexity. In benchmark timings, the `generator-expression` and multiple variables add a bit of overhead. - Readability here might not be the easiest for those not used to reading generator expressions inside calls to other functions, so if that is the case, this can be re-written as: +## Variation 1 - ```python +The above solution might not be very readable for those not used to reading generator expressions inside calls to other functions, so if that is the case, this can be re-written as: + + +```python def convert(number): - drops = ["Pling","Plang","Plong"] - factors = [3,5,7] + drops = ['Pling', 'Plang', 'Plong'] + factors = [3, 5, 7] sounds = (drops[index] for index, factor in - enumerate(factors) if (number % factor == 0)) + enumerate(factors) if number % factor == 0) return ''.join(sounds) or str(number) ``` [approach-loop-and-fstring]: https://exercism.org/tracks/python/exercises/raindrops/approaches/loop-and-fstring +[enumerate]: https://docs.python.org/3/builtins/functions.html#enumerate [generator-expression]: https://peps.python.org/pep-0289/ -[enumerate]: https://docs.python.org/3/library/functions.html#enumerate +[str-join]: https://docs.python.org/3/builtins/stdtypes.html#str.join diff --git a/exercises/practice/raindrops/.approaches/sequence-with-join/snippet.txt b/exercises/practice/raindrops/.approaches/sequence-with-join/snippet.txt index 2df433229e5..6c923f1fb62 100644 --- a/exercises/practice/raindrops/.approaches/sequence-with-join/snippet.txt +++ b/exercises/practice/raindrops/.approaches/sequence-with-join/snippet.txt @@ -1,8 +1,8 @@ def convert(number): - drops = ["Pling","Plang","Plong"] - factors = [3,5,7] + drops = ['Pling', 'Plang', 'Plong'] + factors = [3, 5, 7] sounds = ''.join(drops[index] for index, factor in - enumerate(factors) if (number % factor == 0)) + enumerate(factors) if number % factor == 0) - return sounds or str(number) \ No newline at end of file + return sounds or str(number) diff --git a/exercises/practice/raindrops/.approaches/truthy-and-falsey-with-fstring/content.md b/exercises/practice/raindrops/.approaches/truthy-and-falsey-with-fstring/content.md index 816052c3b12..61223aa6963 100644 --- a/exercises/practice/raindrops/.approaches/truthy-and-falsey-with-fstring/content.md +++ b/exercises/practice/raindrops/.approaches/truthy-and-falsey-with-fstring/content.md @@ -1,15 +1,15 @@ -# Truthy and Falsey Values with f-strings +# Truthy and Falsey Values with `f-string`s ```python def convert(number): - threes = '' if number % 3 else 'Pling' # Empty string if modulo == 1 (0 evaluates to False) + threes = '' if number % 3 else 'Pling' # Empty string if modulo > 0 (0 evaluates to False) fives = '' if number % 5 else 'Plang' sevens = '' if number % 7 else 'Plong' return f'{threes}{fives}{sevens}' or str(number) - #OR# +#OR# def convert(number): threes = 'Pling' if not number % 3 else '' # Sound if NOT modulo == 0 @@ -20,20 +20,21 @@ def convert(number): ``` This is very similar to the [`if-statement`][approach-if-statements] approach logic, but uses [ternary expressions][ternary expression] to assign either an empty string or a drop sound to a variable. -The variables are then used in an `f-string` to compose the result, avoiding the use of `join()`, or a `loop`. +The variables are then used in an [`f-string`][f-strings] to compose the result, avoiding the use of `join()` or a `loop`. If the `f-string` is empty _(evaluating to False in a [boolean context][truth-value-testing]_), a `str` of the input number is returned instead. This has `O(1)` time and space complexity. These two variations both exploit the fact that boolean `True` and `False` are a subtype of `int` in Python. 0 evaluates to `False`, and 1 to `True`. -So the expression `'Pling' if not number % 3 else ''` can be read as "return 'Pling" if number % 3 not False" where `False` is 0, and (not `False`) is 1. -The expression `'' if number % 3 else 'Pling'` is the inverse: "return '' if number % 3 is True" - where number % 3 > 0 is `True`, and number % 3 == 0 is `False`. +So the expression `'Pling' if not number % 3 else ''` can be read as "return `'Pling'` if number % 3 is not False" where `False` is 0, and (not `False`) is 1. +The expression `'' if number % 3 else 'Pling'` is the inverse: "return `''` if number % 3 is True" - where number % 3 > 0 is `True`, and number % 3 == 0 is `False`. Like the `if-statement` approach, these solutions are nicely readable and to-the-point, but will grow in length and get harder to read if many more factors are added or business logic changes. The `f-string` in particular could get unwieldy beyond about 5 factors. Other solutions using data structures to hold factors and `join()` to assemble strings might be a better option in 'high change' situations. [approach-if-statements]: https://exercism.org/tracks/python/exercises/raindrops/approaches/if-statements +[f-strings]: https://docs.python.org/3/reference/lexical_analysis.html#f-strings [ternary expression]: https://docs.python.org/3/reference/expressions.html#conditional-expressions [truth-value-testing]: https://docs.python.org/3/library/stdtypes.html#truth-value-testing diff --git a/exercises/practice/raindrops/.articles/config.json b/exercises/practice/raindrops/.articles/config.json index c85ae72079a..7f4d34dbd84 100644 --- a/exercises/practice/raindrops/.articles/config.json +++ b/exercises/practice/raindrops/.articles/config.json @@ -5,7 +5,8 @@ "slug": "performance", "title": "Performance deep dive", "blurb": "Deep dive to find out the most performant approach to the Raindrops exercise.", - "authors": ["BethanyG", "colinleach"] + "authors": ["BethanyG", "colinleach"], + "contributors": ["yrahcaz7"] } ] -} \ No newline at end of file +} diff --git a/exercises/practice/raindrops/.articles/performance/content.md b/exercises/practice/raindrops/.articles/performance/content.md index 82ade02c90f..9751c145145 100644 --- a/exercises/practice/raindrops/.articles/performance/content.md +++ b/exercises/practice/raindrops/.articles/performance/content.md @@ -4,16 +4,16 @@ In this article, we will find out how to most efficiently complete the Raindrops The [introduction page][approaches-intro] lists seven idiomatic approaches to this exercise: -1. Using [`if` statements and `+`][approach-if-statements] to assemble a return string -2. Exploiting ["Truthy" and "Falsy" values in ternary checks][approach-truthy-and-falsey-with-fstring] and an `f-string` -3. Using one or more sequences, a [`loop`, and an `f-string`][approach-loop-and-fstring] -4. Using one or more [sequences and a `generator-expression` within `join()`][approach-sequence-with-join]. -5. Using a [`dict` of `{factors : sounds}` for lookup and `join()`][approach-dict-and-join] -6. Using [`itertools.compress()`][approach-itertools-compress] -7. Using [`functools.reduce()`][approach-functools-reduce] +1. Using [`if` statements and `+`][approach-if-statements] to assemble a return string. +2. Exploiting ["Truthy" and "Falsy" values in ternary checks][approach-truthy-and-falsey-with-fstring] and an `f-string`. +3. Using one or more sequences, a [`loop`, and an `f-string`][approach-loop-and-fstring]. +4. Using one or more [sequences and a `generator-expression` within `join()`][approach-sequence-with-join]. +5. Using a [`dict` of `{factors : sounds}` for lookup and `join()`][approach-dict-and-join]. +6. Using [`itertools.compress()`][approach-itertools-compress]. +7. Using [`functools.reduce()`][approach-functools-reduce]. -For our performance investigation, we'll also include an 8th approach that [uses Python 3.10's `structural pattern matching`][PEP0622]. +For our performance investigation, we'll also include an 8th approach that [uses Python 3.10's `structural pattern matching`][structural-pattern-matching]. ## Benchmarks @@ -21,17 +21,17 @@ For our performance investigation, we'll also include an 8th approach that [uses To benchmark these functions, we wrote a small [benchmarking script][benchmark-application] using the [timeit][timeit] module along with third-party libraries [numpy][numpy] and [pandas][pandas]. -| | 10 | 14 | 15 | 70 | 105 | 182 | 189 | 203 | 204 | 399 | 409 | 525 | 735 | 1575 | 3250 | -|----------------------------- |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: | -| if statements | 2.15e-07 | 2.13e-07 | 2.63e-07 | 2.62e-07 | 3.03e-07 | 2.13e-07 | 2.69e-07 | 2.12e-07 | 2.12e-07 | 2.75e-07 | 2.64e-07 | 3.04e-07 | 3.04e-07 | 3.06e-07 | 2.20e-07 | -| ternary with truthy/falsy | 2.79e-07 | 2.80e-07 | 2.89e-07 | 2.85e-07 | 3.02e-07 | 2.80e-07 | 3.13e-07 | 2.81e-07 | 2.79e-07 | 2.82e-07 | 3.30e-07 | 3.02e-07 | 3.02e-07 | 3.02e-07 | 2.80e-07 | -| loop with tuple | 7.91e-07 | 4.08e-07 | 5.07e-07 | 5.14e-07 | 6.13e-07 | 7.95e-07 | 5.10e-07 | 2.01e-07 | 8.18e-07 | 5.06e-07 | 3.85e-07 | 6.25e-07 | 6.10e-07 | 6.10e-07 | 2.00e-07 | -| structural pattern matching | 7.91e-07 | 7.39e-07 | 5.98e-07 | 6.24e-07 | 5.40e-07 | 7.47e-07 | 6.73e-07 | 7.72e-07 | 7.15e-07 | 6.69e-07 | 8.80e-07 | 5.43e-07 | 5.38e-07 | 5.69e-07 | 8.04e-07 | -| dictionary with join | 8.04e-07 | 7.98e-07 | 8.95e-07 | 9.81e-07 | 4.07e-07 | 8.15e-07 | 9.05e-07 | 8.14e-07 | 8.18e-07 | 9.05e-07 | 8.94e-07 | 9.51e-07 | 4.05e-07 | 9.80e-07 | 8.17e-07 | -| dictionary recommended | 8.55e-07 | 8.13e-07 | 8.99e-07 | 8.96e-07 | 9.36e-07 | 8.12e-07 | 9.27e-07 | 8.18e-07 | 8.24e-07 | 9.07e-07 | 9.13e-07 | 9.40e-07 | 9.36e-07 | 9.30e-07 | 8.15e-07 | -| sequence with  join | 8.59e-07 | 8.67e-07 | 9.56e-07 | 9.64e-07 | 4.04e-07 | 8.78e-07 | 9.65e-07 | 8.71e-07 | 8.74e-07 | 9.68e-07 | 9.27e-07 | 1.00e-06 | 1.02e-06 | 4.07e-07 | 8.59e-07 | -| itertools with join | 9.90e-07 | 9.82e-07 | 1.05e-06 | 1.03e-06 | 1.07e-06 | 9.85e-07 | 1.06e-06 | 9.83e-07 | 9.82e-07 | 1.04e-06 | 1.11e-06 | 1.07e-06 | 1.07e-06 | 1.04e-06 | 4.07e-07 | -| functools reduce | 1.42e-06 | 1.56e-06 | 1.45e-06 | 1.43e-06 | 1.50e-06 | 1.41e-06 | 1.46e-06 | 1.44e-06 | 1.41e-06 | 1.47e-06 | 1.49e-06 | 1.52e-06 | 1.49e-06 | 1.51e-06 | 1.43e-06 | +| | 10 | 14 | 15 | 70 | 105 | 182 | 189 | 203 | 204 | 399 | 409 | 525 | 735 | 1575 | 3250 | +|---------------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| +| if statements | 2.15e-07 | 2.13e-07 | 2.63e-07 | 2.62e-07 | 3.03e-07 | 2.13e-07 | 2.69e-07 | 2.12e-07 | 2.12e-07 | 2.75e-07 | 2.64e-07 | 3.04e-07 | 3.04e-07 | 3.06e-07 | 2.20e-07 | +| ternary with truthy/falsy | 2.79e-07 | 2.80e-07 | 2.89e-07 | 2.85e-07 | 3.02e-07 | 2.80e-07 | 3.13e-07 | 2.81e-07 | 2.79e-07 | 2.82e-07 | 3.30e-07 | 3.02e-07 | 3.02e-07 | 3.02e-07 | 2.80e-07 | +| loop with tuple | 7.91e-07 | 4.08e-07 | 5.07e-07 | 5.14e-07 | 6.13e-07 | 7.95e-07 | 5.10e-07 | 2.01e-07 | 8.18e-07 | 5.06e-07 | 3.85e-07 | 6.25e-07 | 6.10e-07 | 6.10e-07 | 2.00e-07 | +| structural pattern matching | 7.91e-07 | 7.39e-07 | 5.98e-07 | 6.24e-07 | 5.40e-07 | 7.47e-07 | 6.73e-07 | 7.72e-07 | 7.15e-07 | 6.69e-07 | 8.80e-07 | 5.43e-07 | 5.38e-07 | 5.69e-07 | 8.04e-07 | +| dictionary with join | 8.04e-07 | 7.98e-07 | 8.95e-07 | 9.81e-07 | 4.07e-07 | 8.15e-07 | 9.05e-07 | 8.14e-07 | 8.18e-07 | 9.05e-07 | 8.94e-07 | 9.51e-07 | 4.05e-07 | 9.80e-07 | 8.17e-07 | +| dictionary recommended | 8.55e-07 | 8.13e-07 | 8.99e-07 | 8.96e-07 | 9.36e-07 | 8.12e-07 | 9.27e-07 | 8.18e-07 | 8.24e-07 | 9.07e-07 | 9.13e-07 | 9.40e-07 | 9.36e-07 | 9.30e-07 | 8.15e-07 | +| sequence with join | 8.59e-07 | 8.67e-07 | 9.56e-07 | 9.64e-07 | 4.04e-07 | 8.78e-07 | 9.65e-07 | 8.71e-07 | 8.74e-07 | 9.68e-07 | 9.27e-07 | 1.00e-06 | 1.02e-06 | 4.07e-07 | 8.59e-07 | +| itertools with join | 9.90e-07 | 9.82e-07 | 1.05e-06 | 1.03e-06 | 1.07e-06 | 9.85e-07 | 1.06e-06 | 9.83e-07 | 9.82e-07 | 1.04e-06 | 1.11e-06 | 1.07e-06 | 1.07e-06 | 1.04e-06 | 4.07e-07 | +| functools reduce | 1.42e-06 | 1.56e-06 | 1.45e-06 | 1.43e-06 | 1.50e-06 | 1.41e-06 | 1.46e-06 | 1.44e-06 | 1.41e-06 | 1.47e-06 | 1.49e-06 | 1.52e-06 | 1.49e-06 | 1.51e-06 | 1.43e-06 | Keep in mind that all these approaches are _very fast_, and that benchmarking at this granularity can be unstable. @@ -45,8 +45,7 @@ Time is reported in seconds taken per string after calculating the 'best of' tim The [`timeit`][timeit] module docs have more details, and [note.nkmk.me][note_nkmk_me] has a nice summary of methods. -[PEP0622]: https://peps.python.org/pep-0622/ -[approach-dict-and-join ]: https://exercism.org/tracks/python/exercises/raindrops/approaches/dict-and-join +[approach-dict-and-join]: https://exercism.org/tracks/python/exercises/raindrops/approaches/dict-and-join [approach-functools-reduce]: https://exercism.org/tracks/python/exercises/raindrops/approaches/functools-reduce [approach-if-statements]: https://exercism.org/tracks/python/exercises/raindrops/approaches/if-statements [approach-itertools-compress]: https://exercism.org/tracks/python/exercises/raindrops/approaches/itertools-compress @@ -55,6 +54,7 @@ The [`timeit`][timeit] module docs have more details, and [note.nkmk.me][note_nk [approach-truthy-and-falsey-with-fstring]: https://exercism.org/tracks/python/exercises/raindrops/approaches/truthy-and-falsey-with-fstring [approaches-intro]: https://exercism.org/tracks/python/exercises/raindrops/approaches/introduction.md [benchmark-application]: https://github.com/exercism/python/blob/main/exercises/practice/raindrops/.articles/performance/code/Benchmark.py +[structural-pattern-matching]: https://docs.python.org/3/reference/compound_stmts.html#match [note_nkmk_me]: https://note.nkmk.me/en/python-timeit-measure/ [numpy]: https://numpy.org/ [pandas]: https://pandas.pydata.org/ diff --git a/exercises/practice/raindrops/.articles/performance/snippet.md b/exercises/practice/raindrops/.articles/performance/snippet.md index d3449e8656e..fb9bb6efebf 100644 --- a/exercises/practice/raindrops/.articles/performance/snippet.md +++ b/exercises/practice/raindrops/.articles/performance/snippet.md @@ -1,8 +1,8 @@ -| | 10 | 14 | 15 | 70 | 105 | 182 | 189 | 203 | 204 | 399 | 409 | 525 | 735 | 1575 | 3250 | -|----------------------------- |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: | -| if statements | 2.12e-07 | 2.16e-07 | 2.60e-07 | 2.76e-07 | 2.98e-07 | 2.10e-07 | 2.59e-07 | 2.15e-07 | 2.10e-07 | 2.75e-07 | 2.61e-07 | 3.41e-07 | 2.99e-07 | 2.98e-07 | 2.13e-07 | -| loop with tuple | 4.01e-07 | 4.09e-07 | 5.05e-07 | 4.94e-07 | 6.48e-07 | 3.97e-07 | 5.25e-07 | 4.10e-07 | 2.04e-07 | 5.51e-07 | 4.06e-07 | 9.04e-07 | 6.16e-07 | 6.89e-07 | 4.33e-07 | -| structural pattern matching | 7.55e-07 | 7.31e-07 | 6.09e-07 | 5.87e-07 | 5.21e-07 | 7.11e-07 | 6.42e-07 | 7.19e-07 | 6.90e-07 | 6.49e-07 | 8.43e-07 | 5.00e-07 | 5.12e-07 | 5.21e-07 | 7.48e-07 | -| dictionary with join | 8.31e-07 | 8.18e-07 | 9.34e-07 | 1.02e-06 | 9.75e-07 | 8.55e-07 | 9.13e-07 | 8.25e-07 | 8.32e-07 | 2.28e-06 | 9.22e-07 | 1.05e-06 | 2.42e-06 | 9.94e-07 | 8.46e-07 | -| itertools with join | 9.46e-07 | 9.33e-07 | 4.04e-07 | 9.88e-07 | 1.01e-06 | 9.41e-07 | 9.91e-07 | 9.65e-07 | 9.80e-07 | 2.51e-06 | 1.10e-06 | 2.50e-06 | 1.02e-06 | 1.00e-06 | 9.60e-07 | -| functools reduce | 1.35e-06 | 1.38e-06 | 1.41e-06 | 1.39e-06 | 1.48e-06 | 1.33e-06 | 1.42e-06 | 1.37e-06 | 1.34e-06 | 1.39e-06 | 1.43e-06 | 1.45e-06 | 1.46e-06 | 1.45e-06 | 1.37e-06 | \ No newline at end of file +| | 10 | +|---------------------------------------|:--------:| +| if statements | 2.15e-07 | +| loop with tuple | 7.91e-07 | +| structural pattern matching | 7.91e-07 | +| dictionary with join | 8.04e-07 | +| itertools with join | 9.90e-07 | +| functools reduce | 1.42e-06 |