Skip to content

Commit fb1cb44

Browse files
authored
[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.
1 parent 3dcbf4c commit fb1cb44

File tree

16 files changed

+1392
-63
lines changed

16 files changed

+1392
-63
lines changed

exercises/practice/wordy/.approaches/config.json

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,52 @@
11
{
22
"introduction": {
3-
"authors": ["bobahop"],
4-
"contributors": []
3+
"authors": ["BethanyG"],
4+
"contributors": ["bobahop"]
55
},
66
"approaches": [
77
{
8+
"uuid": "4eeb0638-671a-4289-a83c-583b616dc698",
9+
"slug": "string-list-and-dict-methods",
10+
"title": "String, List, and Dictionary Methods",
11+
"blurb": "Use Core Python Features to Solve Word Problems.",
12+
"authors": ["BethanyG"]
13+
},
14+
{
15+
"uuid": "d3ff485a-defe-42d9-b9c6-c38019221ffa",
16+
"slug": "import-callables-from-operator",
17+
"title": "Import Callables from the Operator Module",
18+
"blurb": "Use Operator Module Methods to Solve Word Problems.",
19+
"authors": ["BethanyG"]
20+
},
21+
{
22+
"uuid": "61f44943-8a12-471b-ab15-d0d10fa4f72f",
23+
"slug": "regex-with-operator-module",
24+
"title": "Regex with the Operator Module",
25+
"blurb": "Use Regex with the Callables from Operator to solve word problems.",
26+
"authors": ["BethanyG"]
27+
},
28+
{
29+
"uuid": "46bd15dd-cae4-4eb3-ac63-a8b631a508d1",
30+
"slug": "lambdas-in-a-dictionary",
31+
"title": "Lambdas in a Dictionary to Return Functions",
32+
"blurb": "Use lambdas in a dictionary to return functions for solving word problems.",
33+
"authors": ["BethanyG"]
34+
},
35+
{
36+
"uuid": "2e643b88-9b76-45a1-98f4-b211919af061",
37+
"slug": "recursion",
38+
"title": "Recursion for iteration.",
39+
"blurb": "Use recursion with other strategies to solve word problems.",
40+
"authors": ["BethanyG"]
41+
},
42+
{
43+
"uuid": "1e136304-959c-4ad1-bc4a-450d13e5f668",
44+
"slug": "functools-reduce",
45+
"title": "Functools.reduce for Calculation",
46+
"blurb": "Use functools.reduce with other strategies to calculate solutions.",
47+
"authors": ["BethanyG"]
48+
},
49+
{
850
"uuid": "d643e2b4-daee-422d-b8d3-2cad2f439db5",
951
"slug": "dunder-getattribute",
1052
"title": "dunder with __getattribute__",
Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Dunder methods with `__getattribute__`
22

3+
34
```python
45
OPS = {
56
"plus": "__add__",
@@ -33,70 +34,61 @@ def answer(question):
3334

3435
```
3536

36-
This approach begins by defining a [dictionary][dictionaries] of the word keys with their related [dunder][dunder] methods.
37+
This approach begins by defining a [dictionary][dictionaries] of the word keys with their related [`dunder-methods`][dunder] methods.
38+
Since only whole numbers are involved, the available `dunder-methods` are those for the [`int`][int] class/namespace.
39+
The supported methods for the `int()` namespace can be found by using `print(dir(int))` or `print(int.__dict__)` in a Python terminal.
40+
See [SO: Difference between dir() and __dict__][dir-vs-__dict__] for differences between the two.
3741

3842
~~~~exercism/note
39-
They are called "dunder" methods because they have **d**ouble **under**scores at the beginning and end of the method name.
40-
They are also called magic methods.
43+
The built-in [`dir`](https://docs.python.org/3/library/functions.html?#dir) function returns a list of all valid attributes for an object.
44+
The `dunder-method` [`<object>.__dict__`](https://docs.python.org/3/reference/datamodel.html#object.__dict__) is a mapping of an objects writable attributes.
4145
~~~~
4246

43-
Since only whole numbers are involved, the dunder methods are those for [`int`][int].
44-
The supported methods for `int` can be found by using `print(dir(int))`.
45-
46-
~~~~exercism/note
47-
The built-in [`dir`](https://docs.python.org/3/library/functions.html?#dir) function returns a list of valid attributes for an object.
48-
~~~~
47+
The `OPS` dictionary is defined with all uppercase letters, which is the naming convention for a Python [constant][const].
48+
It indicates that the value should not be changed.
4949

50-
Python doesn't _enforce_ having real constant values,
51-
but the `OPS` dictionary is defined with all uppercase letters, which is the naming convention for a Python [constant][const].
52-
It indicates that the value is not intended to be changed.
53-
54-
The input question to the `answer` function is cleaned using the [`removeprefix`][removeprefix], [`removesuffix`][removesuffix], and [`strip`][strip] methods.
50+
The input question to the `answer()` function is cleaned using the [`removeprefix`][removeprefix], [`removesuffix`][removesuffix], and [`strip`][strip] string methods.
5551
The method calls are [chained][method-chaining], so that the output from one call is the input for the next call.
5652
If the input has no characters left,
57-
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.
53+
it uses the [falsiness][falsiness] of an empty string with the [`not`][not] operator to return a `ValueError("syntax error")`.
5854

59-
Next, the [`isdigit`][isdigit] method is used to see if all of the remaining characters in the input are digits.
55+
Next, the [`isdigit`][isdigit] method is used to see if the remaining characters in the input are digits.
6056
If so, it uses the [`int()`][int-constructor] constructor to return the string as an integer.
6157

62-
Next, the elements in the `OPS` dictionary are iterated.
63-
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.
64-
If none of the key names are found in the input, then a `ValueError` is returned for having an unknown operation.
65-
66-
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.
58+
Next, the elements in the `OPS` dictionary are iterated over.
59+
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.
60+
If none of the key names are found in the input, a `ValueError("unknown operation")` is returned.
6761

68-
Within a [try][exception-handling], the list is [destructured][destructure] into `x, op, y, *tail`.
69-
If `op` is not in the supported dunder methods, it raises `ValueError("syntax error")`.
70-
If there are any other exceptions raised in the try, `except` raises `ValueError("syntax error")`
62+
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.
7163

72-
Next, it converts `x` to an `int` and calls the [`__getattribute__`][getattribute] for its dunder method and calls it,
73-
passing it `y` converted to an `int`.
64+
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`.
65+
If `op` is not in the supported `dunder-methods` dictionary, a `ValueError("syntax error")` is raised.
66+
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")`.
7467

75-
It sets the list to the result of the dunder method plus the remaining elements in `*tail`.
68+
Next, `x` is converted to an `int` and [`__getattribute__`][getattribute] is called for the `dunder-method` (`op`) to apply to `x`.
69+
`y` is then converted to an `int` and passed as the second arguemnt to `op`.
7670

77-
~~~~exercism/note
78-
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.
79-
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.
80-
~~~~
71+
Then `ret` is redefined to a `list` containing the result of the dunder method plus the remaining elements in `*tail`.
8172

8273
When the loop exhausts, the first element of the list is selected as the function return value.
8374

75+
[const]: https://realpython.com/python-constants/
8476
[dictionaries]: https://docs.python.org/3/tutorial/datastructures.html#dictionaries
77+
[dir-vs-__dict__]: https://stackoverflow.com/a/14361362
8578
[dunder]: https://www.tutorialsteacher.com/python/magic-methods-in-python
79+
[exception-handling]: https://docs.python.org/3/tutorial/errors.html#handling-exceptions
80+
[falsiness]: https://www.pythontutorial.net/python-basics/python-boolean/
81+
[getattribute]: https://docs.python.org/3/reference/datamodel.html?#object.__getattribute__
82+
[int-constructor]: https://docs.python.org/3/library/functions.html?#int
8683
[int]: https://docs.python.org/3/library/stdtypes.html#typesnumeric
87-
[const]: https://realpython.com/python-constants/
88-
[removeprefix]: https://docs.python.org/3/library/stdtypes.html#str.removeprefix
89-
[removesuffix]: https://docs.python.org/3/library/stdtypes.html#str.removesuffix
90-
[strip]: https://docs.python.org/3/library/stdtypes.html#str.strip
84+
[isdigit]: https://docs.python.org/3/library/stdtypes.html?#str.isdigit
85+
[len]: https://docs.python.org/3/library/functions.html?#len
9186
[method-chaining]: https://www.tutorialspoint.com/Explain-Python-class-method-chaining
9287
[not]: https://docs.python.org/3/library/operator.html?#operator.__not__
93-
[falsiness]: https://www.pythontutorial.net/python-basics/python-boolean/
94-
[value-error]: https://docs.python.org/3/library/exceptions.html?#ValueError
95-
[isdigit]: https://docs.python.org/3/library/stdtypes.html?#str.isdigit
96-
[int-constructor]: https://docs.python.org/3/library/functions.html?#int
88+
[removeprefix]: https://docs.python.org/3/library/stdtypes.html#str.removeprefix
89+
[removesuffix]: https://docs.python.org/3/library/stdtypes.html#str.removesuffix
9790
[replace]: https://docs.python.org/3/library/stdtypes.html?#str.replace
9891
[split]: https://docs.python.org/3/library/stdtypes.html?#str.split
99-
[len]: https://docs.python.org/3/library/functions.html?#len
100-
[exception-handling]: https://docs.python.org/3/tutorial/errors.html#handling-exceptions
101-
[destructure]: https://riptutorial.com/python/example/14981/destructuring-assignment
102-
[getattribute]: https://docs.python.org/3/reference/datamodel.html?#object.__getattribute__
92+
[strip]: https://docs.python.org/3/library/stdtypes.html#str.strip
93+
[unpacking]: https://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/
94+
[unpacking-and-multiple-assignment]: https://exercism.org/tracks/python/concepts/unpacking-and-multiple-assignment
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Functools.reduce for Calculation
2+
3+
4+
```python
5+
from operator import add, mul, sub
6+
from operator import floordiv as div
7+
from functools import reduce
8+
9+
10+
# Define a lookup table for mathematical operations
11+
OPERATORS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div}
12+
13+
def answer(question):
14+
# Check for basic validity right away, and fail out with error if not valid.
15+
if not question.startswith( "What is") or "cubed" in question:
16+
raise ValueError("unknown operation")
17+
18+
# Using the built-in filter() to clean & split the question..
19+
list(filter(lambda x:
20+
x not in ("What", "is", "by"),
21+
question.strip("?").split()))
22+
23+
# Separate candidate operators and numbers into two lists.
24+
operations = question[1::2]
25+
26+
# Convert candidate elements to int(), checking for "-".
27+
# All other values are replaced with None.
28+
digits = [int(element) if (element.isdigit() or element[1:].isdigit())
29+
else None for element in question[::2]]
30+
31+
# If there is a mis-match between operators and numbers, toss error.
32+
if len(digits)-1 != len(operations) or None in digits:
33+
raise ValueError("syntax error")
34+
35+
# Evaluate the expression from left to right using functools.reduce().
36+
# Look up each operation in the operation dictionary.
37+
return reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits)
38+
```
39+
40+
This approach replaces the `while-loop` or `recursion` used in many solutions with a call to [`functools.reduce`][functools-reduce].
41+
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)_).
42+
43+
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.
44+
However, this could easily be accomplished by either using [chained][method-chaining] string methods or a `list-comprehension`:
45+
46+
47+
```python
48+
# Alternative 1 is chaining various string methods together.
49+
# The wrapping () invoke implicit concatenation for the chained functions
50+
return (question.removeprefix("What is")
51+
.removesuffix("?")
52+
.replace("by", "")
53+
.strip()).split() # <-- this split() turns the string into a list.
54+
55+
56+
# Alternative 2 to the nested calls to filter and split is to use a list-comprehension:
57+
return [item for item in
58+
question.strip("?").split()
59+
if item not in ("What", "is", "by")] #<-- The [] of the comprehension invokes implicit concatenation.
60+
```
61+
62+
63+
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.
64+
By that definition, the operators `list` is 1 shorter in `len()` than the digits list.
65+
Anything else (_or having None/an unknown operation in the operations list_) is a `ValueError("syntax error")`.
66+
67+
68+
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.
69+
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()`.
70+
71+
`functools.reduce` is certainly convenient, and makes the solution much shorter.
72+
But it is also hard to understand what is happening if you have not worked with a reduce or foldl function in the past.
73+
It could be argued that writing the code as a `while-loop` or recursive function is easier to reason about for non-functional programmers.
74+
75+
76+
## Variation: 1: Use a Dictionary of `lambdas` instead of importing from operator
77+
78+
79+
The imports from operator can be swapped out for a dictionary of `lambda-expressions` (or calls to `dunder-methods`), if so desired.
80+
The same cautions apply here as were discussed in the [lambdas in a dictionary][approach-lambdas-in-a-dictionary] approach:
81+
82+
83+
```python
84+
from functools import reduce
85+
86+
# Define a lookup table for mathematical operations
87+
OPERATORS = {"plus": lambda x, y: x + y,
88+
"minus": lambda x, y: x - y,
89+
"multiplied": lambda x, y: x * y,
90+
"divided": lambda x, y: x / y}
91+
92+
def answer(question):
93+
94+
# Check for basic validity right away, and fail out with error if not valid.
95+
if not question.startswith( "What is") or "cubed" in question:
96+
raise ValueError("unknown operation")
97+
98+
# Clean and split the question into a list for processing.
99+
question = [item for item in
100+
question.strip("?").split() if
101+
item not in ("What", "is", "by")]
102+
103+
# Separate candidate operators and numbers into two lists.
104+
operations = question[1::2]
105+
106+
# Convert candidate elements to int(), checking for "-".
107+
# All other values are replaced with None.
108+
digits = [int(element) if (element.isdigit() or element[1:].isdigit())
109+
else None for element in question[::2]]
110+
111+
# If there is a mis-match between operators and numbers, toss error.
112+
if len(digits)-1 != len(operations) or None in digits:
113+
raise ValueError("syntax error")
114+
115+
# Evaluate the expression from left to right using functools.reduce().
116+
# Look up each operation in the operation dictionary.
117+
result = reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits)
118+
119+
return result
120+
```
121+
122+
[approach-lambdas-in-a-dictionary]: https://exercise.org/tracks/python/exercises/wordy/approaches/lambdas-in-a-dictionary
123+
[callable]: https://treyhunner.com/2019/04/is-it-a-class-or-a-function-its-a-callable/
124+
[functools-reduce]: https://docs.python.org/3/library/functools.html#functools.reduce
125+
[method-chaining]: https://www.tutorialspoint.com/Explain-Python-class-method-chaining
126+
[sequence-operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
OPERATORS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div}
2+
3+
operations = question[1::2]
4+
digits = [int(element) if (element.isdigit() or element[1:].isdigit())
5+
else None for element in question[::2]]
6+
...
7+
return reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits)

0 commit comments

Comments
 (0)