Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Wordy & Wordy Approaches]: Added 6 Additional Approaches & Modified the Instruction Append for Wordy. #3783

Merged
merged 2 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading