From 8cb4533dff88a86b3c16ead09a481ce5994399d5 Mon Sep 17 00:00:00 2001 From: Yrahcaz7 <74512479+Yrahcaz7@users.noreply.github.com> Date: Wed, 20 May 2026 23:02:42 -0400 Subject: [PATCH 1/9] `string-list-and-dict-methods` cleanup also some formatting fixes --- .../practice/wordy/.approaches/config.json | 37 ++++----- .../.approaches/functools-reduce/content.md | 24 +++--- .../import-callables-from-operator/content.md | 2 +- .../wordy/.approaches/introduction.md | 55 +++++++------ .../lambdas-in-a-dictionary/content.md | 16 ++-- .../wordy/.approaches/recursion/content.md | 42 +++++----- .../regex-with-operator-module/content.md | 10 +-- .../string-list-and-dict-methods/content.md | 81 ++++++++++--------- .../string-list-and-dict-methods/snippet.txt | 8 +- .../wordy/.docs/instructions.append.md | 6 +- 10 files changed, 143 insertions(+), 138 deletions(-) diff --git a/exercises/practice/wordy/.approaches/config.json b/exercises/practice/wordy/.approaches/config.json index 670284d4715..417458430a4 100644 --- a/exercises/practice/wordy/.approaches/config.json +++ b/exercises/practice/wordy/.approaches/config.json @@ -1,7 +1,7 @@ { "introduction": { "authors": ["BethanyG"], - "contributors": ["bobahop"] + "contributors": ["bobahop", "yrahcaz7"] }, "approaches": [ { @@ -9,44 +9,45 @@ "slug": "string-list-and-dict-methods", "title": "String, List, and Dictionary Methods", "blurb": "Use Core Python Features to Solve Word Problems.", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, - { - "uuid": "d3ff485a-defe-42d9-b9c6-c38019221ffa", + { + "uuid": "d3ff485a-defe-42d9-b9c6-c38019221ffa", "slug": "import-callables-from-operator", "title": "Import Callables from the Operator Module", "blurb": "Use Operator Module Methods to Solve Word Problems.", "authors": ["BethanyG"] - }, - { - "uuid": "61f44943-8a12-471b-ab15-d0d10fa4f72f", + }, + { + "uuid": "61f44943-8a12-471b-ab15-d0d10fa4f72f", "slug": "regex-with-operator-module", "title": "Regex with the Operator Module", "blurb": "Use Regex with the Callables from Operator to solve word problems.", "authors": ["BethanyG"] - }, - { - "uuid": "46bd15dd-cae4-4eb3-ac63-a8b631a508d1", + }, + { + "uuid": "46bd15dd-cae4-4eb3-ac63-a8b631a508d1", "slug": "lambdas-in-a-dictionary", "title": "Lambdas in a Dictionary to Return Functions", "blurb": "Use lambdas in a dictionary to return functions for solving word problems.", "authors": ["BethanyG"] - }, - { - "uuid": "2e643b88-9b76-45a1-98f4-b211919af061", + }, + { + "uuid": "2e643b88-9b76-45a1-98f4-b211919af061", "slug": "recursion", "title": "Recursion for Iteration.", "blurb": "Use recursion with other strategies to solve word problems.", "authors": ["BethanyG"] - }, - { - "uuid": "1e136304-959c-4ad1-bc4a-450d13e5f668", + }, + { + "uuid": "1e136304-959c-4ad1-bc4a-450d13e5f668", "slug": "functools-reduce", "title": "Functools.reduce for Calculation", "blurb": "Use functools.reduce with other strategies to calculate solutions.", "authors": ["BethanyG"] - }, - { + }, + { "uuid": "d643e2b4-daee-422d-b8d3-2cad2f439db5", "slug": "dunder-getattribute", "title": "dunder with __getattribute__", diff --git a/exercises/practice/wordy/.approaches/functools-reduce/content.md b/exercises/practice/wordy/.approaches/functools-reduce/content.md index 8bc42449fa0..b8f51710ffd 100644 --- a/exercises/practice/wordy/.approaches/functools-reduce/content.md +++ b/exercises/practice/wordy/.approaches/functools-reduce/content.md @@ -16,8 +16,8 @@ def answer(question): raise ValueError("unknown operation") # Using the built-in filter() to clean & split the question.. - question = list(filter(lambda x: - x not in ("What", "is", "by"), + question = list(filter(lambda x: + x not in ("What", "is", "by"), question.strip("?").split())) # Separate candidate operators and numbers into two lists. @@ -25,8 +25,8 @@ def answer(question): # Convert candidate elements to int(), checking for "-". # All other values are replaced with None. - digits = [int(element) if - (element.isdigit() or element[1:].isdigit()) + digits = [int(element) if + (element.isdigit() or element[1:].isdigit()) else None for element in question[::2]] # If there is a mis-match between operators and numbers, toss error. @@ -51,13 +51,13 @@ However, this could easily be accomplished by either using [chained][method-chai return (question.removeprefix("What is") .removesuffix("?") .replace("by", "") - .strip()).split() # <-- this split() turns the string into a list. + .strip()).split() # <-- This split() turns the string into a list. # Alternative 2 to the nested calls to filter and split is to use a list-comprehension: - return [item for item in - question.strip("?").split() - if item not in ("What", "is", "by")] #<-- The [] of the comprehension invokes implicit concatenation. + return [item for item in + question.strip("?").split() + if item not in ("What", "is", "by")] # <-- The [] of the comprehension invokes implicit concatenation. ``` @@ -98,8 +98,8 @@ def answer(question): raise ValueError("unknown operation") # Clean and split the question into a list for processing. - question = [item for item in - question.strip("?").split() if + question = [item for item in + question.strip("?").split() if item not in ("What", "is", "by")] # Separate candidate operators and numbers into two lists. @@ -107,8 +107,8 @@ def answer(question): # Convert candidate elements to int(), checking for "-". # All other values are replaced with None. - digits = [int(element) if - (element.isdigit() or element[1:].isdigit()) + digits = [int(element) if + (element.isdigit() or element[1:].isdigit()) else None for element in question[::2]] # If there is a mis-match between operators and numbers, toss error. diff --git a/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md index 9fdf3e20e09..7622dcfee8f 100644 --- a/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md +++ b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md @@ -66,7 +66,7 @@ Using a `list-comprehension` to filter out "by" can be replaced with the [`str.r question = (question.removeprefix("What is") .removesuffix("?") .replace("by", "") - .strip()) #<-- Enclosing () means these lines are automatically joined by the interpreter. + .strip()) # <-- Enclosing () means these lines are automatically joined by the interpreter. ``` diff --git a/exercises/practice/wordy/.approaches/introduction.md b/exercises/practice/wordy/.approaches/introduction.md index 821b1228425..5234061bf7d 100644 --- a/exercises/practice/wordy/.approaches/introduction.md +++ b/exercises/practice/wordy/.approaches/introduction.md @@ -13,7 +13,7 @@ The key to a Wordy solution is to remove the "question" portion of the sentence If a single number remains after removing the "question" pieces, it should be converted to an [`int`][int] and returned as the answer. -Any words or word-number combinations that do not fall into the simple mathematical evaluation pattern (_number-operator-number_) should [`raise`][raise-statement] a ["ValueError('syntax error")`][value-error] with a message. +Any words or word-number combinations that do not fall into the simple mathematical evaluation pattern (_number-operator-number_) should [`raise`][raise-statement] a [`ValueError`][value-error] with a message. This includes any "extra" spaces between numbers. As shown in various approaches, there are multiple strategies for validating questions, with no one "canonical" solution. @@ -26,18 +26,18 @@ This could lead to future maintenance issues if the definition of a question eve ~~~~exercism/note There are many Pythonic ways to go about the cleaning, parsing, and calculation steps of Wordy. -However, solutions all follow the same general steps: +However, solutions all follow the same general steps: -1. Remove the parts of the question string that do not apply to calculating the answer. -2. Iterate over the question, determining which words are numbers, and which are meant to be mathematical operations. - — _Converting the question string into a `list` of words is hugely helpful here._ -3. **_Starting from the left_**, take the first three elements and convert number strings to `int` and operations words to the mathematical operations +, -, *, and /. +1. Remove the parts of the question string that do not apply to calculating the answer. +2. Iterate over the question, determining which words are numbers, and which are meant to be mathematical operations. + - _Converting the question string into a `list` of words is hugely helpful here._ +3. **_Starting from the left_**, take the first three elements and convert number strings to `int`s and operation words to the mathematical operations `+`, `-`, `*`, and `/`. 4. Apply the operation to the numbers, which should result in a single number. - — _Employing a `try-except` block can trap any errors thrown and make the code both "safer" and less complex._ -5. Use the calculated number from step 4 as the start for the next "trio" (_number, operation, number_) in the question. The calculated number + the remainder of the question becomes the question being worked on in the next iteration. - — _Using a `while-loop` with a test on the length of the question to do calculation is a very common strategy._ -6. Once the question is calculated down to a single number, that is the answer. Anything else that happens in the loop/iteration or within the accumulated result is a `ValueError("syntax error")`. + - _Employing a `try-except` block can trap any errors thrown and make the code both "safer" and less complex._ +5. Use the calculated number from step 4 as the start for the next "trio" (_number, operation, number_) in the question. The calculated number plus the remainder of the question becomes the question being worked on in the next iteration. + - _Using a `while-loop` with a test on the length of the question to do calculation is a very common strategy._ +6. Once the question is calculated down to a single number, that is the answer. Anything else that happens in the loop/iteration or within the accumulated result is a `ValueError("syntax error")`. ~~~~
@@ -99,7 +99,7 @@ Some solutions use either [lambda][lambdas] expressions, [dunder/"special" metho However, the exercise can be solved without using `operator`, `lambdas`, `dunder-methods` or `eval`. - It is recommended that you first start by solving it _without_ "advanced" strategies, and then refine your solution into something more compact or complex as you learn and practice. +It is recommended that you first start by solving it _without_ "advanced" strategies, and then refine your solution into something more compact or complex as you learn and practice.
@@ -157,11 +157,14 @@ def answer(question): return int(formula[0]) ``` -This approach uses only data structures and methods (_[str methods][str-methods], [list()][list], loops, etc._) from core Python, and does not import any extra modules. +This approach uses only data structures and methods (_[`str` methods][str-methods], [`list()`][list], loops, etc._) from core Python, and does not import any extra modules. It may have more lines of code than average, but it is clear to follow and fairly straightforward to reason about. -It does use a [try-except][handling-exceptions] block for handling unknown operators. -Alternatives could use a [dictionary][dict] to store word --> operator mappings that could be looked up in the `while-loop` using [`.get()`][dict-get], among other strategies. +This approach uses a [`try-except`][handling-exceptions] statment for handling unknown operators. +It does this by raising an error inside the `try` block when `symbol` does not match any operator word. +The `except` block will catch this error (or any other error raised inside the `try` block), and `raise` a `ValueError("syntax error")` instead. (You can look at [exception chaining in the Python docs][exception-chaining] for further detail on this subject.) + +Alternatives could use a [dictionary][dict] to store word to operator mappings that could be looked up in the `while-loop` using [`.get()`][dict-get], among other strategies. For more details and variations, read the [String, List and Dictionary Methods][approach-string-list-and-dict-methods] approach. @@ -185,7 +188,7 @@ def answer(question): if (question.startswith("-") and question[1:].isdigit()) or question.isdigit(): return int(question) - if not question: + if not question: raise ValueError("syntax error") equation = [word for word in question.split() if word != 'by'] @@ -203,7 +206,7 @@ def answer(question): This solution imports methods from the `operator` module, and uses them in a dictionary/lookup map. Like the first approach, it uses a [try-except][handling-exceptions] block for handling unknown operators. - It also uses a [list-comprehension][list-comprehension] to create the parsed "formula" and employs [concept: unpacking and multiple assignment](/tracks/python/concepts/unpacking-and-multiple-assignment). +It also uses a [list-comprehension][list-comprehension] to create the parsed "formula" and employs [concept: unpacking and multiple assignment](/tracks/python/concepts/unpacking-and-multiple-assignment). For more details and options, take a look at the [Import Callables from the Operator Module][approach-import-callables-from-operator] approach. @@ -228,7 +231,7 @@ def get_number(question): pattern = REGEX['number'].match(question) if not pattern: raise ValueError("syntax error") - return [question.removeprefix(pattern.group(0)).lstrip(), + return [question.removeprefix(pattern.group(0)).lstrip(), int(pattern.group(0))] def get_operation(question): @@ -289,7 +292,7 @@ def answer(question): if (question.startswith("-") and question[1:].isdigit()) or question.isdigit(): return int(question) - if not question: + if not question: raise ValueError("syntax error") equation = [word for word in question.split() if word != 'by'] @@ -345,7 +348,7 @@ def calculate(equation): else: try: x_value, operation, y_value, *rest = equation - equation = [OPERATIONS[operation](int(x_value), + equation = [OPERATIONS[operation](int(x_value), int(y_value)), *rest] except: raise ValueError("syntax error") @@ -379,13 +382,13 @@ def answer(question): if not question.startswith( "What is") or "cubed" in question: raise ValueError("unknown operation") - question = list(filter(lambda x: - x not in ("What", "is", "by"), - question.strip("?").split())) + question = list(filter(lambda x: + x not in ("What", "is", "by"), + question.strip("?").split())) operations = question[1::2] - digits = [int(element) if (element.isdigit() or - element[1:].isdigit()) else None for + digits = [int(element) if (element.isdigit() or + element[1:].isdigit()) else None for element in question[::2]] if len(digits)-1 != len(operations) or None in digits: @@ -456,8 +459,7 @@ This is why the `operator` module exists - as a vehicle for providing callable m For more detail on this solution, take a look at the [dunder method with `__getattribute__` approach][approach-dunder-getattribute]. - -[PEMDAS]: https://www.mathnasium.com/math-centers/eagan/news/what-pemdas-e +[PEMDAS]: https://www.mathnasium.com/blog/what-is-pemdas [approach-dunder-getattribute]: https://exercism.org/tracks/python/exercises/wordy/approaches/dunder-getattribute [approach-functools-reduce]: https://exercism.org/tracks/python/exercises/wordy/approaches/functools-reduce [approach-import-callables-from-operator]: https://exercism.org/tracks/python/exercises/wordy/approaches/import-callables-from-operator @@ -470,6 +472,7 @@ For more detail on this solution, take a look at the [dunder method with `__geta [dict]: https://docs.python.org/3/library/stdtypes.html#dict [dunder-methods]: https://www.pythonmorsels.com/what-are-dunder-methods/?watch [endswith]: https://docs.python.org/3.9/library/stdtypes.html#str.endswith +[exception-chaining]: https://docs.python.org/3/tutorial/errors.html#exception-chaining [filter]: https://docs.python.org/3/library/functions.html#filter [find]: https://docs.python.org/3.9/library/stdtypes.html#str.find [functools-reduce]: https://docs.python.org/3/library/functools.html#functools.reduce diff --git a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md index d78f3e7db83..985cec8a83b 100644 --- a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md +++ b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md @@ -3,11 +3,11 @@ ```python OPERATIONS = { - 'minus': lambda a, b: a - b, - 'plus': lambda a, b: a + b, - 'multiplied': lambda a, b: a * b, - 'divided': lambda a, b: a / b - } + 'minus': lambda a, b: a - b, + 'plus': lambda a, b: a + b, + 'multiplied': lambda a, b: a * b, + 'divided': lambda a, b: a / b +} def answer(question): @@ -19,7 +19,7 @@ def answer(question): if (question.startswith("-") and question[1:].isdigit()) or question.isdigit(): return int(question) - if not question: + if not question: raise ValueError("syntax error") equation = question.replace("by", "").split() @@ -40,7 +40,7 @@ The major difference here is the use of [`lambda expressions`][lambdas] in place `lambda expressions` are small "throwaway" expressions that are simple enough to not require a formal function definition or name. They are most commonly used in [`key functions`][key-functions], the built-ins [`map`][map] and [`filter`][filter], and in [`functools.reduce`][functools-reduce]. - `lambdas` are also often defined in areas where a function is needed for one-time use or callback but it would be onerous or confusing to create a full function definition. +`lambdas` are also often defined in areas where a function is needed for one-time use or callback but it would be onerous or confusing to create a full function definition. The two forms are parsed identically (_they are both function definitions_), but in the case of [`lambdas`][lambda], the function name is always "lambda" and the expression cannot contain statements or annotations. For example, the code above could be re-written to include user-defined functions as opposed to `lambda expressions`: @@ -70,7 +70,7 @@ def answer(question): if (question.startswith("-") and question[1:].isdigit()) or question.isdigit(): return int(question) - if not question: + if not question: raise ValueError("syntax error") equation = question.replace("by", "").split() diff --git a/exercises/practice/wordy/.approaches/recursion/content.md b/exercises/practice/wordy/.approaches/recursion/content.md index 794f1b41c19..fe8be593d57 100644 --- a/exercises/practice/wordy/.approaches/recursion/content.md +++ b/exercises/practice/wordy/.approaches/recursion/content.md @@ -13,9 +13,9 @@ That being said, Python famously does not perform [tail-call optimization][tail-
Recursion works best with problem spaces that resemble trees, include [backtracking][backtracking], or become progressively smaller. - Some examples include financial processes like calculating [amortization][amortization] and [depreciation][depreciation], tracking [radiation reduction through nuclei decay][nuclei-decay], and algorithms like [biscetion search][bisection-search], [depth-first search][dfs], and [merge sort][merge-sort]. +Some examples include financial processes like calculating [amortization][amortization] and [depreciation][depreciation], tracking [radiation reduction through nuclei decay][nuclei-decay], and algorithms like [biscetion search][bisection-search], [depth-first search][dfs], and [merge sort][merge-sort]. -
+
Other algorithms such as [breadth-first search][bfs], [Dijkstra's algorithm][dijkstra], and the [Bellman-Ford Algorithm][bellman-ford] lend themselves better to loops. @@ -44,9 +44,9 @@ def clean(question): return (question.removeprefix("What is") .removesuffix("?") .replace("by", "") - .strip()).split() # <-- this split() turns the string into a list. + .strip()).split() # <-- This split() turns the string into a list. -# Recursively calculate the first piece of the equation, calling +# Recursively calculate the first piece of the equation, calling # calculate() on the product + the remainder. # Return the solution when len(equation) is one. def calculate(equation): @@ -60,7 +60,7 @@ def calculate(equation): # Redefine the equation list as the product of the first three # variables concatenated with the unpacked remainder. - equation = [OPERATIONS[operation](int(x_value), + equation = [OPERATIONS[operation](int(x_value), int(y_value)), *rest] except: raise ValueError("syntax error") @@ -90,15 +90,15 @@ The difference being that the `while-loop` test for `len()` 1 now occurs as an ` ```python # Alternative 1 to the chained calls is to use a list-comprehension: - return [item for item in - question.strip("?").split() - if item not in ("What", "is", "by")] #<-- The [] of the comprehension invokes implicit concatenation. + return [item for item in + question.strip("?").split() + if item not in ("What", "is", "by")] # <-- The [] of the comprehension invokes implicit concatenation. # Alternative 2 is the built-in filter(), but it can be somewhat hard to read. - return list(filter(lambda x: - x not in ("What", "is", "by"), - question.strip("?").split())) #<-- The () in list() also invokes implicit concatenation. + return list(filter(lambda x: + x not in ("What", "is", "by"), + question.strip("?").split())) # <-- The () in list() also invokes implicit concatenation. ```
@@ -115,13 +115,13 @@ from operator import floordiv as div # This regex looks for any number 0-9 that may or may not have a - in front of it. DIGITS = re.compile(r"-?\d+") -# These regex look for a number (x or y) before and after a phrase or word. +# These regex look for a number (x or y) before and after a phrase or word. OPERATORS = { - mul: re.compile(r"(?P-?\d+) multiplied by (?P-?\d+)"), - div: re.compile(r"(?P-?\d+) divided by (?P-?\d+)"), - add: re.compile(r"(?P-?\d+) plus (?P-?\d+)"), - sub: re.compile(r"(?P-?\d+) minus (?P-?\d+)"), - } + mul: re.compile(r"(?P-?\d+) multiplied by (?P-?\d+)"), + div: re.compile(r"(?P-?\d+) divided by (?P-?\d+)"), + add: re.compile(r"(?P-?\d+) plus (?P-?\d+)"), + sub: re.compile(r"(?P-?\d+) minus (?P-?\d+)"), +} # This regex looks for any digit 0-9 (optionally negative) followed by any valid operation, # ending in any digit (optionally negative). @@ -145,7 +145,7 @@ def answer(question): # Call the recursive calculate() function. return calculate(question) -# Recursively calculate the first piece of the equation, calling +# Recursively calculate the first piece of the equation, calling # calculate() on the product + the remainder. # Return the solution when len(equation) is one. def calculate(question): @@ -211,7 +211,7 @@ OPERATORS = ( (div, re.compile(r"(?P.*) divided by (?P.*)")), (add, re.compile(r"(?P.*) plus (?P.*)")), (sub, re.compile(r"(?P.*) minus (?P.*)")), - ) +) def answer(question): if not question.startswith( "What is") or "cubed" in question: @@ -230,13 +230,13 @@ def calculate(question): for operation, pattern in OPERATORS: if match := pattern.match(question): - return operation(calculate(match['x']), calculate(match['y'])) #<-- the loop is paused here to make the two recursive calls. + return operation(calculate(match['x']), calculate(match['y'])) # <-- The loop is paused here to make the two recursive calls. raise ValueError("syntax error") ``` This solution uses a `tuple` of nested `tuples` containing the operators from `operator` and regex in place of the dictionaries that have been used in the previous approaches. This saves some space, but requires that the nested `tuples` be unpacked as the main `tuple` is iterated over (_note the `for operation, pattern in OPERATORS:` in the `for-loop`_ ) so that operations can be matched to strings in the question. - The regex is also more generic than the example above (_anything before and after the operation words is allowed_). +The regex is also more generic than the example above (_anything before and after the operation words is allowed_). Recursion is used a bit differently here from the previous variations — the calls are placed [within the `for-loop`][recursion-within-loops]. Because the regex are more generic, they will match a `digit-operation-digit` trio in a longer question, so the line `return operation(calculate(match['x']), calculate(match['y']))` is effectively splitting a question into parts that can then be worked on in their own stack frames. diff --git a/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md b/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md index d3d5c21430d..bc9f24b9b0e 100644 --- a/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md +++ b/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md @@ -22,9 +22,9 @@ def get_number(question): if not pattern: raise ValueError("syntax error") - # Remove the matched pattern from the question, and convert + # Remove the matched pattern from the question, and convert # that same pattern to an int. Return the modified question and the int. - return [question.removeprefix(pattern.group(0)).lstrip(), + return [question.removeprefix(pattern.group(0)).lstrip(), int(pattern.group(0))] # Helper function to extract an operation from the question. @@ -36,7 +36,7 @@ def get_operation(question): if not pattern: raise ValueError("unknown operation") - # Remove the matched pattern from the question, and look up + # Remove the matched pattern from the question, and look up # that same pattern in OPERATIONS. Return the modified question and the operator. return [question.removeprefix(pattern.group(0)).lstrip(), OPERATIONS[pattern.group(0)]] @@ -64,11 +64,11 @@ def answer(question): # into question and operation. question, operation = get_operation(question) - # Call get_number and unpack the result + # Call get_number and unpack the result # into question and num question, num = get_number(question) - # Perform the calculation, using result and num as + # Perform the calculation, using result and num as # arguments to operation. result = operation(result, num) diff --git a/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md index cce88a4bb06..151ab41b2b9 100644 --- a/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md +++ b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md @@ -1,6 +1,5 @@ # String, List, and Dictionary Methods - ```python def answer(question): if not question.startswith("What is") or "cubed" in question: @@ -43,24 +42,25 @@ This eliminates all the [current cases][unknown-operation-tests] where a [`Value Should the definition of a question expand or change, this strategy would need to be revised. -The question is then "cleaned" by removing the prefix "What is" and the suffix "?" ([`str.removeprefix`][removeprefix], [`str.removesuffix`][removesuffix]), replacing "by" with "" ([`str.replace`][str-replace]), and [stripping][strip] any leading or trailing whitespaces. +The question is then "cleaned" by removing the prefix `"What is"` and the suffix `"?"` ([`str.removeprefix`][removeprefix], [`str.removesuffix`][removesuffix]), replacing `"by"` with `""` ([`str.replace`][str-replace]), and [stripping][strip] any leading or trailing whitespace. If the question is now an empty string, a `ValueError("syntax error")` is raised. -The remaining question string is then converted into a `list` of elements via [`str.split`][split], and that `list` is iterated over using a `while-loop` with a `len()` > 1 condition. +The remaining question string is then converted into a `list` of elements via [`str.split`][split], and that `list` is iterated over using a `while-loop` with a `len() > 1` condition. Within a [`try-except`][handling-exceptions] block to trap/handle any errors (_which will all map to `ValueError("syntax error")`_), the question `list` is divided up among 4 variables using [bracket notation][bracket-notation]: 1. The first element, `x_value`. This is assumed to be a number, so it is converted to an `int()` 2. The third element, `y_value`. This is also assumed to be a number and converted to an `int()`. -3. The second element, `symbol`. This is assumed to be an operator, and is left as-is. -4. The `remainder` of the question, if there is any. This is a [slice][list-slice] starting at index 3, and going to the end. +3. The second element, `symbol`. This is assumed to be an operator, and is left as-is. +4. The `remainder` of the question, if there is any. This is a [slice][list-slice] starting at index 3 and going to the end. -`symbol` is then tested for "plus, minus, multiplied, or divided", and the `formula` list is modified by applying the given operation, and creating a new `formula` `list` by concatenating a `list` of the first product with the `remainder` list. +`symbol` is then tested for "plus", "minus", "multiplied", or "divided", and the `formula` list is modified by applying the given operation, and creating a new `formula` `list` by concatenating a `list` of the first product with the `remainder` list. If `symbol` doesn't match any known operators, a `ValueError("syntax error")` is raised. +(Note that this is still inside the `try-except` block, so the [exception is chained][exception-chaining].) Once `len(formula) == 1`, the first element (`formula[0]`) is converted to an `int()` and returned as the answer. @@ -69,7 +69,7 @@ Once `len(formula) == 1`, the first element (`formula[0]`) is converted to an `i ```python -OPERATIONS = {"plus": '+', "minus": '-', "multiplied": '*', "divided": '/'} +OPERATIONS = {"plus": "+", "minus": "-", "multiplied": "*", "divided": "/"} def answer(question): @@ -110,21 +110,21 @@ def answer(question): ````exercism/note -[chaining][method-chaining] is used in the clean step for this variation, and is the equivalent of assigning and re-assigning `question` as is done in the initial approach. - This is because `str.startswith`, `str.endswith`, and `str.replace` all return strings, so the output of one can be used as the input to the next. - - [method-chaining]: https://www.tutorialspoint.com/Explain-Python-class-method-chaining +[Method chaining][method-chaining] is used in the clean step for this variation, and is the equivalent of assigning and re-assigning `question` as is done in the initial approach. +This is because `str.startswith`, `str.endswith`, and `str.replace` all return strings, so the output of one can be used as the input to the next. + +[method-chaining]: https://www.tutorialspoint.com/Explain-Python-class-method-chaining ```` This variation creates a dictionary to map operation words to symbols. It pre-processes the question string into a `formula` list by looking up the operation words and replacing them with the symbols via the [`.get`][dict-get] method, which takes a [default argument][default-argument] for when a [`KeyError`][keyerror] is thrown. -Here the default for `dict.get()` is set to the element being iterated over, which is effectively _"if not found, skip it"_. -This means the number strings will be passed through, even though they would otherwise toss an error. - The results of iterating through the question are appended to `formula` via [`list.append`][list-append]. +Here the default for `dict.get()` is set to the element being iterated over, which is effectively _"if not found, skip it"_. +This means that the number strings will be passed through, even though they would otherwise toss an error. +The results of iterating through the question are appended to `formula` via [`list.append`][list-append]. -This dictionary is not necessary, but does potentially make adding/tracking future operations easier, although the `if-elif-else` block in the `while-loop` is equally awkward for maintenance (_see the [import callables from operator][approach-import-callables-from-operator] for a way to replace the block_). +This dictionary is not necessary, but does potentially make adding/tracking future operations easier, although the `if-elif-else` block in the `while-loop` is equally awkward for maintenance (_see the [import callables from operator approach][approach-import-callables-from-operator] for a way to replace the block_). The `while-loop`, `if-elif-else` block, and the `try-except` block are then the same as in the initial approach. @@ -134,17 +134,16 @@ There are a couple of common alternatives to the `loop-append` used here: 1. [`list-comprehensions`][list-comprehension] duplicate the same process in a more succinct and declarative fashion. This one also includes filtering out "by": ```python - - formula = [OPERATIONS.get(operation, operation) for - operation in question.split() if operation != 'by'] - ``` + formula = [OPERATIONS.get(operation, operation) for + operation in question.split() if operation != "by"] + ``` -2. The built-in [`filter()`][filter] and [`map()`][map] functions used with a [`lambda`][lambdas] to process the elements of the list. - This is identical in process to both the `loop-append` and the `list-comprehension`, but might be easier to reason about for those coming from a more functional programming language: +2. The built-in [`filter()`][filter] and [`map()`][map] functions used with a [`lambda`][lambdas] to process the elements of the list. + This is identical in process to both the `loop-append` and the `list-comprehension`, but might be easier to reason about for those coming from a more functional programming language: ```python - formula = list(map(lambda x : OPERATIONS.get(x, x), - filter(lambda x: x != "by", question.split()))) + formula = list(map(lambda op: OPERATIONS.get(op, op), + filter(lambda op: op != "by", question.split()))) ``` [list-comprehension]: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions @@ -153,27 +152,27 @@ There are a couple of common alternatives to the `loop-append` used here: [map]: https://docs.python.org/3/library/functions.html#map ```` - Rather than indexing and slicing, [concept: unpacking and multiple assignment](/tracks/python/concepts/unpacking-and-multiple-assignment) can be used to assign the variables. - However, this does require a modification to the returned formula `list`: +Rather than indexing and slicing, [concept: unpacking and multiple assignment](/tracks/python/concepts/unpacking-and-multiple-assignment) can be used to assign the variables. +However, this does require a modification to the returned formula `list`: - ```python +```python x_value, operation, y_value, *remainder = formula # <-- Unpacking won't allow conversion to int() here. ... - if symbol == "+": - formula = [int(x_value) + int(y_value)] + remainder # <-- Instead, conversion to int() must happen here. + if symbol == "+": + formula = [int(x_value) + int(y_value)] + remainder # <-- Instead, conversion to int() must happen here. ... - return int(formula[0]) - ``` + return int(formula[0]) +``` ## Variation 2: Structural Pattern Matching to Replace `if-elif-else` Introduced in Python 3.10, [structural pattern matching][structural-pattern-matching] can be used to replace the `if-elif-else` chain in the `while-loop` used in the two approaches above. -In some circumstances, this could be easier to read and/or reason about: +In some circumstances, this could be easier to read and/or reason about: ```python @@ -189,29 +188,31 @@ def answer(question): formula = question.split() while len(formula) > 1: try: - x_value, symbol, y_value, *remainder = formula #<-- unpacking and multiple assignment. + x_value, symbol, y_value, *remainder = formula # <-- Unpacking and multiple assignment. match symbol: - case "plus": + case "plus": formula = [int(x_value) + int(y_value)] + remainder - case "minus": + case "minus": formula = [int(x_value) - int(y_value)] + remainder - case "multiplied": + case "multiplied": formula = [int(x_value) * int(y_value)] + remainder - case "divided": + case "divided": formula = [int(x_value) / int(y_value)] + remainder - case _: - raise ValueError("syntax error") #<-- "fall through case for no match." - except: raise ValueError("syntax error") # <-- error handling for anything else that goes wrong. + case _: + raise ValueError("syntax error") # <-- Fall through case for no match. + except: + raise ValueError("syntax error") # <-- Error handling for anything else that goes wrong. return int(formula[0]) -``` +``` [approach-import-callables-from-operator]: https://exercism.org/tracks/python/exercises/wordy/approaches/import-callables-from-operator [bracket-notation]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations [default-argument]: https://docs.python.org/3/tutorial/controlflow.html#default-argument-values [dict-get]: https://docs.python.org/3/library/stdtypes.html#dict.get [endswith]: https://docs.python.org/3.9/library/stdtypes.html#str.endswith +[exception-chaining]: https://docs.python.org/3/tutorial/errors.html#exception-chaining [handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions [keyerror]: https://docs.python.org/3/library/exceptions.html#KeyError [list-append]: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists diff --git a/exercises/practice/wordy/.approaches/string-list-and-dict-methods/snippet.txt b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/snippet.txt index 700804b6d18..ccf9cc3052d 100644 --- a/exercises/practice/wordy/.approaches/string-list-and-dict-methods/snippet.txt +++ b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/snippet.txt @@ -1,8 +1,8 @@ try: x_value, y_value, symbol, remainder = int(formula[0]), int(formula[2]), formula[1], formula[3:] - if symbol == "+": formula = [x_value + y_value] + remainder - elif symbol == "-": formula = [x_value - y_value] + remainder - elif symbol == "*": formula = [x_value * y_value] + remainder - elif symbol == "/": formula = [x_value / y_value] + remainder + if symbol == "+": formula = [x_value + y_value] + remainder + elif symbol == "-": formula = [x_value - y_value] + remainder + elif symbol == "*": formula = [x_value * y_value] + remainder + elif symbol == "/": formula = [x_value / y_value] + remainder else: raise ValueError("syntax error") except: raise ValueError("syntax error") \ No newline at end of file diff --git a/exercises/practice/wordy/.docs/instructions.append.md b/exercises/practice/wordy/.docs/instructions.append.md index d26afab5fff..c5972700aad 100644 --- a/exercises/practice/wordy/.docs/instructions.append.md +++ b/exercises/practice/wordy/.docs/instructions.append.md @@ -20,14 +20,14 @@ raise ValueError("syntax error") ``` To _handle_ a raised error within a particular code block, one can use a [try-except][handling-exceptions]. - `try-except` blocks "wrap" the code that could potentially cause an error, mapping all the exceptions to one error, multiple errors, or other pieces of code to deal with the problem: +`try-except` blocks "wrap" the code that could potentially cause an error, mapping all the exceptions to one error, multiple errors, or other pieces of code to deal with the problem: ```python while len(equation) > 1: - try: + try: # The questionable/error-prone code goes here,in an indented block - # It can contain statements, loops, if-else blocks, or other executable code. + # It can contain statements, loops, if-else blocks, or other executable code. x_value, operation, y_value, *rest = equation ... ... From d764d802617492762f6429b266536ab50bc95e14 Mon Sep 17 00:00:00 2001 From: Yrahcaz7 <74512479+Yrahcaz7@users.noreply.github.com> Date: Thu, 21 May 2026 11:39:40 -0400 Subject: [PATCH 2/9] `import-callables-from-operator` cleanup also more formatting fixes --- .../practice/wordy/.approaches/config.json | 20 +++-- .../dunder-getattribute/content.md | 2 - .../.approaches/functools-reduce/content.md | 32 ++++--- .../import-callables-from-operator/content.md | 32 ++++--- .../snippet.txt | 2 +- .../wordy/.approaches/introduction.md | 83 +++++++------------ .../lambdas-in-a-dictionary/content.md | 12 ++- .../lambdas-in-a-dictionary/snippet.txt | 10 +-- .../wordy/.approaches/recursion/content.md | 18 ++-- .../regex-with-operator-module/content.md | 13 ++- .../string-list-and-dict-methods/content.md | 27 +++--- 11 files changed, 108 insertions(+), 143 deletions(-) diff --git a/exercises/practice/wordy/.approaches/config.json b/exercises/practice/wordy/.approaches/config.json index 417458430a4..43164f352c8 100644 --- a/exercises/practice/wordy/.approaches/config.json +++ b/exercises/practice/wordy/.approaches/config.json @@ -17,42 +17,48 @@ "slug": "import-callables-from-operator", "title": "Import Callables from the Operator Module", "blurb": "Use Operator Module Methods to Solve Word Problems.", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, { "uuid": "61f44943-8a12-471b-ab15-d0d10fa4f72f", "slug": "regex-with-operator-module", "title": "Regex with the Operator Module", "blurb": "Use Regex with the Callables from Operator to solve word problems.", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, { "uuid": "46bd15dd-cae4-4eb3-ac63-a8b631a508d1", "slug": "lambdas-in-a-dictionary", "title": "Lambdas in a Dictionary to Return Functions", "blurb": "Use lambdas in a dictionary to return functions for solving word problems.", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, { "uuid": "2e643b88-9b76-45a1-98f4-b211919af061", "slug": "recursion", "title": "Recursion for Iteration.", "blurb": "Use recursion with other strategies to solve word problems.", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, { "uuid": "1e136304-959c-4ad1-bc4a-450d13e5f668", "slug": "functools-reduce", - "title": "Functools.reduce for Calculation", + "title": "functools.reduce for Calculation", "blurb": "Use functools.reduce with other strategies to calculate solutions.", - "authors": ["BethanyG"] + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] }, { "uuid": "d643e2b4-daee-422d-b8d3-2cad2f439db5", "slug": "dunder-getattribute", "title": "dunder with __getattribute__", "blurb": "Use dunder methods with __getattribute__.", - "authors": ["bobahop"] + "authors": ["bobahop"], + "contributors": ["yrahcaz7"] } ] } diff --git a/exercises/practice/wordy/.approaches/dunder-getattribute/content.md b/exercises/practice/wordy/.approaches/dunder-getattribute/content.md index 167460f2d3c..4e0c81aeeb7 100644 --- a/exercises/practice/wordy/.approaches/dunder-getattribute/content.md +++ b/exercises/practice/wordy/.approaches/dunder-getattribute/content.md @@ -1,6 +1,5 @@ # Dunder methods with `__getattribute__` - ```python OPS = { "plus": "__add__", @@ -35,7 +34,6 @@ def answer(question): except: raise ValueError("syntax error") return ret[0] - ``` This approach begins by defining a [dictionary][dictionaries] of the word keys with their related [`dunder-methods`][dunder] methods. diff --git a/exercises/practice/wordy/.approaches/functools-reduce/content.md b/exercises/practice/wordy/.approaches/functools-reduce/content.md index b8f51710ffd..892752763ea 100644 --- a/exercises/practice/wordy/.approaches/functools-reduce/content.md +++ b/exercises/practice/wordy/.approaches/functools-reduce/content.md @@ -1,5 +1,4 @@ -# Functools.reduce for Calculation - +# `functools.reduce()` for Calculation ```python from operator import add, mul, sub @@ -39,28 +38,26 @@ def answer(question): ``` This approach replaces the `while-loop` or `recursion` used in many solutions with a call to [`functools.reduce`][functools-reduce]. -It requires that the question be separated into candidate digits and candidate operators, which is accomplished here via [list-slicing][sequence-operations] (_for some additional information on working with `lists`, see [concept: lists](/tracks/python/concepts/lists)_). +It requires that the question be separated into candidate digits and candidate operators, which is accomplished here via [list-slicing][sequence-operations] (_for some additional information on working with `lists`, see [concept:python/lists]()_). A nested call to `filter()` and `split()` within a `list` constructor is used to clean and process the question into an initial `list` of digit and operator strings. -However, this could easily be accomplished by either using [chained][method-chaining] string methods or a `list-comprehension`: - +However, this could easily be accomplished by either using [chained][method-chaining] string methods or a list comprehension: ```python # Alternative 1 is chaining various string methods together. - # The wrapping () invoke implicit concatenation for the chained functions + # The wrapping () invoke implicit concatenation for the chained functions. return (question.removeprefix("What is") .removesuffix("?") .replace("by", "") .strip()).split() # <-- This split() turns the string into a list. - - - # Alternative 2 to the nested calls to filter and split is to use a list-comprehension: + + + # Alternative 2 to the nested calls to filter and split is to use a list comprehension: return [item for item in question.strip("?").split() if item not in ("What", "is", "by")] # <-- The [] of the comprehension invokes implicit concatenation. ``` - Since "valid" questions are all in the form of `digit-operator-digit` (_and so on_), it is safe to assume that every other element beginning at index 0 is a "number", and every other element beginning at index 1 is an operator. By that definition, the operators `list` is 1 shorter in `len()` than the digits list. Anything else (_or having None/an unknown operation in the operations list_) is a `ValueError("syntax error")`. @@ -75,24 +72,23 @@ It could be argued that writing the code as a `while-loop` or recursive function
-## Variation 1: Use a Dictionary of `lambdas` instead of importing from operator - +## Variation 1: Use a Dictionary of `lambdas` instead of importing from operator The imports from operator can be swapped out for a dictionary of `lambda-expressions` (or calls to `dunder-methods`), if so desired. The same cautions apply here as were discussed in the [lambdas in a dictionary][approach-lambdas-in-a-dictionary] approach: - ```python from functools import reduce # Define a lookup table for mathematical operations -OPERATORS = {"plus": lambda x, y: x + y, - "minus": lambda x, y: x - y, - "multiplied": lambda x, y: x * y, - "divided": lambda x, y: x / y} +OPERATORS = { + "plus": lambda x, y: x + y, + "minus": lambda x, y: x - y, + "multiplied": lambda x, y: x * y, + "divided": lambda x, y: x / y +} def answer(question): - # Check for basic validity right away, and fail out with error if not valid. if not question.startswith( "What is") or "cubed" in question: raise ValueError("unknown operation") diff --git a/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md index 7622dcfee8f..63c8f2563c4 100644 --- a/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md +++ b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md @@ -1,6 +1,5 @@ # Import Callables from the Operator Module - ```python from operator import add, mul, sub from operator import floordiv as div @@ -21,7 +20,7 @@ def answer(question): if (question.startswith("-") and question[1:].isdigit()) or question.isdigit(): return int(question) - equation = [word for word in question.split() if word != 'by'] + equation = [word for word in question.split() if word != "by"] while len(equation) > 1: try: @@ -34,44 +33,41 @@ def answer(question): return equation[0] ``` - This approach is nearly identical to the [string, list, and dict methods][approach-string-list-and-dict-methods] approach, so it is recommended to review that before going over this one. The two major differences are the `operator` module, and the elimination of the `if-elif-else` block. The solution begins by importing basic mathematical operations as methods from the [`operator`][operator] module. -These functions (_floordiv is [aliased][aliasing] to "div"_) are stored in a dictionary that serves as a lookup table when the problems are processed. -These operations are later made [callable][callable] by using `()` after the name, and supplying arguments. +`add`, `mul` and `sub` keep their original names, while `floordiv` is [aliased][aliasing] to `div`. +These functions are then stored in a dictionary that serves as a lookup table when the problems are processed. +These operations are later used as [callables][callables] by putting `()` after the name, and supplying arguments between the parentheses. -In `answer()`, the question is first checked for validity, cleaned, and finally split into a `list` using [`str.startswith`][startswith], [`str.removeprefix`][removeprefix]/[`str.removesuffix`][removesuffix], [strip][strip], and [split][split]. -Checks for digits and an empty string are done, and the word "by" is filtered from the equation `list` using a [`list-comprehension`][list-comprehension]. +In `answer()`, the question is first checked for validity, cleaned, and finally split into a `list` using [`str.startswith`][startswith], [`str.removeprefix`][removeprefix]/[`str.removesuffix`][removesuffix], [`str.strip()`][strip], and [`str.split()`][split]. +Next, checks for digits and an empty string are done, and the word "by" is filtered from the equation `list` by using a [list comprehension][list-comprehension]. -The equation `list` is then processed in a `while-loop` within a [try-except][handling-exceptions] block. -The `list` is [unpacked][unpacking] (_see also [concept: unpacking and multiple assignment](/tracks/python/concepts/unpacking-and-multiple-assignment)_) into `x_value`, `operation`, `y_value`, and `*rest`, and reduced by looking up and calling the mathematical function in the OPERATIONS dictionary and passing in `int(x_value)` and `int(y_value)` as arguments. +The equation `list` is then processed in a `while-loop` within a [`try-except`][handling-exceptions] block. +The `list` is [unpacked][unpacking] (_see also [concept:python/unpacking-and-multiple-assignment]()_) into `x_value`, `operation`, `y_value`, and `*rest`, and reduced by looking up and calling the mathematical function in the `OPERATIONS` dictionary and passing in `int(x_value)` and `int(y_value)` as arguments. -The processing of the equation `list` continues until it is of `len()` 1, at which point the single element is returned as the answer. +The processing of the equation `list` continues until its `len() == 1`, at which point the single element is returned as the answer. To walk through this step-by-step, you can interact with this code on [`pythontutor.com`][pythontutor]. -Using a `list-comprehension` to filter out "by" can be replaced with the [`str.replace`][str-replace] method during question cleaning. +Using a list comprehension to filter out "by" can be replaced with the [`str.replace`][str-replace] method during question cleaning. [Implicit concatenation][implicit-concatenation] can be used to improve the readability of the [chained][chaining-method-calls] method calls: - ```python question = (question.removeprefix("What is") .removesuffix("?") .replace("by", "") - .strip()) # <-- Enclosing () means these lines are automatically joined by the interpreter. + .strip()) # <-- Enclosing parentheses means these lines are automatically joined by the interpreter. ``` - -The call to `str.replace` could instead be chained to the call to `split` when creating the equation `list`: - +The call to `str.replace` could instead be chained with the call to `str.split` when creating the equation `list`: ```python equation = question.replace("by", "").split() @@ -79,13 +75,13 @@ equation = question.replace("by", "").split() [aliasing]: https://mimo.org/glossary/python [approach-string-list-and-dict-methods]: https://exercism.org/tracks/python/exercises/wordy/approaches/string-list-and-dict-methods -[callable]: https://treyhunner.com/2019/04/is-it-a-class-or-a-function-its-a-callable/ +[callables]: https://treyhunner.com/2019/04/is-it-a-class-or-a-function-its-a-callable/ [chaining-method-calls]: https://nikhilakki.in/understanding-method-chaining-in-python [handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions [implicit-concatenation]: https://docs.python.org/3/reference/lexical_analysis.html#implicit-line-joining [list-comprehension]: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions [operator]: https://docs.python.org/3/library/operator.html#module-operator -[pythontutor]: https://pythontutor.com/render.html#code=from%20operator%20import%20add,%20mul,%20sub%0Afrom%20operator%20import%20floordiv%20as%20div%0A%0AOPERATIONS%20%3D%20%7B%22plus%22%3A%20add,%20%22minus%22%3A%20sub,%20%22multiplied%22%3A%20mul,%20%22divided%22%3A%20div%7D%0A%0Adef%20answer%28question%29%3A%0A%20%20%20%20if%20not%20question.startswith%28%22What%20is%22%29%20or%20%22cubed%22%20in%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22unknown%20operation%22%29%0A%20%20%20%20%0A%20%20%20%20question%20%3D%20question.removeprefix%28%22What%20is%22%29.removesuffix%28%22%3F%22%29.strip%28%29%0A%0A%20%20%20%20if%20question.isdigit%28%29%3A%20%0A%20%20%20%20%20%20%20%20return%20int%28question%29%0A%20%20%20%20%0A%20%20%20%20if%20not%20question%3A%20%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%20%20%20%20%0A%20%20%20%20equation%20%3D%20%5Bword%20for%20word%20in%20question.split%28%29%20if%20word%20!%3D%20'by'%5D%0A%20%20%20%20%0A%20%20%20%20while%20len%28equation%29%20%3E%201%3A%0A%20%20%20%20%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20x_value,%20operation,%20y_value,%20*rest%20%3D%20equation%0A%20%20%20%20%20%20%20%20%20%20%20%20equation%20%3D%20%5BOPERATIONS%5Boperation%5D%28int%28x_value%29,%20int%28y_value%29%29,%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20*rest%5D%0A%20%20%20%20%20%20%20%20except%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%20%20%20%20%0A%20%20%20%20return%20equation%5B0%5D%0A%20%20%20%20%0Aprint%28answer%28%22What%20is%202%20plus%202%20plus%203%3F%22%29%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false +[pythontutor]: https://pythontutor.com/visualize.html#code=from%20operator%20import%20add,%20mul,%20sub%0Afrom%20operator%20import%20floordiv%20as%20div%0A%0AOPERATIONS%20%3D%20%7B%22plus%22%3A%20add,%20%22minus%22%3A%20sub,%20%22multiplied%22%3A%20mul,%20%22divided%22%3A%20div%7D%0A%0Adef%20answer%28question%29%3A%0A%20%20%20%20if%20not%20question.startswith%28%22What%20is%22%29%20or%20%22cubed%22%20in%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22unknown%20operation%22%29%0A%20%20%20%20%0A%20%20%20%20question%20%3D%20question.removeprefix%28%22What%20is%22%29.removesuffix%28%22%3F%22%29.strip%28%29%0A%0A%20%20%20%20if%20question.isdigit%28%29%3A%20%0A%20%20%20%20%20%20%20%20return%20int%28question%29%0A%20%20%20%20%0A%20%20%20%20if%20not%20question%3A%20%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%20%20%20%20%0A%20%20%20%20equation%20%3D%20%5Bword%20for%20word%20in%20question.split%28%29%20if%20word%20!%3D%20%22by%22%5D%0A%20%20%20%20%0A%20%20%20%20while%20len%28equation%29%20%3E%201%3A%0A%20%20%20%20%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20x_value,%20operation,%20y_value,%20*rest%20%3D%20equation%0A%20%20%20%20%20%20%20%20%20%20%20%20equation%20%3D%20%5BOPERATIONS%5Boperation%5D%28int%28x_value%29,%20int%28y_value%29%29,%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20*rest%5D%0A%20%20%20%20%20%20%20%20except%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%20%20%20%20%0A%20%20%20%20return%20equation%5B0%5D%0A%20%20%20%20%0Aprint%28answer%28%22What%20is%202%20plus%202%20plus%203%3F%22%29%29&curInstr=0&mode=display&origin=opt-frontend.js&py=311 [removeprefix]: https://docs.python.org/3.9/library/stdtypes.html#str.removeprefix [removesuffix]: https://docs.python.org/3.9/library/stdtypes.html#str.removesuffix [split]: https://docs.python.org/3.9/library/stdtypes.html#str.split diff --git a/exercises/practice/wordy/.approaches/import-callables-from-operator/snippet.txt b/exercises/practice/wordy/.approaches/import-callables-from-operator/snippet.txt index d5cb5a13547..de87954eaef 100644 --- a/exercises/practice/wordy/.approaches/import-callables-from-operator/snippet.txt +++ b/exercises/practice/wordy/.approaches/import-callables-from-operator/snippet.txt @@ -2,7 +2,7 @@ OPERATIONS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div} while len(equation) > 1: try: x_value, operation, y_value, *rest = equation - equation = [OPERATIONS[operation](int(x_value), int(y_value)),*rest] + equation = [OPERATIONS[operation](int(x_value), int(y_value)), *rest] except: raise ValueError("syntax error") return equation[0] \ No newline at end of file diff --git a/exercises/practice/wordy/.approaches/introduction.md b/exercises/practice/wordy/.approaches/introduction.md index 5234061bf7d..b23518eff86 100644 --- a/exercises/practice/wordy/.approaches/introduction.md +++ b/exercises/practice/wordy/.approaches/introduction.md @@ -6,18 +6,15 @@ This means that for some of the test cases, the solution will not be the same as
- ## General Guidance The key to a Wordy solution is to remove the "question" portion of the sentence (_"What is", "?"_) and process the remaining words between numbers as [operators][mathematical operators]. If a single number remains after removing the "question" pieces, it should be converted to an [`int`][int] and returned as the answer. - Any words or word-number combinations that do not fall into the simple mathematical evaluation pattern (_number-operator-number_) should [`raise`][raise-statement] a [`ValueError`][value-error] with a message. This includes any "extra" spaces between numbers. As shown in various approaches, there are multiple strategies for validating questions, with no one "canonical" solution. - A whole class of error can be eliminated up front by checking if a question starts with "What is", ends with "?", and does not include the word "cubed". Any other question formulation becomes a `ValueError("unknown operation")`. This could lead to future maintenance issues if the definition of a question ever changes or operations are added, but for the purposes of passing the current Wordy tests, it works well. @@ -45,59 +42,52 @@ However, solutions all follow the same general steps: For question cleaning, [`str.removeprefix`][removeprefix] and [`str.removesuffix`][removesuffix] introduced in `Python 3.9` can be very useful: - ```python ->>> 'Supercalifragilisticexpialidocious'.removeprefix('Super') +>>> "Supercalifragilisticexpialidocious".removeprefix("Super") 'califragilisticexpialidocious' ->>> 'Supercalifragilisticexpialidocious'.removesuffix('expialidocious') +>>> "Supercalifragilisticexpialidocious".removesuffix("expialidocious") 'Supercalifragilistic' -#The two methods can be chained to remove both a suffix and prefix in "one line". -#The line has been broken up here for better display. ->>> ('Supercalifragilisticexpialidocious' - .removesuffix('expialidocious') - .removeprefix('Super')) +# The two methods can be chained to remove both a suffix and prefix in "one line". +# The line has been broken up here for better display. +>>> ("Supercalifragilisticexpialidocious" + .removesuffix("expialidocious") + .removeprefix("Super")) 'califragilistic' ``` - You can also use [`str.startswith`][startswith] and [`str.endswith`][endswith] in conjunction with [string slicing][sequence-operations] for cleaning: - ```python ->>> if 'Supercalifragilisticexpialidocious'.startswith('Super'): - new_string = 'Supercalifragilisticexpialidocious'[5:] +>>> if "Supercalifragilisticexpialidocious".startswith("Super"): + new_string = "Supercalifragilisticexpialidocious"[5:] >>> new_string 'califragilisticexpialidocious' ->>> if new_string.endswith('expialidocious'): +>>> if new_string.endswith("expialidocious"): new_string = new_string[:15] >>> new_string 'califragilistic' ``` - Different combinations of [`str.find`][find], [`str.rfind`][rfind], or [`str.index`][index] with string slicing could also be used to clean up the initial question. A [regex][regex] could be used to process the question as well, but might be considered overkill given the fixed nature of the prefix/suffix and operations. Finally, [`str.strip`][strip] and its variants are very useful for cleaning up any leftover leading or trailing whitespace. Many solutions then use [`str.split`][split] to process the remaining "cleaned" question into a `list` for convenient looping/iteration, although other strategies can also be used: - ```python >>> sentence = "The quick brown fox jumped over the lazy dog 10 times" >>> sentence.split() ['The', 'quick', 'brown', 'fox', 'jumped', 'over', 'the', 'lazy', 'dog', '10', 'times'] ``` - For math operations, many solutions involve importing and using methods from the [operator][operator] module. Some solutions use either [lambda][lambdas] expressions, [dunder/"special" methods][dunder-methods], or even `eval()` to replace words with arithmetic operations. - However, the exercise can be solved without using `operator`, `lambdas`, `dunder-methods` or `eval`. It is recommended that you first start by solving it _without_ "advanced" strategies, and then refine your solution into something more compact or complex as you learn and practice. @@ -116,10 +106,8 @@ It is also entirely unnecessary, as the other methods described here are safer a _____________ - ## Approach: String, List, and Dictionary Methods - ```python def answer(question): if not question.startswith("What is") or "cubed" in question: @@ -160,9 +148,10 @@ def answer(question): This approach uses only data structures and methods (_[`str` methods][str-methods], [`list()`][list], loops, etc._) from core Python, and does not import any extra modules. It may have more lines of code than average, but it is clear to follow and fairly straightforward to reason about. -This approach uses a [`try-except`][handling-exceptions] statment for handling unknown operators. +This approach uses a [`try-except`][handling-exceptions] statement for handling unknown operators. It does this by raising an error inside the `try` block when `symbol` does not match any operator word. -The `except` block will catch this error (or any other error raised inside the `try` block), and `raise` a `ValueError("syntax error")` instead. (You can look at [exception chaining in the Python docs][exception-chaining] for further detail on this subject.) +The `except` block will catch this error (or any other error raised inside the `try` block), and `raise` a `ValueError("syntax error")` instead. +(You can look at [exception chaining in the Python docs][exception-chaining] for further detail on this subject.) Alternatives could use a [dictionary][dict] to store word to operator mappings that could be looked up in the `while-loop` using [`.get()`][dict-get], among other strategies. @@ -172,7 +161,6 @@ For more details and variations, read the [String, List and Dictionary Methods][ ## Approach: Import Callables from the Operator Module - ```python from operator import add, mul, sub from operator import floordiv as div @@ -191,7 +179,7 @@ def answer(question): if not question: raise ValueError("syntax error") - equation = [word for word in question.split() if word != 'by'] + equation = [word for word in question.split() if word != "by"] while len(equation) > 1: try: @@ -205,8 +193,8 @@ def answer(question): ``` This solution imports methods from the `operator` module, and uses them in a dictionary/lookup map. -Like the first approach, it uses a [try-except][handling-exceptions] block for handling unknown operators. -It also uses a [list-comprehension][list-comprehension] to create the parsed "formula" and employs [concept: unpacking and multiple assignment](/tracks/python/concepts/unpacking-and-multiple-assignment). +Like the first approach, it uses a [`try-except`][handling-exceptions] block for handling unknown operators. +It also uses a [list comprehension][list-comprehension] to create the parsed "formula" and employs [concept:python/unpacking-and-multiple-assignment](). For more details and options, take a look at the [Import Callables from the Operator Module][approach-import-callables-from-operator] approach. @@ -214,28 +202,28 @@ For more details and options, take a look at the [Import Callables from the Oper ## Approach: Regex and the Operator Module - ```python import re from operator import add, mul, sub from operator import floordiv as div OPERATIONS = {"plus": add, "minus": sub, "multiplied by": mul, "divided by": div} + REGEX = { - 'number': re.compile(r'-?\d+'), - 'operator': re.compile(f'(?:{"|".join(OPERATIONS)})\\b') + "number": re.compile(r'-?\d+'), + "operator": re.compile(f'(?:{"|".join(OPERATIONS)})\\b') } def get_number(question): - pattern = REGEX['number'].match(question) + pattern = REGEX["number"].match(question) if not pattern: raise ValueError("syntax error") return [question.removeprefix(pattern.group(0)).lstrip(), int(pattern.group(0))] def get_operation(question): - pattern = REGEX['operator'].match(question) + pattern = REGEX["operator"].match(question) if not pattern: raise ValueError("unknown operation") return [question.removeprefix(pattern.group(0)).lstrip(), @@ -250,7 +238,7 @@ def answer(question): question, result = get_number(question) while len(question) > 0: - if REGEX['number'].match(question): + if REGEX["number"].match(question): raise ValueError("syntax error") question, operation = get_operation(question) @@ -261,7 +249,6 @@ def answer(question): return result ``` - This approach uses a dictionary of regex patterns for matching numbers and operators, paired with a dictionary of operations imported from the `operator` module. It pulls number and operator processing out into separate functions and uses a while loop in `answer()` to evaluate the word problem. It also uses multiple assignment for various variables. @@ -273,14 +260,13 @@ For more details, take a look at the [regex-with-operator-module][approach-regex ## Approach: Lambdas in a Dictionary to return Functions - ```python OPERATIONS = { - 'minus': lambda a, b: a - b, - 'plus': lambda a, b: a + b, - 'multiplied': lambda a, b: a * b, - 'divided': lambda a, b: a / b - } + "minus": lambda a, b: a - b, + "plus": lambda a, b: a + b, + "multiplied": lambda a, b: a * b, + "divided": lambda a, b: a / b +} def answer(question): @@ -295,7 +281,7 @@ def answer(question): if not question: raise ValueError("syntax error") - equation = [word for word in question.split() if word != 'by'] + equation = [word for word in question.split() if word != "by"] while len(equation) > 1: try: @@ -308,7 +294,6 @@ def answer(question): return equation[0] ``` - Rather than import methods from the `operator` module, this approach defines a series of [`lambda expressions`][lambdas] in the OPERATIONS dictionary. These `lambdas` then return a function that takes two numbers as arguments, returning the result. @@ -323,7 +308,6 @@ For more details, take a look at the [Lambdas in a Dictionary][approach-lambdas- ## Approach: Recursion - ```python from operator import add, mul, sub from operator import floordiv as div @@ -356,8 +340,7 @@ def calculate(equation): return calculate(equation) ``` - -Like previous approaches that substitute methods from `operator` for `lambdas` or `list-comprehensions` for `loops` that append to a `list` -- `recursion` can be substituted for the `while-loop` that many solutions use to process a parsed word problem. +Like previous approaches that substitute methods from `operator` for `lambdas` or list comprehensions for `loops` that append to a `list` -- `recursion` can be substituted for the `while-loop` that many solutions use to process a parsed word problem. Depending on who is reading the code, `recursion` may or may not be easier to reason about. It may also be more (_or less!_) performant than using a `while-loop` or `functools.reduce` (_see below_), depending on how the various cleaning and error-checking actions are performed. @@ -367,8 +350,7 @@ For more details, take a look at the [recursion][approach-recursion] approach.
-## Approach: functools.reduce() - +## Approach: `functools.reduce()` ```python from operator import add, mul, sub @@ -399,9 +381,8 @@ def answer(question): return result ``` - This approach replaces the `while-loop` used in many solutions (_or the `recursion` strategy outlined in the approach above_) with a call to [`functools.reduce`][functools-reduce]. -It also employs a lookup dictionary for methods imported from the `operator` module, as well as a `list-comprehension`, the built-in [`filter`][filter] function, and multiple string [slices][sequence-operations]. +It also employs a lookup dictionary for methods imported from the `operator` module, as well as a list comprehension, the built-in [`filter`][filter] function, and multiple string [slices][sequence-operations]. If desired, the `operator` imports can be replaced with a dictionary of `lambda` expressions or `dunder-methods`. This solution may be a little less clear to follow or reason about due to the slicing syntax and the particular syntax of both `filter` and `fuctools.reduce`. @@ -412,7 +393,6 @@ For more details and variations, take a look at the [functools.reduce for Calcul ## Approach: Dunder methods with `__getattribute__` - ```python OPS = { "plus": "__add__", @@ -447,7 +427,6 @@ def answer(question): except: raise ValueError("syntax error") return ret[0] - ``` This approach uses the [`dunder methods`][dunder-methods] / ["special methods"][special-methods] / "magic methods" associated with the `int()` class, using the `dunder-method` called [`.__getattribute__`][getattribute] to find the [callable][callable] operation in the `int()` class [namespace][namespace] / dictionary. diff --git a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md index 985cec8a83b..88e1ec96f43 100644 --- a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md +++ b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md @@ -1,12 +1,11 @@ # Lambdas in a Dictionary to Return Functions - ```python OPERATIONS = { - 'minus': lambda a, b: a - b, - 'plus': lambda a, b: a + b, - 'multiplied': lambda a, b: a * b, - 'divided': lambda a, b: a / b + "minus": lambda a, b: a - b, + "plus": lambda a, b: a + b, + "multiplied": lambda a, b: a * b, + "divided": lambda a, b: a / b } @@ -45,7 +44,6 @@ The two forms are parsed identically (_they are both function definitions_), but For example, the code above could be re-written to include user-defined functions as opposed to `lambda expressions`: - ```python def add_(x, y): return x + y @@ -60,7 +58,7 @@ def sub_(x, y): return x - y def answer(question): - operations = {'minus': sub_,'plus': add_,'multiplied': mul_,'divided': div_} + operations = {"minus": sub_, "plus": add_, "multiplied": mul_, "divided": div_} if not question.startswith("What is") or "cubed" in question: raise ValueError("unknown operation") diff --git a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/snippet.txt b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/snippet.txt index 3769bef8c5c..1364338e161 100644 --- a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/snippet.txt +++ b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/snippet.txt @@ -1,6 +1,6 @@ OPERATIONS = { - 'minus': lambda a, b: a - b, - 'plus': lambda a, b: a + b, - 'multiplied': lambda a, b: a * b, - 'divided': lambda a, b: a / b - } \ No newline at end of file + "minus": lambda a, b: a - b, + "plus": lambda a, b: a + b, + "multiplied": lambda a, b: a * b, + "divided": lambda a, b: a / b +} \ No newline at end of file diff --git a/exercises/practice/wordy/.approaches/recursion/content.md b/exercises/practice/wordy/.approaches/recursion/content.md index fe8be593d57..d21c5b3f920 100644 --- a/exercises/practice/wordy/.approaches/recursion/content.md +++ b/exercises/practice/wordy/.approaches/recursion/content.md @@ -1,6 +1,5 @@ # Recursion for Iteration - [Any function that can be written iteratively (_with loops_) can be written using recursion][recursion-and-iteration], and [vice-versa][recursion-is-not-a-superpower]. A recursive strategy [may not always be obvious][looping-vs-recursion] or easy — but it is always possible. So the `while-loop`s used in other approaches to Wordy can be re-written to use recursive calls. @@ -71,9 +70,9 @@ def calculate(equation): This approach separates the solution into three functions: -1. `answer()`, which takes the question and calls `calculate(clean())`, returning the answer to the question. -2. `clean()`, which takes a question string and returns a `list` of parsed words and numbers to calculate from. -3. `calculate()`, which performs the calculations on the `list` recursively, until a single number (_the base case check_) is returned as the answer — or an error is thrown. +1. `answer()`, which takes the question and calls `calculate(clean())`, returning the answer to the question. +2. `clean()`, which takes a question string and returns a `list` of parsed words and numbers to calculate from. +3. `calculate()`, which performs the calculations on the `list` recursively, until a single number (_the base case check_) is returned as the answer — or an error is thrown.
@@ -89,7 +88,7 @@ The difference being that the `while-loop` test for `len()` 1 now occurs as an ` `clean()` can also use any of the strategies detailed in other approaches, two of which are below: ```python - # Alternative 1 to the chained calls is to use a list-comprehension: + # Alternative 1 to the chained calls is to use a list comprehension: return [item for item in question.strip("?").split() if item not in ("What", "is", "by")] # <-- The [] of the comprehension invokes implicit concatenation. @@ -103,8 +102,7 @@ The difference being that the `while-loop` test for `len()` 1 now occurs as an `
-## Variation 1: Use Regex for matching, cleaning, and calculating - +## Variation 1: Use Regex for matching, cleaning, and calculating ```python @@ -129,7 +127,7 @@ VALIDATE = re.compile(r"(?P-?\d+) (multiplied by|divided by|plus|minus) (?P -## Variation 2: Use Regex, Recurse within the For-loop - +## Variation 2: Use Regex, Recurse within the for-loop ```python import re @@ -206,6 +203,7 @@ from operator import add, mul, sub from operator import floordiv as div DIGITS = re.compile(r"-?\d+") + OPERATORS = ( (mul, re.compile(r"(?P.*) multiplied by (?P.*)")), (div, re.compile(r"(?P.*) divided by (?P.*)")), diff --git a/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md b/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md index bc9f24b9b0e..7c5d63b0faa 100644 --- a/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md +++ b/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md @@ -1,6 +1,5 @@ # Regex and the Operator Module - ```python import re from operator import add, mul, sub @@ -9,14 +8,14 @@ from operator import floordiv as div OPERATIONS = {"plus": add, "minus": sub, "multiplied by": mul, "divided by": div} REGEX = { - 'number': re.compile(r'-?\d+'), - 'operator': re.compile(f'(?:{"|".join(OPERATIONS)})\\b') + "number": re.compile(r'-?\d+'), + "operator": re.compile(f'(?:{"|".join(OPERATIONS)})\\b') } # Helper function to extract a number from the question. def get_number(question): # Match a number. - pattern = REGEX['number'].match(question) + pattern = REGEX["number"].match(question) # Toss an error if there is no match. if not pattern: @@ -30,7 +29,7 @@ def get_number(question): # Helper function to extract an operation from the question. def get_operation(question): # Match an operation word - pattern = REGEX['operator'].match(question) + pattern = REGEX["operator"].match(question) # Toss an error if there is no match. if not pattern: @@ -56,8 +55,8 @@ def answer(question): # While there are portions of the question left, continue to process. while len(question) > 0: - # can't have a number followed by a number - if REGEX['number'].match(question): + # Can't have a number followed by a number + if REGEX["number"].match(question): raise ValueError("syntax error") # Call get_operation and unpack the result diff --git a/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md index 151ab41b2b9..0cda8547ac5 100644 --- a/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md +++ b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md @@ -64,9 +64,9 @@ If `symbol` doesn't match any known operators, a `ValueError("syntax error")` is Once `len(formula) == 1`, the first element (`formula[0]`) is converted to an `int()` and returned as the answer. +
-## Variation 1: Use a Dictionary for Lookup/Replace - +## Variation 1: Use a Dictionary for Lookup/Replace ```python OPERATIONS = {"plus": "+", "minus": "-", "multiplied": "*", "divided": "/"} @@ -108,14 +108,12 @@ def answer(question): return int(formula[0]) ``` - -````exercism/note +~~~~exercism/note [Method chaining][method-chaining] is used in the clean step for this variation, and is the equivalent of assigning and re-assigning `question` as is done in the initial approach. This is because `str.startswith`, `str.endswith`, and `str.replace` all return strings, so the output of one can be used as the input to the next. [method-chaining]: https://www.tutorialspoint.com/Explain-Python-class-method-chaining -```` - +~~~~ This variation creates a dictionary to map operation words to symbols. It pre-processes the question string into a `formula` list by looking up the operation words and replacing them with the symbols via the [`.get`][dict-get] method, which takes a [default argument][default-argument] for when a [`KeyError`][keyerror] is thrown. @@ -128,18 +126,17 @@ This dictionary is not necessary, but does potentially make adding/tracking futu The `while-loop`, `if-elif-else` block, and the `try-except` block are then the same as in the initial approach. - -````exercism/note +~~~~exercism/note There are a couple of common alternatives to the `loop-append` used here: -1. [`list-comprehensions`][list-comprehension] duplicate the same process in a more succinct and declarative fashion. This one also includes filtering out "by": +1. [List comprehensions][list-comprehension] duplicate the same process in a more succinct and declarative fashion. This one also includes filtering out "by": ```python formula = [OPERATIONS.get(operation, operation) for operation in question.split() if operation != "by"] ``` 2. The built-in [`filter()`][filter] and [`map()`][map] functions used with a [`lambda`][lambdas] to process the elements of the list. - This is identical in process to both the `loop-append` and the `list-comprehension`, but might be easier to reason about for those coming from a more functional programming language: + This is identical in process to both the `loop-append` and the list comprehension, but might be easier to reason about for those coming from a more functional programming language: ```python formula = list(map(lambda op: OPERATIONS.get(op, op), @@ -150,12 +147,11 @@ There are a couple of common alternatives to the `loop-append` used here: [lambdas]: https://docs.python.org/3/howto/functional.html#small-functions-and-the-lambda-expression [filter]: https://docs.python.org/3/library/functions.html#filter [map]: https://docs.python.org/3/library/functions.html#map -```` +~~~~ -Rather than indexing and slicing, [concept: unpacking and multiple assignment](/tracks/python/concepts/unpacking-and-multiple-assignment) can be used to assign the variables. +Rather than indexing and slicing, [concept:python/unpacking-and-multiple-assignment]() can be used to assign the variables. However, this does require a modification to the returned formula `list`: - ```python x_value, operation, y_value, *remainder = formula # <-- Unpacking won't allow conversion to int() here. @@ -167,14 +163,13 @@ However, this does require a modification to the returned formula `list`: return int(formula[0]) ``` +
-## Variation 2: Structural Pattern Matching to Replace `if-elif-else` - +## Variation 2: Structural Pattern Matching to Replace `if-elif-else` Introduced in Python 3.10, [structural pattern matching][structural-pattern-matching] can be used to replace the `if-elif-else` chain in the `while-loop` used in the two approaches above. In some circumstances, this could be easier to read and/or reason about: - ```python def answer(question): if not question.startswith("What is") or "cubed" in question: From 825194d74f7b5b5d99a2b7b71035e3ae03679be0 Mon Sep 17 00:00:00 2001 From: Yrahcaz7 <74512479+Yrahcaz7@users.noreply.github.com> Date: Thu, 21 May 2026 12:10:53 -0400 Subject: [PATCH 3/9] `regex-with-operator-module` cleanup --- .../import-callables-from-operator/content.md | 4 +- .../wordy/.approaches/introduction.md | 8 +-- .../lambdas-in-a-dictionary/content.md | 4 +- .../regex-with-operator-module/content.md | 52 +++++++++---------- .../regex-with-operator-module/snippet.txt | 2 +- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md index 63c8f2563c4..ffa27422340 100644 --- a/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md +++ b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md @@ -40,7 +40,7 @@ The two major differences are the `operator` module, and the elimination of the The solution begins by importing basic mathematical operations as methods from the [`operator`][operator] module. `add`, `mul` and `sub` keep their original names, while `floordiv` is [aliased][aliasing] to `div`. These functions are then stored in a dictionary that serves as a lookup table when the problems are processed. -These operations are later used as [callables][callables] by putting `()` after the name, and supplying arguments between the parentheses. +These operations are later used as [callables][callable] by putting `()` after the name, and supplying arguments between the parentheses. In `answer()`, the question is first checked for validity, cleaned, and finally split into a `list` using [`str.startswith`][startswith], [`str.removeprefix`][removeprefix]/[`str.removesuffix`][removesuffix], [`str.strip()`][strip], and [`str.split()`][split]. @@ -75,7 +75,7 @@ equation = question.replace("by", "").split() [aliasing]: https://mimo.org/glossary/python [approach-string-list-and-dict-methods]: https://exercism.org/tracks/python/exercises/wordy/approaches/string-list-and-dict-methods -[callables]: https://treyhunner.com/2019/04/is-it-a-class-or-a-function-its-a-callable/ +[callable]: https://treyhunner.com/2019/04/is-it-a-class-or-a-function-its-a-callable/ [chaining-method-calls]: https://nikhilakki.in/understanding-method-chaining-in-python [handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions [implicit-concatenation]: https://docs.python.org/3/reference/lexical_analysis.html#implicit-line-joining diff --git a/exercises/practice/wordy/.approaches/introduction.md b/exercises/practice/wordy/.approaches/introduction.md index b23518eff86..416a390c64b 100644 --- a/exercises/practice/wordy/.approaches/introduction.md +++ b/exercises/practice/wordy/.approaches/introduction.md @@ -210,8 +210,8 @@ from operator import floordiv as div OPERATIONS = {"plus": add, "minus": sub, "multiplied by": mul, "divided by": div} REGEX = { - "number": re.compile(r'-?\d+'), - "operator": re.compile(f'(?:{"|".join(OPERATIONS)})\\b') + "number": re.compile(r"-?\d+"), + "operator": re.compile(f"(?:{'|'.join(OPERATIONS)})\\b") } @@ -233,7 +233,7 @@ def answer(question): prefix = "What is" if not question.startswith(prefix): raise ValueError("unknown operation") - + question = question.removesuffix("?").removeprefix(prefix).lstrip() question, result = get_number(question) @@ -254,7 +254,7 @@ It pulls number and operator processing out into separate functions and uses a w It also uses multiple assignment for various variables. It is longer than some solutions, but clearer and potentially easier to maintain due to the separate `get_operation()` and `get_number()` functions. -For more details, take a look at the [regex-with-operator-module][approach-regex-with-operator-module] approach. +For more details, take a look at the [Regex with the `operator` Module][approach-regex-with-operator-module] approach.
diff --git a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md index 88e1ec96f43..7cb090d0f12 100644 --- a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md +++ b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md @@ -71,8 +71,8 @@ def answer(question): if not question: raise ValueError("syntax error") - equation = question.replace("by", "").split() - + equation = question.replace("by", "").split() + while len(equation) > 1: try: x_value, operation, y_value, *rest = equation diff --git a/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md b/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md index 7c5d63b0faa..0bf7362cf17 100644 --- a/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md +++ b/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md @@ -1,4 +1,4 @@ -# Regex and the Operator Module +# Regex with the `operator` Module ```python import re @@ -8,85 +8,85 @@ from operator import floordiv as div OPERATIONS = {"plus": add, "minus": sub, "multiplied by": mul, "divided by": div} REGEX = { - "number": re.compile(r'-?\d+'), - "operator": re.compile(f'(?:{"|".join(OPERATIONS)})\\b') + "number": re.compile(r"-?\d+"), + "operator": re.compile(f"(?:{'|'.join(OPERATIONS)})\\b") } # Helper function to extract a number from the question. def get_number(question): # Match a number. pattern = REGEX["number"].match(question) - + # Toss an error if there is no match. if not pattern: raise ValueError("syntax error") - - # Remove the matched pattern from the question, and convert - # that same pattern to an int. Return the modified question and the int. + + # Remove the matched pattern from the question, and convert that + # same pattern to an int. Return the modified question and the int. return [question.removeprefix(pattern.group(0)).lstrip(), int(pattern.group(0))] # Helper function to extract an operation from the question. def get_operation(question): - # Match an operation word + # Match an operation word. pattern = REGEX["operator"].match(question) - + # Toss an error if there is no match. if not pattern: raise ValueError("unknown operation") - - # Remove the matched pattern from the question, and look up - # that same pattern in OPERATIONS. Return the modified question and the operator. + + # Remove the matched pattern from the question, and look up that same + # pattern in OPERATIONS. Return the modified question and the operator. return [question.removeprefix(pattern.group(0)).lstrip(), OPERATIONS[pattern.group(0)]] def answer(question): prefix = "What is" - + # Toss an error right away if the question isn't valid. if not question.startswith(prefix): raise ValueError("unknown operation") - + # Clean the question by removing the suffix and prefix and whitespace. question = question.removesuffix("?").removeprefix(prefix).lstrip() - # the question should start with a number + # The question should start with a number. question, result = get_number(question) # While there are portions of the question left, continue to process. while len(question) > 0: - # Can't have a number followed by a number + # Can't have a number followed by a number. if REGEX["number"].match(question): raise ValueError("syntax error") # Call get_operation and unpack the result # into question and operation. question, operation = get_operation(question) - + # Call get_number and unpack the result # into question and num question, num = get_number(question) - # Perform the calculation, using result and num as - # arguments to operation. + # Perform the calculation, using result and num + # as arguments to operation. result = operation(result, num) return result ``` -This approach uses two dictionaries: one of operations imported from `operators`, and another that holds regex for matching digits and matching operations in the text of a question. +This approach uses two dictionaries: one of operations imported from `operators`, and another that holds regex for matching digits and matching operations in the text of a question. -It defines two "helper" functions, `get_number()` and `get_operation`, that take a question and use the regex patterns to remove, convert, and return a number (_`get_number()`_) or an operation (_`get_operation()`_), along with a modified "new question". +It defines two "helper" functions (`get_number()` and `get_operation()`), that both take a question and use the regex patterns to remove, convert, and return a number (_`get_number()`_) or an operation (_`get_operation()`_), along with a modified "new question". -In the `answer()` function, the question is checked for validity (_does it start with "What is"_), and a `ValueError("unknown operation")` it raised if it is not a valid question. -Next, the question is cleaned with [`str.removeprefix`][removeprefix] & [`str.removesuffix`][removesuffix], removing "What is" and "?". -Left-trailing white space is stripped with the help of [`lstrip()`][lstrip]. +In the `answer()` function, the question is checked for validity (_if it starts with "What is"_), and a `ValueError("unknown operation")` it raised if it is not a valid question. +Next, the question is cleaned with [`str.removeprefix`][removeprefix] and [`str.removesuffix`][removesuffix], removing "What is" and "?". +Left-trailing whitespace is stripped with the help of [`str.lstrip()`][lstrip]. After that, the variable `result` is declared with an initial value from `get_number()`. The question is then iterated over via a `while-loop`, which calls `get_operation()` and `get_number()` — "reducing" the question by removing the leading numbers and operator. The return values from each call are [unpacked][unpacking] into a "leftover" question portion, and the number or operator. -The returned operation is then made [callable][callable] using `()`, with result and the "new" number (_returned from `get_number()`_) passed as arguments. -The `loop` then proceeds with processing of the "new question", until the `len()` is 0. +The returned operation is then used as a [callable][callable] by putting `()` after the name, with `result` and the "new" number (_returned from `get_number()`_) passed as arguments. +The loop then proceeds with the processing of the "new question", until the `len()` is 0. Once there is no more question to process, `result` is returned as the answer. diff --git a/exercises/practice/wordy/.approaches/regex-with-operator-module/snippet.txt b/exercises/practice/wordy/.approaches/regex-with-operator-module/snippet.txt index 4d89edb5377..e9f549370a8 100644 --- a/exercises/practice/wordy/.approaches/regex-with-operator-module/snippet.txt +++ b/exercises/practice/wordy/.approaches/regex-with-operator-module/snippet.txt @@ -1,5 +1,5 @@ while len(question) > 0: - if REGEX['number'].match(question): + if REGEX["number"].match(question): raise ValueError("syntax error") question, operation = get_operation(question) From 5c2ec939d8dc64527a398f58c599f289ef0b2570 Mon Sep 17 00:00:00 2001 From: Yrahcaz7 <74512479+Yrahcaz7@users.noreply.github.com> Date: Thu, 21 May 2026 12:44:26 -0400 Subject: [PATCH 4/9] `lambdas-in-a-dictionary` cleanup --- .../import-callables-from-operator/content.md | 2 +- .../wordy/.approaches/introduction.md | 8 +++---- .../lambdas-in-a-dictionary/content.md | 22 ++++++++++--------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md index ffa27422340..618b7daf198 100644 --- a/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md +++ b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md @@ -1,4 +1,4 @@ -# Import Callables from the Operator Module +# Import Callables from the `operator` Module ```python from operator import add, mul, sub diff --git a/exercises/practice/wordy/.approaches/introduction.md b/exercises/practice/wordy/.approaches/introduction.md index 416a390c64b..43a0fb5d4e6 100644 --- a/exercises/practice/wordy/.approaches/introduction.md +++ b/exercises/practice/wordy/.approaches/introduction.md @@ -159,7 +159,7 @@ For more details and variations, read the [String, List and Dictionary Methods][
-## Approach: Import Callables from the Operator Module +## Approach: Import Callables from the `operator` Module ```python from operator import add, mul, sub @@ -200,7 +200,7 @@ For more details and options, take a look at the [Import Callables from the Oper
-## Approach: Regex and the Operator Module +## Approach: Regex with the `operator` Module ```python import re @@ -258,7 +258,7 @@ For more details, take a look at the [Regex with the `operator` Module][approach
-## Approach: Lambdas in a Dictionary to return Functions +## Approach: Lambdas in a Dictionary to Return Functions ```python OPERATIONS = { @@ -294,7 +294,7 @@ def answer(question): return equation[0] ``` -Rather than import methods from the `operator` module, this approach defines a series of [`lambda expressions`][lambdas] in the OPERATIONS dictionary. +Rather than import methods from the `operator` module, this approach defines a series of [`lambda expressions`][lambdas] in the `OPERATIONS` dictionary. These `lambdas` then return a function that takes two numbers as arguments, returning the result. One drawback of this strategy over using named functions or methods from `operator` is the lack of debugging information should something go wrong with evaluation. diff --git a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md index 7cb090d0f12..75f5da8a260 100644 --- a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md +++ b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md @@ -34,8 +34,8 @@ def answer(question): return equation[0] ``` -This approach is nearly identical to the [string, list, and dict methods][approach-string-list-and-dict-methods] and the [import callables from the operator][approach-import-callables-from-operator] approaches, so it is recommended that you review those before going over this one. -The major difference here is the use of [`lambda expressions`][lambdas] in place of `operator` methods or string representations in the OPERATIONS dictionary. +This approach is nearly identical to the [string, list, and dict methods][approach-string-list-and-dict-methods] and the [import callables from the `operator` module][approach-import-callables-from-operator] approaches, so it is recommended that you review those before going over this one. +The major difference here is the use of [`lambda expressions`][lambdas] in place of `operator` methods or string representations in the `OPERATIONS` dictionary. `lambda expressions` are small "throwaway" expressions that are simple enough to not require a formal function definition or name. They are most commonly used in [`key functions`][key-functions], the built-ins [`map`][map] and [`filter`][filter], and in [`functools.reduce`][functools-reduce]. @@ -45,20 +45,20 @@ The two forms are parsed identically (_they are both function definitions_), but For example, the code above could be re-written to include user-defined functions as opposed to `lambda expressions`: ```python -def add_(x, y): +def _add(x, y): return x + y -def mul_(x, y): +def _mul(x, y): return x * y -def div_(x, y): - return x//y +def _div(x, y): + return x // y -def sub_(x, y): +def _sub(x, y): return x - y def answer(question): - operations = {"minus": sub_, "plus": add_, "multiplied": mul_, "divided": div_} + operations = {"minus": _sub, "plus": _add, "multiplied": _mul, "divided": _div} if not question.startswith("What is") or "cubed" in question: raise ValueError("unknown operation") @@ -85,8 +85,9 @@ def answer(question): ``` However, this makes the code more verbose and does not improve readability. -In addition, the functions need to carry a trailing underscore to avoid potential shadowing or name conflict. -It is better and cleaner in this circumstance to use `lambda expressions` for the functions - although it could be argued that importing and using the methods from `operator` is even better and clearer. +In addition, the functions need to have a leading underscore to indicate that they are internal functions for the module, which helps avoid potential shadowing or name conflict (see [PEP 8][pep-8-naming-styles] for more detail). + +It is better and cleaner in this circumstance to use `lambda expressions` for the functions — although it could be argued that importing and using the methods from `operator` is even better and clearer. [approach-import-callables-from-operator]: https://exercism.org/tracks/python/exercises/wordy/approaches/import-callables-from-operator [approach-string-list-and-dict-methods]: https://exercism.org/tracks/python/exercises/wordy/approaches/string-list-and-dict-methods @@ -96,3 +97,4 @@ It is better and cleaner in this circumstance to use `lambda expressions` for th [lambda]: https://docs.python.org/3/reference/expressions.html#lambda [lambdas]: https://docs.python.org/3/howto/functional.html#small-functions-and-the-lambda-expression [map]: https://docs.python.org/3/library/functions.html#map +[pep-8-naming-styles]: https://peps.python.org/pep-0008/#descriptive-naming-styles From 32529fc3681b350a5c910ddc6f6179bd3c9e7627 Mon Sep 17 00:00:00 2001 From: Yrahcaz7 <74512479+Yrahcaz7@users.noreply.github.com> Date: Thu, 21 May 2026 13:35:24 -0400 Subject: [PATCH 5/9] `recursion` approach cleanup --- .../practice/wordy/.approaches/config.json | 2 +- .../.approaches/functools-reduce/snippet.txt | 2 +- .../wordy/.approaches/introduction.md | 32 ++--- .../wordy/.approaches/recursion/content.md | 134 +++++++++--------- .../wordy/.approaches/recursion/snippet.txt | 12 +- 5 files changed, 90 insertions(+), 92 deletions(-) diff --git a/exercises/practice/wordy/.approaches/config.json b/exercises/practice/wordy/.approaches/config.json index 43164f352c8..46ba1e9b3b1 100644 --- a/exercises/practice/wordy/.approaches/config.json +++ b/exercises/practice/wordy/.approaches/config.json @@ -39,7 +39,7 @@ { "uuid": "2e643b88-9b76-45a1-98f4-b211919af061", "slug": "recursion", - "title": "Recursion for Iteration.", + "title": "Recursion for Iteration", "blurb": "Use recursion with other strategies to solve word problems.", "authors": ["BethanyG"], "contributors": ["yrahcaz7"] diff --git a/exercises/practice/wordy/.approaches/functools-reduce/snippet.txt b/exercises/practice/wordy/.approaches/functools-reduce/snippet.txt index f8d5a294195..fa7ff137f6c 100644 --- a/exercises/practice/wordy/.approaches/functools-reduce/snippet.txt +++ b/exercises/practice/wordy/.approaches/functools-reduce/snippet.txt @@ -1,5 +1,5 @@ OPERATORS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div} - +... operations = question[1::2] digits = [int(element) if (element.isdigit() or element[1:].isdigit()) else None for element in question[::2]] diff --git a/exercises/practice/wordy/.approaches/introduction.md b/exercises/practice/wordy/.approaches/introduction.md index 43a0fb5d4e6..b92a6873b7a 100644 --- a/exercises/practice/wordy/.approaches/introduction.md +++ b/exercises/practice/wordy/.approaches/introduction.md @@ -196,7 +196,7 @@ This solution imports methods from the `operator` module, and uses them in a dic Like the first approach, it uses a [`try-except`][handling-exceptions] block for handling unknown operators. It also uses a [list comprehension][list-comprehension] to create the parsed "formula" and employs [concept:python/unpacking-and-multiple-assignment](). -For more details and options, take a look at the [Import Callables from the Operator Module][approach-import-callables-from-operator] approach. +For more details and options, take a look at the [Import Callables from the `operator` Module][approach-import-callables-from-operator] approach.
@@ -325,28 +325,28 @@ def clean(question): .removesuffix("?") .replace("by", "") .strip()).split() - + def calculate(equation): if len(equation) == 1: return int(equation[0]) - else: - try: - x_value, operation, y_value, *rest = equation - equation = [OPERATIONS[operation](int(x_value), - int(y_value)), *rest] - except: - raise ValueError("syntax error") - - return calculate(equation) + + try: + x_value, operation, y_value, *rest = equation + equation = [OPERATIONS[operation](int(x_value), + int(y_value)), *rest] + except: + raise ValueError("syntax error") + + return calculate(equation) ``` -Like previous approaches that substitute methods from `operator` for `lambdas` or list comprehensions for `loops` that append to a `list` -- `recursion` can be substituted for the `while-loop` that many solutions use to process a parsed word problem. +Like previous approaches that substitute methods from `operator` for `lambdas` or list comprehensions for `loops` that append to a `list` — `recursion` can be substituted for the `while-loop` that many solutions use to process a parsed word problem. Depending on who is reading the code, `recursion` may or may not be easier to reason about. It may also be more (_or less!_) performant than using a `while-loop` or `functools.reduce` (_see below_), depending on how the various cleaning and error-checking actions are performed. -The dictionary in this example could use functions from `operator`, `lambdas`, `dunder-methods`, or other strategies -- as long as they can be applied in the `calculate()` function. +The dictionary in this example could use functions from `operator`, `lambdas`, `dunder-methods`, or other strategies — as long as they can be applied in the `calculate()` function. -For more details, take a look at the [recursion][approach-recursion] approach. +For more details, take a look at the [Recursion for Iteration][approach-recursion] approach.
@@ -370,8 +370,8 @@ def answer(question): operations = question[1::2] digits = [int(element) if (element.isdigit() or - element[1:].isdigit()) else None for - element in question[::2]] + element[1:].isdigit()) else None for + element in question[::2]] if len(digits)-1 != len(operations) or None in digits: raise ValueError("syntax error") diff --git a/exercises/practice/wordy/.approaches/recursion/content.md b/exercises/practice/wordy/.approaches/recursion/content.md index d21c5b3f920..69d0d807697 100644 --- a/exercises/practice/wordy/.approaches/recursion/content.md +++ b/exercises/practice/wordy/.approaches/recursion/content.md @@ -24,63 +24,63 @@ Other algorithms such as [breadth-first search][bfs], [Dijkstra's algorithm][dij from operator import add, mul, sub from operator import floordiv as div -# Define a lookup table for mathematical operations +# Define a lookup table for mathematical operations. OPERATIONS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div} def answer(question): - # Call clean() and feed it to calculate() + # Call clean() and feed it to calculate(). return calculate(clean(question)) def clean(question): - # It's not a question unless it starts with 'What is'. + # It's not a question unless it starts with "What is". if not question.startswith("What is") or "cubed" in question: raise ValueError("unknown operation") # Remove the unnecessary parts of the question and # parse the cleaned question into a list of items to process. - # The wrapping () invoke implicit concatenation for the chained functions + # The wrapping () invoke implicit concatenation for the chained functions. return (question.removeprefix("What is") .removesuffix("?") .replace("by", "") .strip()).split() # <-- This split() turns the string into a list. -# Recursively calculate the first piece of the equation, calling -# calculate() on the product + the remainder. +# Recursively calculate the first piece of the equation, +# calling calculate() on the product plus the remainder. # Return the solution when len(equation) is one. def calculate(equation): if len(equation) == 1: return int(equation[0]) - else: - try: - # Unpack the equation into first int, operator, and second int. - # Stuff the remainder into *rest - x_value, operation, y_value, *rest = equation - - # Redefine the equation list as the product of the first three - # variables concatenated with the unpacked remainder. - equation = [OPERATIONS[operation](int(x_value), - int(y_value)), *rest] - except: - raise ValueError("syntax error") - - # Call calculate() with the redefined/partially reduced equation. - return calculate(equation) + + try: + # Unpack the equation into first int, operator, and second int. + # Stuff the remainder into *rest. + x_value, operation, y_value, *rest = equation + + # Redefine the equation list as the product of the first three + # variables concatenated with the unpacked remainder. + equation = [OPERATIONS[operation](int(x_value), + int(y_value)), *rest] + except: + raise ValueError("syntax error") + + # Call calculate() with the redefined/partially reduced equation. + return calculate(equation) ``` This approach separates the solution into three functions: 1. `answer()`, which takes the question and calls `calculate(clean())`, returning the answer to the question. 2. `clean()`, which takes a question string and returns a `list` of parsed words and numbers to calculate from. -3. `calculate()`, which performs the calculations on the `list` recursively, until a single number (_the base case check_) is returned as the answer — or an error is thrown. +3. `calculate()`, which performs the calculations on the `list` recursively, until a single number (_the [base case][base-case] check_) is returned as the answer — or an error is thrown.
The cleaning logic is separate from the processing logic so that the cleaning steps aren't repeated over and over with each recursive `calculate()` call. This separation also makes it easier to make changes without creating conflict or confusion. -`calculate()` performs the same steps as the `while-loop` from [Import Callables from the Operator Module][approach-import-callables-from-operator] and others. -The difference being that the `while-loop` test for `len()` 1 now occurs as an `if` condition in the function (_the base case_), and the "looping" is now a call to `calculate()` in the `else` condition. +`calculate()` performs the same steps as the `while-loop` from the [Import Callables from the `operator` Module][approach-import-callables-from-operator] approach and others. +The difference being that the `while-loop` test for `len() == 1` now occurs as an `if` condition in the function (_the base case_), and the "looping" is now a call to `calculate()` in the `else` condition. `calculate()` can also use many of the strategies detailed in other approaches, as long as they work with the recursion.
@@ -102,10 +102,9 @@ The difference being that the `while-loop` test for `len()` 1 now occurs as an `
-## Variation 1: Use Regex for matching, cleaning, and calculating +## Variation 1: Use regex for matching, cleaning, and calculating ```python - import re from operator import add, mul, sub from operator import floordiv as div @@ -127,75 +126,73 @@ VALIDATE = re.compile(r"(?P-?\d+) (multiplied by|divided by|plus|minus) (?P -## Variation 2: Use Regex, Recurse within the for-loop +## Variation 2: Use regex, recurse within the for-loop ```python import re @@ -212,39 +209,39 @@ OPERATORS = ( ) def answer(question): - if not question.startswith( "What is") or "cubed" in question: + if not question.startswith("What is") or "cubed" in question: raise ValueError("unknown operation") - - question = question.removeprefix( "What is").removesuffix("?").strip() + + question = question.removeprefix("What is").removesuffix("?").strip() if not question: raise ValueError("syntax error") - + return calculate(question) def calculate(question): if DIGITS.fullmatch(question): return int(question) - + for operation, pattern in OPERATORS: - if match := pattern.match(question): - return operation(calculate(match['x']), calculate(match['y'])) # <-- The loop is paused here to make the two recursive calls. + if matches := pattern.match(question): + return operation(calculate(matches["x"]), calculate(matches["y"])) # <-- The loop is paused here to make the two recursive calls. raise ValueError("syntax error") ``` -This solution uses a `tuple` of nested `tuples` containing the operators from `operator` and regex in place of the dictionaries that have been used in the previous approaches. -This saves some space, but requires that the nested `tuples` be unpacked as the main `tuple` is iterated over (_note the `for operation, pattern in OPERATORS:` in the `for-loop`_ ) so that operations can be matched to strings in the question. +This solution uses a `tuple` of nested `tuple`s containing the operators and the regex in place of the dictionaries that have been used in the previous approaches. +This saves some space, but requires that the nested `tuple`s be unpacked as the main `tuple` is iterated over (_note the `for operation, pattern in OPERATORS:` in the `for-loop`_) so that operations can be matched to strings in the question. The regex is also more generic than the example above (_anything before and after the operation words is allowed_). Recursion is used a bit differently here from the previous variations — the calls are placed [within the `for-loop`][recursion-within-loops]. -Because the regex are more generic, they will match a `digit-operation-digit` trio in a longer question, so the line `return operation(calculate(match['x']), calculate(match['y']))` is effectively splitting a question into parts that can then be worked on in their own stack frames. +Because the regex are more generic, they will match a `digit-operation-digit` trio in a longer question, so the line `return operation(calculate(matches["x"]), calculate(matches["y"]))` is effectively splitting a question into parts that can then be worked on in their own stack frames. For example: 1. "1 plus -10 multiplied by 13 divided by 2" would match on "1 plus -10" (_group x_) **multiplied by** "13 divided by 2" (_group y_). -2. This is re-arranged to `mul(calculate("1 plus -10"), calculate("13 divided by 2"))` -3. At this point, the loop pauses as the two recursive calls to `calculate()` spawn +2. This is re-arranged to `mul(calculate("1 plus -10"), calculate("13 divided by 2"))`. +3. At this point, the loop pauses as the two recursive calls to `calculate()` spawn. 4. The loop runs again — and so do the calls to `calculate()` — until there isn't any match that splits the question or any errors. 5. One at a time, the numbers are returned from the `calculate()` calls on the stack, until the main `mul(calculate("1 plus -10"), calculate("13 divided by 2"))` is solved, at which point the answer is returned. @@ -253,6 +250,7 @@ For a more visual picture, you can step through the code on [pythontutor.com][re [amortization]: https://www.investopedia.com/terms/a/amortization.asp [approach-import-callables-from-operator]: https://exercism.org/tracks/python/exercises/wordy/approaches/import-callables-from-operator [backtracking]: https://en.wikipedia.org/wiki/Backtracking +[base-case]: https://en.wikipedia.org/wiki/Recursion_(computer_science)#Base_case [bellman-ford]: https://www.programiz.com/dsa/bellman-ford-algorithm [bfs]: https://en.wikipedia.org/wiki/Breadth-first_search [bisection-search]: https://en.wikipedia.org/wiki/Bisection_method @@ -270,7 +268,7 @@ For a more visual picture, you can step through the code on [pythontutor.com][re [re-match]: https://docs.python.org/3/library/re.html#re.match [re]: https://docs.python.org/3/library/re.html [recursion-and-iteration]: https://web.mit.edu/6.102/www/sp23/classes/11-recursive-data-types/recursion-and-iteration-review.html#:~:text=The%20converse%20is%20also%20true,we%20are%20trying%20to%20solve. -[recursion-in-loop-pythontutor]: https://pythontutor.com/render.html#code=import%20re%0Afrom%20operator%20import%20add,%20mul,%20sub%0Afrom%20operator%20import%20floordiv%20as%20div%0A%0ADIGITS%20%3D%20re.compile%28r%22-%3F%5Cd%2B%22%29%0AOPERATORS%20%3D%20%28%0A%20%20%20%20%28mul,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20multiplied%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28div,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20divided%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28add,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20plus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28sub,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20minus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%29%0A%0Adef%20answer%28question%29%3A%0A%20%20%20%20if%20not%20question.startswith%28%20%22What%20is%22%29%20or%20%22cubed%22%20in%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22unknown%20operation%22%29%0A%20%20%20%20%0A%20%20%20%20question%20%3D%20question.removeprefix%28%20%22What%20is%22%29.removesuffix%28%22%3F%22%29.strip%28%29%0A%0A%20%20%20%20if%20not%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%20%20%20%20%0A%20%20%20%20return%20calculate%28question%29%0A%0Adef%20calculate%28question%29%3A%0A%20%20%20%20if%20DIGITS.fullmatch%28question%29%3A%0A%20%20%20%20%20%20%20%20return%20int%28question%29%0A%20%20%20%20%20%20%20%20%0A%20%20%20%20for%20operation,%20pattern%20in%20OPERATORS%3A%0A%20%20%20%20%20%20%20%20if%20match%20%3A%3D%20pattern.match%28question%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20operation%28calculate%28match%5B'x'%5D%29,%20calculate%28match%5B'y'%5D%29%29%20%23%3C--%20the%20loop%20is%20paused%20here%20to%20make%20the%20two%20recursive%20calls.%0A%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0Aprint%28answer%28%22What%20is%201%20plus%20-10%20multiplied%20by%2013%20divided%20by%202%3F%22%29%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false +[recursion-in-loop-pythontutor]: https://pythontutor.com/visualize.html#code=import%20re%0Afrom%20operator%20import%20add,%20mul,%20sub%0Afrom%20operator%20import%20floordiv%20as%20div%0A%0ADIGITS%20%3D%20re.compile%28r%22-%3F%5Cd%2B%22%29%0A%0AOPERATORS%20%3D%20%28%0A%20%20%20%20%28mul,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20multiplied%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28div,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20divided%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28add,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20plus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28sub,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20minus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%29%0A%0Adef%20answer%28question%29%3A%0A%20%20%20%20if%20not%20question.startswith%28%22What%20is%22%29%20or%20%22cubed%22%20in%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22unknown%20operation%22%29%0A%0A%20%20%20%20question%20%3D%20question.removeprefix%28%22What%20is%22%29.removesuffix%28%22%3F%22%29.strip%28%29%0A%0A%20%20%20%20if%20not%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0A%20%20%20%20return%20calculate%28question%29%0A%0Adef%20calculate%28question%29%3A%0A%20%20%20%20if%20DIGITS.fullmatch%28question%29%3A%0A%20%20%20%20%20%20%20%20return%20int%28question%29%0A%0A%20%20%20%20for%20operation,%20pattern%20in%20OPERATORS%3A%0A%20%20%20%20%20%20%20%20if%20match%20%3A%3D%20pattern.match%28question%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20operation%28calculate%28match%5B%22x%22%5D%29,%20calculate%28match%5B%22y%22%5D%29%29%20%23%20%3C--%20the%20loop%20is%20paused%20here%20to%20make%20the%20two%20recursive%20calls.%0A%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0Aprint%28answer%28%22What%20is%201%20plus%20-10%20multiplied%20by%2013%20divided%20by%202%3F%22%29%29&curInstr=0&mode=display&origin=opt-frontend.js&py=311 [recursion-is-not-a-superpower]: https://inventwithpython.com/blog/2021/09/05/recursion-is-not-a-superpower-an-iterative-ackermann/ [recursion-within-loops]: https://stackoverflow.com/questions/4795527/how-recursion-works-inside-a-for-loop [tail-call-optimization]: https://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html diff --git a/exercises/practice/wordy/.approaches/recursion/snippet.txt b/exercises/practice/wordy/.approaches/recursion/snippet.txt index 373481f8f4b..0a6a15c20d3 100644 --- a/exercises/practice/wordy/.approaches/recursion/snippet.txt +++ b/exercises/practice/wordy/.approaches/recursion/snippet.txt @@ -1,8 +1,8 @@ def calculate(equation): if len(equation) == 1: return int(equation[0]) - else: - try: - x_value, operation, y_value, *rest = equation - equation = [OPERATIONS[operation](int(x_value), int(y_value)), *rest] - except: raise ValueError("syntax error") - return calculate(equation) \ No newline at end of file + try: + x_value, operation, y_value, *rest = equation + equation = [OPERATIONS[operation](int(x_value), int(y_value)), *rest] + except: + raise ValueError("syntax error") + return calculate(equation) \ No newline at end of file From d8f9db23bcd126a444994e266b5a2eb370b3aef6 Mon Sep 17 00:00:00 2001 From: Yrahcaz7 <74512479+Yrahcaz7@users.noreply.github.com> Date: Thu, 21 May 2026 16:17:08 -0400 Subject: [PATCH 6/9] add warning about `recursion` variation 2 --- .../practice/wordy/.approaches/recursion/content.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/exercises/practice/wordy/.approaches/recursion/content.md b/exercises/practice/wordy/.approaches/recursion/content.md index 69d0d807697..673ae5593a4 100644 --- a/exercises/practice/wordy/.approaches/recursion/content.md +++ b/exercises/practice/wordy/.approaches/recursion/content.md @@ -194,6 +194,14 @@ Note that the `for-loop` and `VALIDATE` use [`re.match`][re-match], but the `DIG ## Variation 2: Use regex, recurse within the for-loop +~~~~exercism/caution +As of the time of writing, this variation of the approach passes all of the tests that are currently in the Exercism test suite. +**However**, it gives an incorrect answer to any sufficiently long problem, such as the following: "What is 1 plus -10 multiplied by 3 minus 4?" + +Parsing this example from left to right, we get `((1 + -10) * 3) - 4 == -31`. +However, this variation returns `9` in this case, [as seen here][recursion-in-loop-pythontutor]. +~~~~ + ```python import re from operator import add, mul, sub @@ -245,7 +253,7 @@ For example: 4. The loop runs again — and so do the calls to `calculate()` — until there isn't any match that splits the question or any errors. 5. One at a time, the numbers are returned from the `calculate()` calls on the stack, until the main `mul(calculate("1 plus -10"), calculate("13 divided by 2"))` is solved, at which point the answer is returned. -For a more visual picture, you can step through the code on [pythontutor.com][recursion-in-loop-pythontutor]. +For a more visual picture of the process (including how it fails on long inputs), you can step through the code on [pythontutor.com][recursion-in-loop-pythontutor]. [amortization]: https://www.investopedia.com/terms/a/amortization.asp [approach-import-callables-from-operator]: https://exercism.org/tracks/python/exercises/wordy/approaches/import-callables-from-operator @@ -268,7 +276,7 @@ For a more visual picture, you can step through the code on [pythontutor.com][re [re-match]: https://docs.python.org/3/library/re.html#re.match [re]: https://docs.python.org/3/library/re.html [recursion-and-iteration]: https://web.mit.edu/6.102/www/sp23/classes/11-recursive-data-types/recursion-and-iteration-review.html#:~:text=The%20converse%20is%20also%20true,we%20are%20trying%20to%20solve. -[recursion-in-loop-pythontutor]: https://pythontutor.com/visualize.html#code=import%20re%0Afrom%20operator%20import%20add,%20mul,%20sub%0Afrom%20operator%20import%20floordiv%20as%20div%0A%0ADIGITS%20%3D%20re.compile%28r%22-%3F%5Cd%2B%22%29%0A%0AOPERATORS%20%3D%20%28%0A%20%20%20%20%28mul,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20multiplied%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28div,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20divided%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28add,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20plus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28sub,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20minus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%29%0A%0Adef%20answer%28question%29%3A%0A%20%20%20%20if%20not%20question.startswith%28%22What%20is%22%29%20or%20%22cubed%22%20in%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22unknown%20operation%22%29%0A%0A%20%20%20%20question%20%3D%20question.removeprefix%28%22What%20is%22%29.removesuffix%28%22%3F%22%29.strip%28%29%0A%0A%20%20%20%20if%20not%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0A%20%20%20%20return%20calculate%28question%29%0A%0Adef%20calculate%28question%29%3A%0A%20%20%20%20if%20DIGITS.fullmatch%28question%29%3A%0A%20%20%20%20%20%20%20%20return%20int%28question%29%0A%0A%20%20%20%20for%20operation,%20pattern%20in%20OPERATORS%3A%0A%20%20%20%20%20%20%20%20if%20match%20%3A%3D%20pattern.match%28question%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20operation%28calculate%28match%5B%22x%22%5D%29,%20calculate%28match%5B%22y%22%5D%29%29%20%23%20%3C--%20the%20loop%20is%20paused%20here%20to%20make%20the%20two%20recursive%20calls.%0A%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0Aprint%28answer%28%22What%20is%201%20plus%20-10%20multiplied%20by%2013%20divided%20by%202%3F%22%29%29&curInstr=0&mode=display&origin=opt-frontend.js&py=311 +[recursion-in-loop-pythontutor]: https://pythontutor.com/visualize.html#code=import%20re%0Afrom%20operator%20import%20add,%20mul,%20sub%0Afrom%20operator%20import%20floordiv%20as%20div%0A%0ADIGITS%20%3D%20re.compile%28r%22-%3F%5Cd%2B%22%29%0A%0AOPERATORS%20%3D%20%28%0A%20%20%20%20%28mul,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20multiplied%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28div,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20divided%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28add,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20plus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28sub,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20minus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%29%0A%0Adef%20answer%28question%29%3A%0A%20%20%20%20if%20not%20question.startswith%28%22What%20is%22%29%20or%20%22cubed%22%20in%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22unknown%20operation%22%29%0A%0A%20%20%20%20question%20%3D%20question.removeprefix%28%22What%20is%22%29.removesuffix%28%22%3F%22%29.strip%28%29%0A%0A%20%20%20%20if%20not%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0A%20%20%20%20return%20calculate%28question%29%0A%0Adef%20calculate%28question%29%3A%0A%20%20%20%20if%20DIGITS.fullmatch%28question%29%3A%0A%20%20%20%20%20%20%20%20return%20int%28question%29%0A%0A%20%20%20%20for%20operation,%20pattern%20in%20OPERATORS%3A%0A%20%20%20%20%20%20%20%20if%20matches%20%3A%3D%20pattern.match%28question%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20operation%28calculate%28matches%5B%22x%22%5D%29,%20calculate%28matches%5B%22y%22%5D%29%29%20%23%20%3C--%20the%20loop%20is%20paused%20here%20to%20make%20the%20two%20recursive%20calls.%0A%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0Aprint%28answer%28%22What%20is%201%20plus%20-10%20multiplied%20by%203%20minus%204%3F%22%29%29%0A%0Aprint%28%22Answer%20should%20be%3A%22,%20%28%281%20%2B%20-10%29%20*%203%29%20-%204%29%0A%0Aprint%28%22Result%20from%20wrong%20order%3A%22,%20%281%20%2B%20-10%29%20*%20%283%20-%204%29%29&curInstr=0&mode=display&origin=opt-frontend.js&py=311 [recursion-is-not-a-superpower]: https://inventwithpython.com/blog/2021/09/05/recursion-is-not-a-superpower-an-iterative-ackermann/ [recursion-within-loops]: https://stackoverflow.com/questions/4795527/how-recursion-works-inside-a-for-loop [tail-call-optimization]: https://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html From 3ff204ecdced1878475a6431052d63ee35729255 Mon Sep 17 00:00:00 2001 From: Yrahcaz7 <74512479+Yrahcaz7@users.noreply.github.com> Date: Thu, 21 May 2026 17:02:47 -0400 Subject: [PATCH 7/9] clean up the rest of the approaches --- .../practice/wordy/.approaches/config.json | 2 +- .../dunder-getattribute/content.md | 52 ++++++++++--------- .../dunder-getattribute/snippet.txt | 2 +- .../.approaches/functools-reduce/content.md | 40 +++++++------- .../.approaches/functools-reduce/snippet.txt | 2 +- .../wordy/.approaches/introduction.md | 33 ++++++------ 6 files changed, 69 insertions(+), 62 deletions(-) diff --git a/exercises/practice/wordy/.approaches/config.json b/exercises/practice/wordy/.approaches/config.json index 46ba1e9b3b1..957fc960bfd 100644 --- a/exercises/practice/wordy/.approaches/config.json +++ b/exercises/practice/wordy/.approaches/config.json @@ -55,7 +55,7 @@ { "uuid": "d643e2b4-daee-422d-b8d3-2cad2f439db5", "slug": "dunder-getattribute", - "title": "dunder with __getattribute__", + "title": "Dunder with __getattribute__", "blurb": "Use dunder methods with __getattribute__.", "authors": ["bobahop"], "contributors": ["yrahcaz7"] diff --git a/exercises/practice/wordy/.approaches/dunder-getattribute/content.md b/exercises/practice/wordy/.approaches/dunder-getattribute/content.md index 4e0c81aeeb7..26652cc693f 100644 --- a/exercises/practice/wordy/.approaches/dunder-getattribute/content.md +++ b/exercises/practice/wordy/.approaches/dunder-getattribute/content.md @@ -11,11 +11,12 @@ OPS = { def answer(question): question = question.removeprefix("What is").removesuffix("?").strip() - if not question: raise ValueError("syntax error") + if not question: + raise ValueError("syntax error") if question.startswith("-") and question[1:].isdigit(): return -int(question[1:]) - elif question.isdigit(): + if question.isdigit(): return int(question) found_op = False @@ -23,29 +24,31 @@ def answer(question): if name in question: question = question.replace(name, op) found_op = True - if not found_op: raise ValueError("unknown operation") + if not found_op: + raise ValueError("unknown operation") ret = question.split() while len(ret) > 1: try: x, op, y, *tail = ret - if op not in OPS.values(): raise ValueError("syntax error") + if op not in OPS.values(): + raise ValueError("syntax error") ret = [int(x).__getattribute__(op)(int(y)), *tail] except: raise ValueError("syntax error") return ret[0] ``` -This approach begins by defining a [dictionary][dictionaries] of the word keys with their related [`dunder-methods`][dunder] methods. -Since only whole numbers are involved, the available `dunder-methods` are those for the [`int`][int] class/namespace. +This approach begins by defining a [dictionary][dictionaries] of the word keys with their related [dunder method][dunder] values. +Since only whole numbers are involved, the available dunder methods are those for the [`int`][int] class/namespace. The supported methods for the `int()` namespace can be found by using `print(dir(int))` or `print(int.__dict__)` in a Python terminal. -See [`SO: Difference between dir() and __dict__`][dir-vs-__dict__] for more details. +See [this StackOverflow post][dir-vs-__dict__] for more details.
~~~~exercism/note The built-in [`dir`](https://docs.python.org/3/library/functions.html?#dir) function returns a list of all valid attributes for an object. -The `dunder-method` [`.__dict__`](https://docs.python.org/3/reference/datamodel.html#object.__dict__) is a mapping of an objects writable attributes. +The dunder method [`.__dict__`](https://docs.python.org/3/reference/datamodel.html#object.__dict__) is a mapping of an object's writable attributes. ~~~~
@@ -54,35 +57,37 @@ The `OPS` dictionary is defined with all uppercase letters, which is the naming It indicates that the value should not be changed. The input question to the `answer()` function is cleaned using the [`removeprefix`][removeprefix], [`removesuffix`][removesuffix], and [`strip`][strip] string methods. -The method calls are [chained][method-chaining], so that the output from one call is the input for the next call. -If the input has no characters left, -it uses the [falsiness][falsiness] of an empty string with the [`not`][not] operator to return a `ValueError("syntax error")`. +The method calls are [chained][method-chaining], so the output from one call is the input for the next call. +If the input has no characters left, it uses the [falsiness][falsiness] of an empty string with the [`not`][not] operator to return a `ValueError("syntax error")`. -Next, the [`str.startswith()`][startswith] and [`isdigit`][isdigit] methods are used to see if the remaining characters in the input are either negative or positive digits. -Because "-" is used to denote negative numbers, `str.startswith("-")` is used in the first condition and `question[1:].isdigit()` is then used for the remaining string. -If the `str.isdigit()` checks pass, the [`int()`][int-constructor] constructor is used to return the string as an integer with the proper sign. +Next, the [`str.startswith()`][startswith] and [`str.isdigit()`][isdigit] methods are used to see if the remaining characters in the input are either negative or positive digits. +Because "-" is used to denote negative numbers, `str.startswith("-")` is used in the first condition and `question[1:].isdigit()` is used for the remaining string. +If the `str.isdigit()` checks pass, the [`int()` constructor][int-constructor] is used to return the string as an integer with the proper sign. Next, the elements in the `OPS` dictionary are iterated over. -If the key name is in the input, then the [`str.replace`][replace] method is used to replace the name in the input with the `dunder-method` value. -If none of the key names are found in the input, a `ValueError("unknown operation")` is returned. +If the key name is in the input, the [`str.replace`][replace] method is used to replace the name in the input with the dunder method value. +If none of the key names are found in the input, a `ValueError("unknown operation")` is raised. -At this point the input question is [`split()`][split] into a `list` of its words, which is then iterated over while its [`len()`][len] is greater than 1. +At this point, the input question is [`split()`][split] into a `list` of its words, which is then iterated over while its [`len()`][len] is greater than 1. -Within a [try-except][exception-handling] block, the list is [unpacked][unpacking] (_see also [Concept: unpacking][unpacking-and-multiple-assignment]_) into the variables `x, op, y, and *tail`. -If `op` is not in the supported `dunder-methods` dictionary, a `ValueError("syntax error")` is raised. -If there are any other exceptions raised within the `try` block, they are "caught"/ handled in the `except` clause by raising a `ValueError("syntax error")`. +Within a [`try-except`][exception-handling] block, the list is [unpacked][unpacking] (_see also [concept:python/unpacking-and-multiple-assignment]()_) into the variables `x`, `op`, `y`, and `*tail`. +If `op` is not in the `OPS` dictionary, a `ValueError("syntax error")` is raised. -Next, `x` is converted to an `int` and [`__getattribute__`][getattribute] is called for the `dunder-method` (`op`) to apply to `x`. -`y` is then converted to an `int` and passed as the second arguemnt to `op`. +The `except` block will catch this error (or any other error raised inside the `try` block), and `raise` a `ValueError("syntax error")` instead. +(You can look at [exception chaining in the Python docs][exception-chaining] for further detail on this subject.) + +Next, `x` is converted to an `int` and [`__getattribute__`][getattribute] is called for the dunder method (`op`) to apply to `x`. +`y` is then converted to an `int` and passed as the second argument to `op`. Then `ret` is redefined to a `list` containing the result of the dunder method plus the remaining elements in `*tail`. -When the loop exhausts, the first element of the list is selected as the function return value. +When `ret` reaches `len() == 1` and the loop ends, the first element of `ret` is returned as the answer. [const]: https://realpython.com/python-constants/ [dictionaries]: https://docs.python.org/3/tutorial/datastructures.html#dictionaries [dir-vs-__dict__]: https://stackoverflow.com/a/14361362 [dunder]: https://www.tutorialsteacher.com/python/magic-methods-in-python +[exception-chaining]: https://docs.python.org/3/tutorial/errors.html#exception-chaining [exception-handling]: https://docs.python.org/3/tutorial/errors.html#handling-exceptions [falsiness]: https://www.pythontutorial.net/python-basics/python-boolean/ [getattribute]: https://docs.python.org/3/reference/datamodel.html?#object.__getattribute__ @@ -99,4 +104,3 @@ When the loop exhausts, the first element of the list is selected as the functio [strip]: https://docs.python.org/3/library/stdtypes.html#str.strip [startswith]: https://docs.python.org/3/library/stdtypes.html#str.startswith [unpacking]: https://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/ -[unpacking-and-multiple-assignment]: https://exercism.org/tracks/python/concepts/unpacking-and-multiple-assignment diff --git a/exercises/practice/wordy/.approaches/dunder-getattribute/snippet.txt b/exercises/practice/wordy/.approaches/dunder-getattribute/snippet.txt index d3cc3d16701..7e648873b33 100644 --- a/exercises/practice/wordy/.approaches/dunder-getattribute/snippet.txt +++ b/exercises/practice/wordy/.approaches/dunder-getattribute/snippet.txt @@ -5,4 +5,4 @@ while len(ret) > 1: ret = [int(x).__getattribute__(op)(int(y)), *tail] except: raise ValueError("syntax error") -return ret[0] +return ret[0] \ No newline at end of file diff --git a/exercises/practice/wordy/.approaches/functools-reduce/content.md b/exercises/practice/wordy/.approaches/functools-reduce/content.md index 892752763ea..88920450160 100644 --- a/exercises/practice/wordy/.approaches/functools-reduce/content.md +++ b/exercises/practice/wordy/.approaches/functools-reduce/content.md @@ -11,25 +11,25 @@ OPERATORS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div} def answer(question): # Check for basic validity right away, and fail out with error if not valid. - if not question.startswith( "What is") or "cubed" in question: + if not question.startswith("What is") or "cubed" in question: raise ValueError("unknown operation") - - # Using the built-in filter() to clean & split the question.. + + # Use the built-in filter() to clean and split the question. question = list(filter(lambda x: x not in ("What", "is", "by"), question.strip("?").split())) # Separate candidate operators and numbers into two lists. operations = question[1::2] - + # Convert candidate elements to int(), checking for "-". # All other values are replaced with None. digits = [int(element) if - (element.isdigit() or element[1:].isdigit()) - else None for element in question[::2]] - - # If there is a mis-match between operators and numbers, toss error. - if len(digits)-1 != len(operations) or None in digits: + (element.isdigit() or element[1:].isdigit()) + else None for element in question[::2]] + + # If there is a mis-match between operators and numbers, throw an error. + if len(digits) - 1 != len(operations) or None in digits: raise ValueError("syntax error") # Evaluate the expression from left to right using functools.reduce(). @@ -38,7 +38,7 @@ def answer(question): ``` This approach replaces the `while-loop` or `recursion` used in many solutions with a call to [`functools.reduce`][functools-reduce]. -It requires that the question be separated into candidate digits and candidate operators, which is accomplished here via [list-slicing][sequence-operations] (_for some additional information on working with `lists`, see [concept:python/lists]()_). +It requires that the question be separated into candidate digits and candidate operators, which is accomplished here via [list slicing][sequence-operations] (_for some additional information on working with `lists`, see [concept:python/lists]()_). A nested call to `filter()` and `split()` within a `list` constructor is used to clean and process the question into an initial `list` of digit and operator strings. However, this could easily be accomplished by either using [chained][method-chaining] string methods or a list comprehension: @@ -52,29 +52,29 @@ However, this could easily be accomplished by either using [chained][method-chai .strip()).split() # <-- This split() turns the string into a list. - # Alternative 2 to the nested calls to filter and split is to use a list comprehension: + # Alternative 2 to the nested calls is to use a list comprehension: return [item for item in question.strip("?").split() if item not in ("What", "is", "by")] # <-- The [] of the comprehension invokes implicit concatenation. ``` Since "valid" questions are all in the form of `digit-operator-digit` (_and so on_), it is safe to assume that every other element beginning at index 0 is a "number", and every other element beginning at index 1 is an operator. -By that definition, the operators `list` is 1 shorter in `len()` than the digits list. -Anything else (_or having None/an unknown operation in the operations list_) is a `ValueError("syntax error")`. +By that definition, the `operators` list is 1 shorter in `len()` than the `digits` list. +Anything else (_or having `None`/an unknown operation in the operations list_) is a `ValueError("syntax error")`. -The final call to `functools.reduce` essentially performs the same steps as the `while-loop` implementation, with the `lambda-expression` passing successive items of the digits `list` to the popped and looked-up operation from the operations `list` (_made [callable][callable] by adding `()`_), until it is reduced to one number and returned. +The final call to `functools.reduce` essentially performs the same steps as the `while-loop` implementation, with the `lambda-expression` passing successive items of the `digits` list to the popped and looked-up operation from the operations `list` (_used as a [callable][callable] with `()`_), until it is reduced to one number and returned. A `try-except` is not needed here because the error scenarios are already filtered out in the `if` check right before the call to `reduce()`. -`functools.reduce` is certainly convenient, and makes the solution much shorter. -But it is also hard to understand what is happening if you have not worked with a reduce or foldl function in the past. +`functools.reduce` is certainly convenient, and it makes the solution much shorter. +However, it is also hard to understand what is happening if you have not worked with a `reduce` or `foldl` function in the past. It could be argued that writing the code as a `while-loop` or recursive function is easier to reason about for non-functional programmers.
-## Variation 1: Use a Dictionary of `lambdas` instead of importing from operator +## Variation 1: Use a dictionary of `lambdas` instead of importing from `operator` -The imports from operator can be swapped out for a dictionary of `lambda-expressions` (or calls to `dunder-methods`), if so desired. +The imports from the `operator` module can be swapped out for a dictionary of `lambda-expressions` (or calls to `dunder-methods`), if so desired. The same cautions apply here as were discussed in the [lambdas in a dictionary][approach-lambdas-in-a-dictionary] approach: ```python @@ -90,7 +90,7 @@ OPERATORS = { def answer(question): # Check for basic validity right away, and fail out with error if not valid. - if not question.startswith( "What is") or "cubed" in question: + if not question.startswith("What is") or "cubed" in question: raise ValueError("unknown operation") # Clean and split the question into a list for processing. @@ -112,7 +112,7 @@ def answer(question): raise ValueError("syntax error") # Evaluate the expression from left to right using functools.reduce(). - # Look up each operation in the operation dictionary. + # Look up each operation in the OPERATORS dictionary. result = reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits) return result diff --git a/exercises/practice/wordy/.approaches/functools-reduce/snippet.txt b/exercises/practice/wordy/.approaches/functools-reduce/snippet.txt index fa7ff137f6c..2c1cb3afbd6 100644 --- a/exercises/practice/wordy/.approaches/functools-reduce/snippet.txt +++ b/exercises/practice/wordy/.approaches/functools-reduce/snippet.txt @@ -2,6 +2,6 @@ OPERATORS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div} ... operations = question[1::2] digits = [int(element) if (element.isdigit() or element[1:].isdigit()) - else None for element in question[::2]] + else None for element in question[::2]] ... return reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits) \ No newline at end of file diff --git a/exercises/practice/wordy/.approaches/introduction.md b/exercises/practice/wordy/.approaches/introduction.md index b92a6873b7a..13834278857 100644 --- a/exercises/practice/wordy/.approaches/introduction.md +++ b/exercises/practice/wordy/.approaches/introduction.md @@ -145,7 +145,7 @@ def answer(question): return int(formula[0]) ``` -This approach uses only data structures and methods (_[`str` methods][str-methods], [`list()`][list], loops, etc._) from core Python, and does not import any extra modules. +This approach uses only data structures and methods (_[`str` methods][str-methods], [`list()`][list], loOPERATORS, etc._) from core Python, and does not import any extra modules. It may have more lines of code than average, but it is clear to follow and fairly straightforward to reason about. This approach uses a [`try-except`][handling-exceptions] statement for handling unknown operators. @@ -340,7 +340,7 @@ def calculate(equation): return calculate(equation) ``` -Like previous approaches that substitute methods from `operator` for `lambdas` or list comprehensions for `loops` that append to a `list` — `recursion` can be substituted for the `while-loop` that many solutions use to process a parsed word problem. +Like previous approaches that substitute methods from `operator` for `lambdas` or list comprehensions for `loOPERATORS` that append to a `list` — `recursion` can be substituted for the `while-loop` that many solutions use to process a parsed word problem. Depending on who is reading the code, `recursion` may or may not be easier to reason about. It may also be more (_or less!_) performant than using a `while-loop` or `functools.reduce` (_see below_), depending on how the various cleaning and error-checking actions are performed. @@ -361,7 +361,7 @@ from functools import reduce OPERATORS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div} def answer(question): - if not question.startswith( "What is") or "cubed" in question: + if not question.startswith("What is") or "cubed" in question: raise ValueError("unknown operation") question = list(filter(lambda x: @@ -387,7 +387,7 @@ If desired, the `operator` imports can be replaced with a dictionary of `lambda` This solution may be a little less clear to follow or reason about due to the slicing syntax and the particular syntax of both `filter` and `fuctools.reduce`. -For more details and variations, take a look at the [functools.reduce for Calculation][approach-functools-reduce] approach. +For more details and variations, take a look at the [`functools.reduce` for Calculation][approach-functools-reduce] approach.
@@ -404,11 +404,12 @@ OPS = { def answer(question): question = question.removeprefix("What is").removesuffix("?").strip() - if not question: raise ValueError("syntax error") - + if not question: + raise ValueError("syntax error") + if question.startswith("-") and question[1:].isdigit(): return -int(question[1:]) - elif question.isdigit(): + if question.isdigit(): return int(question) found_op = False @@ -416,27 +417,29 @@ def answer(question): if name in question: question = question.replace(name, op) found_op = True - if not found_op: raise ValueError("unknown operation") + if not found_op: + raise ValueError("unknown operation") ret = question.split() while len(ret) > 1: try: x, op, y, *tail = ret - if op not in OPS.values(): raise ValueError("syntax error") + if op not in OPS.values(): + raise ValueError("syntax error") ret = [int(x).__getattribute__(op)(int(y)), *tail] except: raise ValueError("syntax error") return ret[0] ``` -This approach uses the [`dunder methods`][dunder-methods] / ["special methods"][special-methods] / "magic methods" associated with the `int()` class, using the `dunder-method` called [`.__getattribute__`][getattribute] to find the [callable][callable] operation in the `int()` class [namespace][namespace] / dictionary. -This works because the operators for basic math (_"+, -, *, /, //, %, **"_) have been implemented as callable methods for all integers (_as well as floats and other number types_) and are automatically loaded when the Python interpreter is loaded. +This approach uses the [dunder methods][dunder-methods] / ["special methods"][special-methods] / "magic methods" associated with the `int` class, using the dunder method called [`.__getattribute__`][getattribute] to find the [callable][callable] operation in the `int` class [namespace][namespace] / dictionary. +This works because the operators for basic math (_`+`, `-`, `*`, `/`, `//`, `%`, `**`_) have been implemented as callable methods for all integers (_as well as floats and other numeric types_) and are automatically loaded when the Python interpreter is loaded. -As described in the first link, it is considered bad form to directly call a `dunder method` (_there are some exceptions_), as they are intended mostly for internal Python use, user-defined class customization, and operator overloading (_a specific form of class-customization_). +As described in the first link, it is considered bad form to directly call a dunder method (_there are some exceptions_), as they are intended mostly for internal Python use, user-defined class customization, and operator overloading (_a specific form of class-customization_). -This is why the `operator` module exists - as a vehicle for providing callable methods for basic math when **not** overloading or customizing class functionality. +This is why the `operator` module exists — It is a vehicle for providing callable methods for basic math when **not** overloading or customizing class functionality. -For more detail on this solution, take a look at the [dunder method with `__getattribute__` approach][approach-dunder-getattribute]. +For more detail on this solution, take a look at the [dunder methods with `__getattribute__` approach][approach-dunder-getattribute]. [PEMDAS]: https://www.mathnasium.com/blog/what-is-pemdas [approach-dunder-getattribute]: https://exercism.org/tracks/python/exercises/wordy/approaches/dunder-getattribute @@ -449,7 +452,7 @@ For more detail on this solution, take a look at the [dunder method with `__geta [callable]: https://treyhunner.com/2019/04/is-it-a-class-or-a-function-its-a-callable/ [dict-get]: https://docs.python.org/3/library/stdtypes.html#dict.get [dict]: https://docs.python.org/3/library/stdtypes.html#dict -[dunder-methods]: https://www.pythonmorsels.com/what-are-dunder-methods/?watch +[dunder-methods]: https://www.pythonmorsels.com/what-are-dunder-methods/ [endswith]: https://docs.python.org/3.9/library/stdtypes.html#str.endswith [exception-chaining]: https://docs.python.org/3/tutorial/errors.html#exception-chaining [filter]: https://docs.python.org/3/library/functions.html#filter From cea58508961c61d904b9ebc5555f5020ea29b93a Mon Sep 17 00:00:00 2001 From: Yrahcaz7 <74512479+Yrahcaz7@users.noreply.github.com> Date: Thu, 21 May 2026 21:22:20 -0400 Subject: [PATCH 8/9] fix minor errors in approaches --- .../practice/wordy/.approaches/functools-reduce/content.md | 4 ++-- .../wordy/.approaches/string-list-and-dict-methods/content.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/exercises/practice/wordy/.approaches/functools-reduce/content.md b/exercises/practice/wordy/.approaches/functools-reduce/content.md index 88920450160..01c03a54f7c 100644 --- a/exercises/practice/wordy/.approaches/functools-reduce/content.md +++ b/exercises/practice/wordy/.approaches/functools-reduce/content.md @@ -46,14 +46,14 @@ However, this could easily be accomplished by either using [chained][method-chai ```python # Alternative 1 is chaining various string methods together. # The wrapping () invoke implicit concatenation for the chained functions. - return (question.removeprefix("What is") + question = (question.removeprefix("What is") .removesuffix("?") .replace("by", "") .strip()).split() # <-- This split() turns the string into a list. # Alternative 2 to the nested calls is to use a list comprehension: - return [item for item in + question = [item for item in question.strip("?").split() if item not in ("What", "is", "by")] # <-- The [] of the comprehension invokes implicit concatenation. ``` diff --git a/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md index 0cda8547ac5..eaa5b334048 100644 --- a/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md +++ b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md @@ -153,7 +153,7 @@ Rather than indexing and slicing, [concept:python/unpacking-and-multiple-assignm However, this does require a modification to the returned formula `list`: ```python - x_value, operation, y_value, *remainder = formula # <-- Unpacking won't allow conversion to int() here. + x_value, symbol, y_value, *remainder = formula # <-- Unpacking won't allow conversion to int() here. ... if symbol == "+": From 7ddf961e3c949dacd443ab98c84eac766fbf1e0c Mon Sep 17 00:00:00 2001 From: Yrahcaz7 <74512479+Yrahcaz7@users.noreply.github.com> Date: Thu, 21 May 2026 21:55:52 -0400 Subject: [PATCH 9/9] Apply suggestion from code review Co-authored-by: BethanyG --- exercises/practice/wordy/.approaches/recursion/content.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exercises/practice/wordy/.approaches/recursion/content.md b/exercises/practice/wordy/.approaches/recursion/content.md index 673ae5593a4..4cad29778d4 100644 --- a/exercises/practice/wordy/.approaches/recursion/content.md +++ b/exercises/practice/wordy/.approaches/recursion/content.md @@ -200,6 +200,8 @@ As of the time of writing, this variation of the approach passes all of the test Parsing this example from left to right, we get `((1 + -10) * 3) - 4 == -31`. However, this variation returns `9` in this case, [as seen here][recursion-in-loop-pythontutor]. + +[recursion-in-loop-pythontutor]: https://pythontutor.com/visualize.html#code=import%20re%0Afrom%20operator%20import%20add,%20mul,%20sub%0Afrom%20operator%20import%20floordiv%20as%20div%0A%0ADIGITS%20%3D%20re.compile%28r%22-%3F%5Cd%2B%22%29%0A%0AOPERATORS%20%3D%20%28%0A%20%20%20%20%28mul,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20multiplied%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28div,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20divided%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28add,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20plus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28sub,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20minus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%29%0A%0Adef%20answer%28question%29%3A%0A%20%20%20%20if%20not%20question.startswith%28%22What%20is%22%29%20or%20%22cubed%22%20in%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22unknown%20operation%22%29%0A%0A%20%20%20%20question%20%3D%20question.removeprefix%28%22What%20is%22%29.removesuffix%28%22%3F%22%29.strip%28%29%0A%0A%20%20%20%20if%20not%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0A%20%20%20%20return%20calculate%28question%29%0A%0Adef%20calculate%28question%29%3A%0A%20%20%20%20if%20DIGITS.fullmatch%28question%29%3A%0A%20%20%20%20%20%20%20%20return%20int%28question%29%0A%0A%20%20%20%20for%20operation,%20pattern%20in%20OPERATORS%3A%0A%20%20%20%20%20%20%20%20if%20matches%20%3A%3D%20pattern.match%28question%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20operation%28calculate%28matches%5B%22x%22%5D%29,%20calculate%28matches%5B%22y%22%5D%29%29%20%23%20%3C--%20the%20loop%20is%20paused%20here%20to%20make%20the%20two%20recursive%20calls.%0A%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0Aprint%28answer%28%22What%20is%201%20plus%20-10%20multiplied%20by%203%20minus%204%3F%22%29%29%0A%0Aprint%28%22Answer%20should%20be%3A%22,%20%28%281%20%2B%20-10%29%20*%203%29%20-%204%29%0A%0Aprint%28%22Result%20from%20wrong%20order%3A%22,%20%281%20%2B%20-10%29%20*%20%283%20-%204%29%29&curInstr=0&mode=display&origin=opt-frontend.js&py=311 ~~~~ ```python