[Wordy & Wordy Approaches]: Added 6 Additional Approaches & Modified the Instruction Append for Wordy. (#3783)
* Added 6 additional appraches and extended introduction for Wordy. * Corrected slug for regex with operator module approach.
This commit is contained in:
@@ -1,9 +1,51 @@
|
||||
{
|
||||
"introduction": {
|
||||
"authors": ["bobahop"],
|
||||
"contributors": []
|
||||
"authors": ["BethanyG"],
|
||||
"contributors": ["bobahop"]
|
||||
},
|
||||
"approaches": [
|
||||
{
|
||||
"uuid": "4eeb0638-671a-4289-a83c-583b616dc698",
|
||||
"slug": "string-list-and-dict-methods",
|
||||
"title": "String, List, and Dictionary Methods",
|
||||
"blurb": "Use Core Python Features to Solve Word Problems.",
|
||||
"authors": ["BethanyG"]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"slug": "recursion",
|
||||
"title": "Recursion for iteration.",
|
||||
"blurb": "Use recursion with other strategies to solve word problems.",
|
||||
"authors": ["BethanyG"]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Dunder methods with `__getattribute__`
|
||||
|
||||
|
||||
```python
|
||||
OPS = {
|
||||
"plus": "__add__",
|
||||
@@ -33,70 +34,61 @@ def answer(question):
|
||||
|
||||
```
|
||||
|
||||
This approach begins by defining a [dictionary][dictionaries] of the word keys with their related [dunder][dunder] methods.
|
||||
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.
|
||||
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 differences between the two.
|
||||
|
||||
~~~~exercism/note
|
||||
They are called "dunder" methods because they have **d**ouble **under**scores at the beginning and end of the method name.
|
||||
They are also called magic methods.
|
||||
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` [`<object>.__dict__`](https://docs.python.org/3/reference/datamodel.html#object.__dict__) is a mapping of an objects writable attributes.
|
||||
~~~~
|
||||
|
||||
Since only whole numbers are involved, the dunder methods are those for [`int`][int].
|
||||
The supported methods for `int` can be found by using `print(dir(int))`.
|
||||
The `OPS` dictionary is defined with all uppercase letters, which is the naming convention for a Python [constant][const].
|
||||
It indicates that the value should not be changed.
|
||||
|
||||
~~~~exercism/note
|
||||
The built-in [`dir`](https://docs.python.org/3/library/functions.html?#dir) function returns a list of valid attributes for an object.
|
||||
~~~~
|
||||
|
||||
Python doesn't _enforce_ having real constant values,
|
||||
but the `OPS` dictionary is defined with all uppercase letters, which is the naming convention for a Python [constant][const].
|
||||
It indicates that the value is not intended to be changed.
|
||||
|
||||
The input question to the `answer` function is cleaned using the [`removeprefix`][removeprefix], [`removesuffix`][removesuffix], and [`strip`][strip] methods.
|
||||
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 the [`ValueError`][value-error] for having a syntax error.
|
||||
it uses the [falsiness][falsiness] of an empty string with the [`not`][not] operator to return a `ValueError("syntax error")`.
|
||||
|
||||
Next, the [`isdigit`][isdigit] method is used to see if all of the remaining characters in the input are digits.
|
||||
Next, the [`isdigit`][isdigit] method is used to see if the remaining characters in the input are digits.
|
||||
If so, it uses the [`int()`][int-constructor] constructor to return the string as an integer.
|
||||
|
||||
Next, the elements in the `OPS` dictionary are iterated.
|
||||
If the key name is in the input, then the [`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, then a `ValueError` is returned for having an unknown operation.
|
||||
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.
|
||||
|
||||
At this point the input question is [`split()`][split] into a list of its words, which is then iterated 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][exception-handling], the list is [destructured][destructure] into `x, op, y, *tail`.
|
||||
If `op` is not in the supported dunder methods, it raises `ValueError("syntax error")`.
|
||||
If there are any other exceptions raised in the try, `except` raises `ValueError("syntax error")`
|
||||
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")`.
|
||||
|
||||
Next, it converts `x` to an `int` and calls the [`__getattribute__`][getattribute] for its dunder method and calls it,
|
||||
passing it `y` converted to an `int`.
|
||||
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`.
|
||||
|
||||
It sets the list to the result of the dunder method plus the remaining elements in `*tail`.
|
||||
|
||||
~~~~exercism/note
|
||||
The `*` prefix in `*tail` [unpacks](https://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/) the `tail` list back into its elements.
|
||||
This concept is also a part of [unpacking-and-multiple-assignment](https://exercism.org/tracks/python/concepts/unpacking-and-multiple-assignment) concept in the syllabus.
|
||||
~~~~
|
||||
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.
|
||||
|
||||
[dictionaries]: https://docs.python.org/3/tutorial/datastructures.html#dictionaries
|
||||
[dunder]: https://www.tutorialsteacher.com/python/magic-methods-in-python
|
||||
[int]: https://docs.python.org/3/library/stdtypes.html#typesnumeric
|
||||
[const]: https://realpython.com/python-constants/
|
||||
[removeprefix]: https://docs.python.org/3/library/stdtypes.html#str.removeprefix
|
||||
[removesuffix]: https://docs.python.org/3/library/stdtypes.html#str.removesuffix
|
||||
[strip]: https://docs.python.org/3/library/stdtypes.html#str.strip
|
||||
[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-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__
|
||||
[int-constructor]: https://docs.python.org/3/library/functions.html?#int
|
||||
[int]: https://docs.python.org/3/library/stdtypes.html#typesnumeric
|
||||
[isdigit]: https://docs.python.org/3/library/stdtypes.html?#str.isdigit
|
||||
[len]: https://docs.python.org/3/library/functions.html?#len
|
||||
[method-chaining]: https://www.tutorialspoint.com/Explain-Python-class-method-chaining
|
||||
[not]: https://docs.python.org/3/library/operator.html?#operator.__not__
|
||||
[falsiness]: https://www.pythontutorial.net/python-basics/python-boolean/
|
||||
[value-error]: https://docs.python.org/3/library/exceptions.html?#ValueError
|
||||
[isdigit]: https://docs.python.org/3/library/stdtypes.html?#str.isdigit
|
||||
[int-constructor]: https://docs.python.org/3/library/functions.html?#int
|
||||
[removeprefix]: https://docs.python.org/3/library/stdtypes.html#str.removeprefix
|
||||
[removesuffix]: https://docs.python.org/3/library/stdtypes.html#str.removesuffix
|
||||
[replace]: https://docs.python.org/3/library/stdtypes.html?#str.replace
|
||||
[split]: https://docs.python.org/3/library/stdtypes.html?#str.split
|
||||
[len]: https://docs.python.org/3/library/functions.html?#len
|
||||
[exception-handling]: https://docs.python.org/3/tutorial/errors.html#handling-exceptions
|
||||
[destructure]: https://riptutorial.com/python/example/14981/destructuring-assignment
|
||||
[getattribute]: https://docs.python.org/3/reference/datamodel.html?#object.__getattribute__
|
||||
[strip]: https://docs.python.org/3/library/stdtypes.html#str.strip
|
||||
[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
|
||||
|
||||
126
exercises/practice/wordy/.approaches/functools-reduce/content.md
Normal file
126
exercises/practice/wordy/.approaches/functools-reduce/content.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Functools.reduce for Calculation
|
||||
|
||||
|
||||
```python
|
||||
from operator import add, mul, sub
|
||||
from operator import floordiv as div
|
||||
from functools import reduce
|
||||
|
||||
|
||||
# Define a lookup table for mathematical operations
|
||||
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:
|
||||
raise ValueError("unknown operation")
|
||||
|
||||
# Using the built-in filter() to clean & split the 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:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
# Evaluate the expression from left to right using functools.reduce().
|
||||
# Look up each operation in the operation dictionary.
|
||||
return reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits)
|
||||
```
|
||||
|
||||
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)_).
|
||||
|
||||
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`:
|
||||
|
||||
|
||||
```python
|
||||
# Alternative 1 is chaining various string methods together.
|
||||
# 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:
|
||||
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")`.
|
||||
|
||||
|
||||
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.
|
||||
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.
|
||||
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
|
||||
|
||||
|
||||
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}
|
||||
|
||||
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")
|
||||
|
||||
# Clean and split the question into a list for processing.
|
||||
question = [item for item in
|
||||
question.strip("?").split() if
|
||||
item not in ("What", "is", "by")]
|
||||
|
||||
# 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:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
# Evaluate the expression from left to right using functools.reduce().
|
||||
# Look up each operation in the operation dictionary.
|
||||
result = reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits)
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
[approach-lambdas-in-a-dictionary]: https://exercise.org/tracks/python/exercises/wordy/approaches/lambdas-in-a-dictionary
|
||||
[callable]: https://treyhunner.com/2019/04/is-it-a-class-or-a-function-its-a-callable/
|
||||
[functools-reduce]: https://docs.python.org/3/library/functools.html#functools.reduce
|
||||
[method-chaining]: https://www.tutorialspoint.com/Explain-Python-class-method-chaining
|
||||
[sequence-operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations
|
||||
@@ -0,0 +1,7 @@
|
||||
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]]
|
||||
...
|
||||
return reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits)
|
||||
@@ -0,0 +1,93 @@
|
||||
# Import Callables from the Operator Module
|
||||
|
||||
|
||||
```python
|
||||
from operator import add, mul, sub
|
||||
from operator import floordiv as div
|
||||
|
||||
OPERATIONS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div}
|
||||
|
||||
def answer(question):
|
||||
if not question.startswith("What is") or "cubed" in question:
|
||||
raise ValueError("unknown operation")
|
||||
|
||||
question = question.removeprefix("What is").removesuffix("?").strip()
|
||||
|
||||
if question.isdigit():
|
||||
return int(question)
|
||||
|
||||
if not question:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
equation = [word for word in question.split() if word != 'by']
|
||||
|
||||
while len(equation) > 1:
|
||||
try:
|
||||
x_value, operation, y_value, *rest = equation
|
||||
equation = [OPERATIONS[operation](int(x_value), int(y_value)),
|
||||
*rest]
|
||||
except:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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].
|
||||
|
||||
|
||||
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 processing of the equation `list` continues until it is of `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.
|
||||
[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.
|
||||
```
|
||||
|
||||
|
||||
The call to `str.replace` could instead be chained to the call to `split` when creating the equation `list`:
|
||||
|
||||
|
||||
```python
|
||||
equation = question.replace("by", "").split()
|
||||
```
|
||||
|
||||
[aliasing]: https://mimo.org/glossary/python
|
||||
[approach-string-list-and-dict-methods]: https://exercise.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/
|
||||
[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
|
||||
[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
|
||||
[startswith]: https://docs.python.org/3.9/library/stdtypes.html#str.startswith
|
||||
[str-replace]: https://docs.python.org/3/library/stdtypes.html#str.replace
|
||||
[strip]: https://docs.python.org/3.9/library/stdtypes.html#str.strip
|
||||
[unpacking]: https://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/
|
||||
@@ -0,0 +1,8 @@
|
||||
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]
|
||||
except:
|
||||
raise ValueError("syntax error")
|
||||
return equation[0]
|
||||
@@ -1,22 +1,367 @@
|
||||
# Introduction
|
||||
|
||||
There are various ways to solve Wordy.
|
||||
Using [`eval`][eval] is a [convenient but potentially dangerous][eval-danger] approach.
|
||||
Another approach could replace the operation words with [dunder][dunder] methods.
|
||||
The objective of the Wordy exercise is to parse and evaluate small/simple mathematical word problems, returning the result as an integer.
|
||||
These problems do not require complex or [PEMDAS][PEMDAS]-based evaluation and are instead processed from left-to-right _in sequence_.
|
||||
This means that for some of the test cases, the solution will not be the same as if the word problem was evaluated like a traditional math problem.
|
||||
|
||||
~~~~exercism/note
|
||||
They are called "dunder" methods because they have **d**ouble **under**scores at the beginning and end of the method name.
|
||||
They are also called magic methods.
|
||||
~~~~
|
||||
|
||||
The dunder methods can be called by using the [`__getattribute__`][getattribute] method for [`int`][int].
|
||||
## General Guidance
|
||||
|
||||
## 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", 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.
|
||||
|
||||
One way to reduce the number of `raise` statements/ `ValueError`s needed in the code is to determine if a problem is a "valid" question _before_ proceeding to parsing and calculation.
|
||||
As shown in various approaches, there are multiple strategies for validating questions, with no one "canonical" solution.
|
||||
One very effective approach is to check if a question starts with "What is", ends with "?", and includes only valid operations.
|
||||
That 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.
|
||||
|
||||
There are various Pythonic ways to go about the cleaning, parsing, and calculation steps of Wordy.
|
||||
For cleaning the "question" portion of the problem, [`str.removeprefix`][removeprefix] and
|
||||
[`str.removesuffix`][removesuffix] introduced in `Python 3.9` can be very useful:
|
||||
|
||||
|
||||
```python
|
||||
>>> 'Supercalifragilisticexpialidocious'.removeprefix('Super')
|
||||
'califragilisticexpialidocious'
|
||||
|
||||
>>> 'Supercalifragilisticexpialidocious'.removesuffix('expialidocious')
|
||||
'Supercalifragilistic'
|
||||
|
||||
|
||||
#The two methods can be chained to remove both a suffix and prefix in one line.
|
||||
>>> '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:]
|
||||
>>> new_string
|
||||
'califragilisticexpialidocious'
|
||||
|
||||
|
||||
>>> 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 be used to clean up the initial word problem.
|
||||
A [regex][regex] could also be used to process the question, 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 iteration, although other strategies are also used.
|
||||
|
||||
For math operations, many solutions involve importing and using methods from the [operator][operator] module in combination with different looping, parsing, and substitution strategies.
|
||||
Some solutions use either [lambda][lambdas] expressions or [dunder/"special" methods][dunder-methods] to replace words with arithmetic operations.
|
||||
However, the exercise can be solved without using `operator`, `lambdas`, or `dunder-methods`.
|
||||
|
||||
Using [`eval`][eval] for the operations might seem convenient, but it is a [dangerous][eval-danger] and possibly [destructive][eval-destructive] approach.
|
||||
It is also entirely unnecessary, as the other methods described here are safer and equally performant.
|
||||
|
||||
|
||||
## Approach: String, List, and Dictionary Methods
|
||||
|
||||
|
||||
```python
|
||||
OPERATIONS = {"plus": '+', "minus": '-', "multiplied": '*', "divided": '/'}
|
||||
|
||||
|
||||
def answer(question):
|
||||
if not question.startswith("What is") or "cubed" in question:
|
||||
raise ValueError("unknown operation")
|
||||
|
||||
question = question.removeprefix("What is").removesuffix("?").strip()
|
||||
|
||||
if not question:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
if question.isdigit():
|
||||
return int(question)
|
||||
|
||||
formula = []
|
||||
for operation in question.split():
|
||||
if operation == 'by':
|
||||
continue
|
||||
else:
|
||||
formula.append(OPERATIONS.get(operation, operation))
|
||||
|
||||
while len(formula) > 1:
|
||||
try:
|
||||
x_value = int(formula[0])
|
||||
symbol = formula[1]
|
||||
y_value = int(formula[2])
|
||||
remainder = 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
|
||||
else:
|
||||
raise ValueError("syntax error")
|
||||
except:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
return formula[0]
|
||||
```
|
||||
|
||||
This approach uses only data structures and methods (_[dict][dict], [dict.get()][dict-get] and [list()][list]_) 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.
|
||||
As an alternative to the `formula` loop-append, a [list-comprehension][list-comprehension] can be used to create the initial parsed formula.
|
||||
|
||||
For more details and variations, read the [String, List and Dictionary Methods][approach-string-list-and-dict-methods] approach.
|
||||
|
||||
|
||||
## Approach: Import Callables from the Operator Module
|
||||
|
||||
|
||||
```python
|
||||
from operator import add, mul, sub
|
||||
from operator import floordiv as div
|
||||
|
||||
OPERATIONS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div}
|
||||
|
||||
def answer(question):
|
||||
if not question.startswith("What is") or "cubed" in question:
|
||||
raise ValueError("unknown operation")
|
||||
|
||||
question = question.removeprefix("What is").removesuffix("?").strip()
|
||||
|
||||
if question.isdigit():
|
||||
return int(question)
|
||||
|
||||
if not question:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
equation = [word for word in question.split() if word != 'by']
|
||||
|
||||
while len(equation) > 1:
|
||||
try:
|
||||
x_value, operation, y_value, *rest = equation
|
||||
equation = [OPERATIONS[operation](int(x_value), int(y_value)),
|
||||
*rest]
|
||||
except:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
return equation[0]
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
For more details and options, take a look at the [Import Callables from the Operator Module][approach-import-callables-from-operator] approach.
|
||||
|
||||
|
||||
## 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')
|
||||
}
|
||||
|
||||
|
||||
def get_number(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)
|
||||
if not pattern:
|
||||
raise ValueError("unknown operation")
|
||||
return [question.removeprefix(pattern.group(0)).lstrip(),
|
||||
OPERATIONS[pattern.group(0)]]
|
||||
|
||||
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)
|
||||
|
||||
while len(question) > 0:
|
||||
if REGEX['number'].match(question):
|
||||
raise ValueError("syntax error")
|
||||
|
||||
question, operation = get_operation(question)
|
||||
question, num = get_number(question)
|
||||
|
||||
result = operation(result, num)
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
## 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
|
||||
}
|
||||
|
||||
|
||||
def answer(question):
|
||||
if not question.startswith("What is") or "cubed" in question:
|
||||
raise ValueError("unknown operation")
|
||||
|
||||
question = question.removeprefix("What is").removesuffix("?").strip()
|
||||
|
||||
if question.isdigit():
|
||||
return int(question)
|
||||
|
||||
if not question:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
equation = [word for word in question.split() if word != 'by']
|
||||
|
||||
while len(equation) > 1:
|
||||
try:
|
||||
x_value, operation, y_value, *rest = equation
|
||||
equation = [OPERATIONS[operation](int(x_value), int(y_value)),
|
||||
*rest]
|
||||
except:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
Lambda expressions are all named `"lambda"` in stack traces, so it becomes less clear where an error is coming from if you have a number of lambda expressions within a large program.
|
||||
Since this is not a large program, debugging these `lambdas` is fairly straightforward.
|
||||
These "hand-crafted" `lambdas` could also introduce a mathematical error, although for the simple problems in Wordy, this is a fairly small consideration.
|
||||
|
||||
For more details, take a look at the [Lambdas in a Dictionary][approach-lambdas-in-a-dictionary] approach.
|
||||
|
||||
|
||||
## Approach: Recursion
|
||||
|
||||
|
||||
```python
|
||||
from operator import add, mul, sub
|
||||
from operator import floordiv as div
|
||||
|
||||
OPERATIONS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div}
|
||||
|
||||
def answer(question):
|
||||
return calculate(clean(question))
|
||||
|
||||
def clean(question):
|
||||
if not question.startswith("What is") or "cubed" in question:
|
||||
raise ValueError("unknown operation")
|
||||
|
||||
return (question.removeprefix("What is")
|
||||
.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)
|
||||
```
|
||||
|
||||
|
||||
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.
|
||||
|
||||
For more details, take a look at the [recursion][approach-recursion] approach.
|
||||
|
||||
|
||||
## Approach: functools.reduce()
|
||||
|
||||
|
||||
```python
|
||||
from operator import add, mul, sub
|
||||
from operator import floordiv as div
|
||||
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:
|
||||
raise ValueError("unknown operation")
|
||||
|
||||
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
|
||||
element in question[::2]]
|
||||
|
||||
if len(digits)-1 != len(operations) or None in digits:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
result = reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits)
|
||||
|
||||
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].
|
||||
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`.
|
||||
|
||||
For more details and variations, take a look at the [functools.reduce for Calculation][approach-functools-reduce] approach.
|
||||
|
||||
Parsing should verify that the expression in words can be translated to a valid mathematical expression.
|
||||
|
||||
## Approach: Dunder methods with `__getattribute__`
|
||||
|
||||
|
||||
```python
|
||||
OPS = {
|
||||
"plus": "__add__",
|
||||
@@ -50,11 +395,52 @@ def answer(question):
|
||||
|
||||
```
|
||||
|
||||
For more information, check the [dunder method with `__getattribute__` approach][approach-dunder-getattribute].
|
||||
This approach uses the [`dunder methods`][dunder-methods] / ["special methods"][special-methods] / "magic methods" associated with the `int()` class, using the `dunder-method` called [`<object>.__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.
|
||||
|
||||
[eval]: https://docs.python.org/3/library/functions.html?#eval
|
||||
[eval-danger]: https://diveintopython3.net/advanced-iterators.html#eval
|
||||
[dunder]: https://www.tutorialsteacher.com/python/magic-methods-in-python
|
||||
[getattribute]: https://docs.python.org/3/reference/datamodel.html?#object.__getattribute__
|
||||
[int]: https://docs.python.org/3/library/stdtypes.html#typesnumeric
|
||||
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.
|
||||
|
||||
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
|
||||
[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
|
||||
[approach-lambdas-in-a-dictionary]: https://exercise.org/tracks/python/exercises/wordy/approaches/lambdas-in-a-dictionary
|
||||
[approach-recursion]: https://exercise.org/tracks/python/exercises/wordy/approaches/recursion
|
||||
[approach-regex-with-operator-module]: https://exercise.org/tracks/python/exercises/wordy/approaches/regex-with-operator-module
|
||||
[approach-string-list-and-dict-methods]: https://exercise.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/
|
||||
[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
|
||||
[endswith]: https://docs.python.org/3.9/library/stdtypes.html#str.endswith
|
||||
[eval-danger]: https://softwareengineering.stackexchange.com/questions/311507/why-are-eval-like-features-considered-evil-in-contrast-to-other-possibly-harmfu
|
||||
[eval-destructive]: https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
|
||||
[eval]: https://docs.python.org/3/library/functions.html?#eval
|
||||
[find]: https://docs.python.org/3.9/library/stdtypes.html#str.find
|
||||
[functools-reduce]: https://docs.python.org/3/library/functools.html#functools.reduce
|
||||
[getattribute]: https://docs.python.org/3/reference/datamodel.html#object.__getattribute__
|
||||
[handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions
|
||||
[index]: https://docs.python.org/3.9/library/stdtypes.html#str.index
|
||||
[int]: https://docs.python.org/3/library/stdtypes.html#typesnumeric
|
||||
[lambdas]: https://docs.python.org/3/howto/functional.html#small-functions-and-the-lambda-expression
|
||||
[list-comprehension]: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions
|
||||
[list]: https://docs.python.org/3/library/stdtypes.html#list
|
||||
[mathematical operators]: https://www.w3schools.com/python/gloss_python_arithmetic_operators.asp
|
||||
[namespace]: https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces
|
||||
[operator]: https://docs.python.org/3/library/operator.html#module-operator
|
||||
[raise-statement]: https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement
|
||||
[regex]: https://docs.python.org/3/library/re.html#module-re
|
||||
[removeprefix]: https://docs.python.org/3.9/library/stdtypes.html#str.removeprefix
|
||||
[removesuffix]: https://docs.python.org/3.9/library/stdtypes.html#str.removesuffix
|
||||
[rfind]: https://docs.python.org/3.9/library/stdtypes.html#str.rfind
|
||||
[sequence-operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations
|
||||
[special-methods]: https://docs.python.org/3/reference/datamodel.html#specialnames
|
||||
[split]: https://docs.python.org/3.9/library/stdtypes.html#str.split
|
||||
[startswith]: https://docs.python.org/3.9/library/stdtypes.html#str.startswith
|
||||
[strip]: https://docs.python.org/3.9/library/stdtypes.html#str.strip
|
||||
[value-error]: https://docs.python.org/3.11/library/exceptions.html#ValueError
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# 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
|
||||
}
|
||||
|
||||
|
||||
def answer(question):
|
||||
if not question.startswith("What is") or "cubed" in question:
|
||||
raise ValueError("unknown operation")
|
||||
|
||||
question = question.removeprefix("What is").removesuffix("?").strip()
|
||||
|
||||
if question.isdigit():
|
||||
return int(question)
|
||||
|
||||
if not question:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
equation = question.replace("by", "").split()
|
||||
|
||||
while len(equation) > 1:
|
||||
try:
|
||||
x_value, operation, y_value, *rest = equation
|
||||
equation = [OPERATIONS[operation](int(x_value), int(y_value)),
|
||||
*rest]
|
||||
except:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
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.
|
||||
|
||||
`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.
|
||||
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`:
|
||||
|
||||
|
||||
```python
|
||||
def add_(x, y):
|
||||
return x + y
|
||||
|
||||
def mul_(x, y):
|
||||
return x * y
|
||||
|
||||
def div_(x, y):
|
||||
return x//y
|
||||
|
||||
def sub_(x, y):
|
||||
return x - y
|
||||
|
||||
def answer(question):
|
||||
operations = {'minus': sub_,'plus': add_,'multiplied': mul_,'divided': div_}
|
||||
|
||||
if not question.startswith("What is") or "cubed" in question:
|
||||
raise ValueError("unknown operation")
|
||||
|
||||
question = question.removeprefix("What is").removesuffix("?").strip()
|
||||
|
||||
if question.isdigit():
|
||||
return int(question)
|
||||
|
||||
if not question:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
equation = question.replace("by", "").split()
|
||||
|
||||
while len(equation) > 1:
|
||||
try:
|
||||
x_value, operation, y_value, *rest = equation
|
||||
equation = [operations[operation](int(x_value), int(y_value)),
|
||||
*rest]
|
||||
except:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
return equation[0]
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
[approach-import-callables-from-operator]: https://exercism.org/tracks/python/exercises/wordy/approaches/import-callables-from-operator
|
||||
[approach-string-list-and-dict-methods]: https://exercise.org/tracks/python/exercises/wordy/approaches/string-list-and-dict-methods
|
||||
[filter]: https://docs.python.org/3/library/functions.html#filter
|
||||
[functools-reduce]: https://docs.python.org/3/library/functools.html#functools.reduce
|
||||
[key-functions]: https://docs.python.org/3/howto/sorting.html#key-functions
|
||||
[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
|
||||
@@ -0,0 +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
|
||||
}
|
||||
266
exercises/practice/wordy/.approaches/recursion/content.md
Normal file
266
exercises/practice/wordy/.approaches/recursion/content.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# Recursion
|
||||
|
||||
|
||||
[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.
|
||||
|
||||
That being said, Pyton famously does not perform [tail-call optimization][tail-call-optimization], and limits recursive calls on the stack to a depth of 1000 frames, so it is important to only use recursion where you are confident that it can complete within the limit (_or something close to it_).
|
||||
[Memoization][memoization] and other strategies in [dynamic programming][dynamic-programming] can help to make recursion more efficient and "shorter" in Python, but it's always good to give it careful consideration.
|
||||
|
||||
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-firs search][dfs], and [merge sort][merge-sort]_).
|
||||
|
||||
Other algorithms such as [breadth-first search][bfs], [Dijkstra's algorithm][dijkstra], and [Bellman-Ford Algorithm][bellman-ford] lend themselves better to iteration.
|
||||
|
||||
|
||||
```python
|
||||
from operator import add, mul, sub
|
||||
from operator import floordiv as div
|
||||
|
||||
# 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()
|
||||
return calculate(clean(question))
|
||||
|
||||
def clean(question):
|
||||
# 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
|
||||
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.
|
||||
# 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)
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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 in processing or calculating without creating conflict or confusion.
|
||||
|
||||
Note that `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()` can also use many of the strategies detailed in other approaches, as long as they work with the recursion.
|
||||
|
||||
|
||||
`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:
|
||||
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.
|
||||
```
|
||||
|
||||
|
||||
## Variation 1: Use Regex for matching, cleaning, and calculating
|
||||
|
||||
|
||||
```python
|
||||
|
||||
import re
|
||||
from operator import add, mul, sub
|
||||
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.
|
||||
OPERATORS = {
|
||||
mul: re.compile(r"(?P<x>-?\d+) multiplied by (?P<y>-?\d+)"),
|
||||
div: re.compile(r"(?P<x>-?\d+) divided by (?P<y>-?\d+)"),
|
||||
add: re.compile(r"(?P<x>-?\d+) plus (?P<y>-?\d+)"),
|
||||
sub: re.compile(r"(?P<x>-?\d+) minus (?P<y>-?\d+)"),
|
||||
}
|
||||
|
||||
# This regex looks for any digit 0-9 (optionally negative) followed by any valid operation,
|
||||
# ending in any digit (optionally negative).
|
||||
VALIDATE = re.compile(r"(?P<x>-?\d+) (multiplied by|divided by|plus|minus) (?P<y>-?\d+)")
|
||||
|
||||
|
||||
def answer(question):
|
||||
if (not question.startswith( "What is") or "cubed" in question):
|
||||
raise ValueError("unknown operation")
|
||||
|
||||
question = question.removeprefix( "What is").removesuffix("?").strip()
|
||||
|
||||
# If after cleaning, there is only one number, return it as an int().
|
||||
if DIGITS.fullmatch(question):
|
||||
return int(question)
|
||||
|
||||
# If after cleaning, there isn't anything, toss an error.
|
||||
if not question:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
# Call the recursive calculate() function.
|
||||
return calculate(question)
|
||||
|
||||
# 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):
|
||||
new_question = ""
|
||||
|
||||
for symbol, pattern in OPERATORS.items():
|
||||
# Declare match variable and assign the pattern match as a value
|
||||
if match := pattern.match(question):
|
||||
|
||||
# Attempt to calculate the first num symbol num trio.
|
||||
# Convert strings to ints where needed.
|
||||
first_calc = f"{symbol(int(match['x']), int(match['y']))}"
|
||||
|
||||
# Strip the pattern from the question
|
||||
remainder = question.removeprefix(match.group())
|
||||
|
||||
# Create new question with first calculation + the remainder
|
||||
new_question = first_calc + remainder
|
||||
|
||||
# Check if there is just a single number, so that it can be returned.
|
||||
# This is the "base case" of this recursive function.
|
||||
if DIGITS.fullmatch(new_question):
|
||||
return int(new_question)
|
||||
|
||||
# Check if the new question is still a "valid" question.
|
||||
# Error out if not.
|
||||
elif not VALIDATE.match(new_question):
|
||||
raise ValueError("syntax error")
|
||||
|
||||
# Otherwise, call yourself to process the new question.
|
||||
else:
|
||||
return calculate(new_question)
|
||||
```
|
||||
|
||||
|
||||
This variation shows how the dictionary of operators from `operator` can be augmented with [regex][re] to perform string matching for a question.
|
||||
Regex are also used here to check that a question is a valid and to ensure that the base case (_nothing but digits are left in the question_) is met for the recursive call in `calculate()`.
|
||||
The regex patterns use [named groups][named-groups] for easy reference, but it's not necessary to do so.
|
||||
|
||||
|
||||
Interestingly, `calculate()` loops through `dict.items()` to find symbols, using a [walrus operator][walrus] to complete successive regex matches and composing an [f-string][f-string] to perform the calculation.
|
||||
The question remains a `str` throughout the process, so `question.removeprefix(match.group())` is used to "reduce" the original question to form a remainder that is then concatenated with the `f-string` to form a new question.
|
||||
|
||||
|
||||
Because each new iteration of the question needs to be validated, there is an `if-elif-else` block at the end that returns the answer, throws a `ValueError("syntax error")`, or makes the recursive call.
|
||||
|
||||
|
||||
Note that the `for-loop` and VALIDATE use [`re.match`][re-match], but DIGITS validation uses [`re.fullmatch`][re-fullmatch].
|
||||
|
||||
|
||||
## Variation 2: Use Regex, Recurse within the For-loop
|
||||
|
||||
|
||||
```python
|
||||
import re
|
||||
from operator import add, mul, sub
|
||||
from operator import floordiv as div
|
||||
|
||||
DIGITS = re.compile(r"-?\d+")
|
||||
OPERATORS = (
|
||||
(mul, re.compile(r"(?P<x>.*) multiplied by (?P<y>.*)")),
|
||||
(div, re.compile(r"(?P<x>.*) divided by (?P<y>.*)")),
|
||||
(add, re.compile(r"(?P<x>.*) plus (?P<y>.*)")),
|
||||
(sub, re.compile(r"(?P<x>.*) minus (?P<y>.*)")),
|
||||
)
|
||||
|
||||
def answer(question):
|
||||
if not question.startswith( "What is") or "cubed" in question:
|
||||
raise ValueError("unknown operation")
|
||||
|
||||
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.
|
||||
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_).
|
||||
|
||||
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.
|
||||
|
||||
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 would then be re-arranged to `mul(calculate("1 plus -10"), calculate("13 divided by 2"))`
|
||||
3. At this point the loop would pause as the two recursive calls to `calculate()` spawn
|
||||
4. The loops would run again — and so would the calls to `calculate()`, until there wasn't any match that caused a split of the question or an error.
|
||||
5. One at a time, the numbers would then be returned, until the main `mul(calculate("1 plus -10"), calculate("13 divided by 2"))` could be solved, at which point the answer would be returned.
|
||||
|
||||
For a more visual picture, 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
|
||||
[backtracking]: https://en.wikipedia.org/wiki/Backtracking
|
||||
[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
|
||||
[depreciation]: https://www.investopedia.com/terms/d/depreciation.asp
|
||||
[dfs]: https://en.wikipedia.org/wiki/Depth-first_search
|
||||
[dijkstra]: https://www.programiz.com/dsa/dijkstra-algorithm
|
||||
[dynamic-programming]: https://algo.monster/problems/dynamic_programming_intro
|
||||
[f-string]: https://docs.python.org/3.11/reference/lexical_analysis.html#formatted-string-literals
|
||||
[looping-vs-recursion]: https://softwareengineering.stackexchange.com/questions/303242/is-there-anything-that-can-be-done-with-recursion-that-cant-be-done-with-loops
|
||||
[memoization]: https://inventwithpython.com/recursion/chapter7.html
|
||||
[merge-sort]: https://www.digitalocean.com/community/tutorials/merge-sort-algorithm-java-c-python
|
||||
[named-groups]: https://docs.python.org/3/howto/regex.html#non-capturing-and-named-groups
|
||||
[nuclei-decay]: https://courses.lumenlearning.com/suny-physics/chapter/31-5-half-life-and-activity/
|
||||
[re-fullmatch]: https://docs.python.org/3/library/re.html#re.full-match
|
||||
[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-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
|
||||
[walrus]: https://docs.python.org/3/reference/expressions.html#grammar-token-python-grammar-assignment_expression
|
||||
@@ -0,0 +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)
|
||||
@@ -0,0 +1,98 @@
|
||||
# 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')
|
||||
}
|
||||
|
||||
# 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.
|
||||
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
|
||||
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.
|
||||
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
|
||||
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
|
||||
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.
|
||||
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.
|
||||
|
||||
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".
|
||||
|
||||
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].
|
||||
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.
|
||||
|
||||
Once there is no more question to process, `result` is returned as the answer.
|
||||
|
||||
[callable]: https://treyhunner.com/2019/04/is-it-a-class-or-a-function-its-a-callable/
|
||||
[lstrip]: https://docs.python.org/3/library/stdtypes.html#str.lstrip
|
||||
[removeprefix]: https://docs.python.org/3.9/library/stdtypes.html#str.removeprefix
|
||||
[removesuffix]: https://docs.python.org/3.9/library/stdtypes.html#str.removesuffix
|
||||
[unpacking]: https://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/
|
||||
@@ -0,0 +1,8 @@
|
||||
while len(question) > 0:
|
||||
if REGEX['number'].match(question):
|
||||
raise ValueError("syntax error")
|
||||
|
||||
question, operation = get_operation(question)
|
||||
question, num = get_number(question)
|
||||
|
||||
result = operation(result, num)
|
||||
@@ -0,0 +1,161 @@
|
||||
# String, List, and Dictionary Methods
|
||||
|
||||
|
||||
```python
|
||||
OPERATIONS = {"plus": '+', "minus": '-', "multiplied": '*', "divided": '/'}
|
||||
|
||||
|
||||
def answer(question):
|
||||
if not question.startswith("What is") or "cubed" in question:
|
||||
raise ValueError("unknown operation")
|
||||
|
||||
question = question.removeprefix("What is").removesuffix("?").strip()
|
||||
|
||||
if not question:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
formula = []
|
||||
for operation in question.split():
|
||||
if operation == 'by':
|
||||
continue
|
||||
else:
|
||||
formula.append(OPERATIONS.get(operation, operation))
|
||||
|
||||
while len(formula) > 1:
|
||||
try:
|
||||
x_value, y_value = int(formula[0]), int(formula[2])
|
||||
symbol = formula[1]
|
||||
remainder = 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
|
||||
else:
|
||||
raise ValueError("syntax error")
|
||||
except:
|
||||
raise ValueError("syntax error")
|
||||
|
||||
return int(formula[0])
|
||||
```
|
||||
|
||||
Within the `answer()` function, the question is first checked for "unknown operations" by validating that it starts with "What is" ([`str.startswith`][startswith], [`str.endswith`][endswith]) and does not include the word "cubed" (_which is an invalid operation_).
|
||||
This eliminates all the [current cases][unknown-operation-tests] where a [`ValueError("unknown operation")`][value-error] needs to be [raised][raise-statement].
|
||||
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]) and [stripping][strip] any leading or trailing whitespaces.
|
||||
|
||||
|
||||
If the question is now an empty string, a `ValueError("syntax error")` is raised.
|
||||
|
||||
|
||||
Next, the question is [split][split] into a `list` and iterated over, with each element looked up and replaced from the OPERATIONS dictionary.
|
||||
The [`dict.get`][dict-get] method is used for this, as it takes a 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 avoids error handling, extra logic, or interruption when an element is not found.
|
||||
One exception here is the word "by", which is explicitly skipped within the `for-loop`, so that it doesn't appear in the formula to be processed.
|
||||
This filtering out could also be accomplished by using [`str.replace`][str-replace] in the cleaning step or during the `split` step.
|
||||
The results of iterating through the question are then appended to a new formula `list`.
|
||||
|
||||
|
||||
|
||||
````exercism/note
|
||||
There are a couple of common alternatives to the `loop-append`:
|
||||
|
||||
1. [`list-comprehensions`][list-comprehension] duplicate the same process in a more succinct and declarative fashion:
|
||||
```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:
|
||||
|
||||
```python
|
||||
formula = list(map(lambda x : OPERATIONS.get(x, x),
|
||||
filter(lambda x: x != "by", question.split())))
|
||||
```
|
||||
|
||||
[list-comprehension]: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions
|
||||
[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
|
||||
````
|
||||
|
||||
|
||||
After the formula `list` is composed, it is processed in a `while-loop`.
|
||||
|
||||
The processing within the `loop` is wrapped in a [try-except][handling-exceptions] block to trap any errors and raise them as `ValueError("syntax error")`.
|
||||
While each type of error could be checked for individually, it is not necessary since only `ValueError("syntax error")` is required here.
|
||||
|
||||
1. `x_value` and `y_value` are assigned to the first element and third element of the list using [bracket notation][bracket-notation], and converted to integers.
|
||||
- Rather than indexing and slicing, [concept: unpacking and multiple assignment](/tracks/python/concepts/unpacking-and-multiple-assignment) can be used to assign the variables.
|
||||
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.
|
||||
|
||||
...
|
||||
if symbol == "+":
|
||||
formula = [int(x_value) + int(y_value)] + remainder # <-- Instead, conversion to int() must happen here.
|
||||
...
|
||||
|
||||
return int(formula[0])
|
||||
```
|
||||
|
||||
2. `symbol` is assigned to the second element of the list.
|
||||
3. `remainder` is assigned to a [slice][list-slice] of everything else in the `list`.
|
||||
|
||||
The `symbol` is then tested in the `if-elif-else` block and the formula `list` is modified by calculating the operation on `x_value` and `y_value` and then appending whatever part of the question remains.
|
||||
|
||||
Once the formula `list` is calculated down to a number, that number is converted to an `int` and returned as the answer.
|
||||
|
||||
|
||||
````exercism/note
|
||||
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`.
|
||||
In some circumstances, this could be easier to read and/or reason about:
|
||||
|
||||
|
||||
```python
|
||||
while len(formula) > 1:
|
||||
try:
|
||||
x_value, symbol, y_value, *remainder = formula
|
||||
|
||||
match symbol:
|
||||
case "+":
|
||||
formula = [int(x_value) + int(y_value)] + remainder
|
||||
case "-":
|
||||
formula = [int(x_value) - int(y_value)] + remainder
|
||||
case "*":
|
||||
formula = [int(x_value) * int(y_value)] + remainder
|
||||
case "/":
|
||||
formula = [int(x_value) / int(y_value)] + remainder
|
||||
case _:
|
||||
raise ValueError("syntax error")
|
||||
except:
|
||||
raise ValueError("syntax error")
|
||||
```
|
||||
|
||||
[structural-pattern-matching]: https://peps.python.org/pep-0636/
|
||||
````
|
||||
|
||||
[bracket-notation]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations
|
||||
[dict-get]: https://docs.python.org/3/library/stdtypes.html#dict.get
|
||||
[endswith]: https://docs.python.org/3.9/library/stdtypes.html#str.endswith
|
||||
[handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions
|
||||
[keyerror]: https://docs.python.org/3/library/exceptions.html#KeyError
|
||||
[list-slice]: https://www.pythonmorsels.com/slicing/
|
||||
[raise-statement]: https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement
|
||||
[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
|
||||
[startswith]: https://docs.python.org/3.9/library/stdtypes.html#str.startswith
|
||||
[str-replace]: https://docs.python.org/3/library/stdtypes.html#str.replace
|
||||
[strip]: https://docs.python.org/3.9/library/stdtypes.html#str.strip
|
||||
[unknown-operation-tests]: https://github.com/exercism/python/blob/main/exercises/practice/wordy/wordy_test.py#L58-L68
|
||||
[value-error]: https://docs.python.org/3.11/library/exceptions.html#ValueError
|
||||
@@ -0,0 +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
|
||||
else: raise ValueError("syntax error")
|
||||
except: raise ValueError("syntax error")
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
## Exception messages
|
||||
|
||||
Sometimes it is necessary to [raise an exception](https://docs.python.org/3/tutorial/errors.html#raising-exceptions). When you do this, you should always include a **meaningful error message** to indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. For situations where you know that the error source will be a certain type, you can choose to raise one of the [built in error types](https://docs.python.org/3/library/exceptions.html#base-classes), but should still include a meaningful message.
|
||||
Sometimes it is necessary to [raise an exception][raise-an-exception]. When you do this, you should always include a **meaningful error message** to indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. For situations where you know that the error source will be a certain type, you can choose to raise one of the [built in error types][built-in-errors], but should still include a meaningful message.
|
||||
|
||||
This particular exercise requires that you use the [raise statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) to "throw" a `ValueError` if the question passed to `answer()` is malformed/invalid, or contains an unknown operation. The tests will only pass if you both `raise` the `exception` and include a message with it.
|
||||
This particular exercise requires that you use the [raise statement][raise-statement] to "throw" a `ValueError` if the question passed to `answer()` is malformed/invalid, or contains an unknown operation. The tests will only pass if you both `raise` the `exception` and include a message with it.
|
||||
|
||||
To raise a [`ValueError`][value-error] with a message, write the message as an argument to the `exception` type:
|
||||
|
||||
To raise a `ValueError` with a message, write the message as an argument to the `exception` type:
|
||||
|
||||
```python
|
||||
# if the question contains an unknown operation.
|
||||
@@ -15,3 +16,22 @@ raise ValueError("unknown operation")
|
||||
# if the question is malformed or invalid.
|
||||
raise ValueError("syntax error")
|
||||
```
|
||||
|
||||
To _handle_ a raised error within a particular code block, one can use a [try-except][handling-exceptions] :
|
||||
|
||||
```python
|
||||
while len(equation) > 1:
|
||||
try:
|
||||
x_value, operation, y_value, *rest = equation
|
||||
|
||||
...
|
||||
|
||||
except:
|
||||
raise ValueError("syntax error")
|
||||
```
|
||||
|
||||
[built-in-errors]: https://docs.python.org/3.11/library/exceptions.html#built-in-exceptions
|
||||
[handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions
|
||||
[raise-an-exception]: https://docs.python.org/3/tutorial/errors.html#raising-exceptions
|
||||
[raise-statement]: https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement
|
||||
[value-error]: https://docs.python.org/3.11/library/exceptions.html#ValueError
|
||||
|
||||
Reference in New Issue
Block a user