Skip to content

Commit

Permalink
[Wordy & Wordy Approaches]: Added 6 Additional Approaches & Modified …
Browse files Browse the repository at this point in the history
…the Instruction Append for Wordy. (#3783)

* Added 6 additional appraches and extended introduction for Wordy.

* Corrected slug for regex with operator module approach.
  • Loading branch information
BethanyG authored Oct 12, 2024
1 parent 3dcbf4c commit fb1cb44
Show file tree
Hide file tree
Showing 16 changed files with 1,392 additions and 63 deletions.
46 changes: 44 additions & 2 deletions exercises/practice/wordy/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,52 @@
{
"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",
"title": "dunder with __getattribute__",
Expand Down
78 changes: 35 additions & 43 deletions exercises/practice/wordy/.approaches/dunder-getattribute/content.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Dunder methods with `__getattribute__`


```python
OPS = {
"plus": "__add__",
Expand Down Expand Up @@ -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))`.

~~~~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.
~~~~
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.

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.

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.
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.

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")`
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.

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`.
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")`.

It sets the list to the result of the dunder method plus the remaining elements in `*tail`.
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`.

~~~~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.

[const]: https://realpython.com/python-constants/
[dictionaries]: https://docs.python.org/3/tutorial/datastructures.html#dictionaries
[dir-vs-__dict__]: https://stackoverflow.com/a/14361362
[dunder]: https://www.tutorialsteacher.com/python/magic-methods-in-python
[exception-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
[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
[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 changes: 126 additions & 0 deletions exercises/practice/wordy/.approaches/functools-reduce/content.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit fb1cb44

Please sign in to comment.