Skip to content
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
61 changes: 34 additions & 27 deletions exercises/practice/wordy/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -1,57 +1,64 @@
{
"introduction": {
"authors": ["BethanyG"],
"contributors": ["bobahop"]
"contributors": ["bobahop", "yrahcaz7"]
},
"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"]
"authors": ["BethanyG"],
"contributors": ["yrahcaz7"]
},
{
"uuid": "d3ff485a-defe-42d9-b9c6-c38019221ffa",
{
"uuid": "d3ff485a-defe-42d9-b9c6-c38019221ffa",
"slug": "import-callables-from-operator",
"title": "Import Callables from the Operator Module",
"blurb": "Use Operator Module Methods to Solve Word Problems.",
"authors": ["BethanyG"]
},
{
"uuid": "61f44943-8a12-471b-ab15-d0d10fa4f72f",
"authors": ["BethanyG"],
"contributors": ["yrahcaz7"]
},
{
"uuid": "61f44943-8a12-471b-ab15-d0d10fa4f72f",
"slug": "regex-with-operator-module",
"title": "Regex with the Operator Module",
"blurb": "Use Regex with the Callables from Operator to solve word problems.",
"authors": ["BethanyG"]
},
{
"uuid": "46bd15dd-cae4-4eb3-ac63-a8b631a508d1",
"authors": ["BethanyG"],
"contributors": ["yrahcaz7"]
},
{
"uuid": "46bd15dd-cae4-4eb3-ac63-a8b631a508d1",
"slug": "lambdas-in-a-dictionary",
"title": "Lambdas in a Dictionary to Return Functions",
"blurb": "Use lambdas in a dictionary to return functions for solving word problems.",
"authors": ["BethanyG"]
},
{
"uuid": "2e643b88-9b76-45a1-98f4-b211919af061",
"authors": ["BethanyG"],
"contributors": ["yrahcaz7"]
},
{
"uuid": "2e643b88-9b76-45a1-98f4-b211919af061",
"slug": "recursion",
"title": "Recursion for Iteration.",
"title": "Recursion for Iteration",
"blurb": "Use recursion with other strategies to solve word problems.",
"authors": ["BethanyG"]
},
{
"uuid": "1e136304-959c-4ad1-bc4a-450d13e5f668",
"authors": ["BethanyG"],
"contributors": ["yrahcaz7"]
},
{
"uuid": "1e136304-959c-4ad1-bc4a-450d13e5f668",
"slug": "functools-reduce",
"title": "Functools.reduce for Calculation",
"title": "functools.reduce for Calculation",
"blurb": "Use functools.reduce with other strategies to calculate solutions.",
"authors": ["BethanyG"]
},
{
"authors": ["BethanyG"],
"contributors": ["yrahcaz7"]
},
{
"uuid": "d643e2b4-daee-422d-b8d3-2cad2f439db5",
"slug": "dunder-getattribute",
"title": "dunder with __getattribute__",
"title": "Dunder with __getattribute__",
"blurb": "Use dunder methods with __getattribute__.",
"authors": ["bobahop"]
"authors": ["bobahop"],
"contributors": ["yrahcaz7"]
}
]
}
54 changes: 28 additions & 26 deletions exercises/practice/wordy/.approaches/dunder-getattribute/content.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Dunder methods with `__getattribute__`


```python
OPS = {
"plus": "__add__",
Expand All @@ -12,42 +11,44 @@ OPS = {

def answer(question):
question = question.removeprefix("What is").removesuffix("?").strip()
if not question: raise ValueError("syntax error")
if not question:
raise ValueError("syntax error")

if question.startswith("-") and question[1:].isdigit():
return -int(question[1:])
elif question.isdigit():
if question.isdigit():
return int(question)

found_op = False
for name, op in OPS.items():
if name in question:
question = question.replace(name, op)
found_op = True
if not found_op: raise ValueError("unknown operation")
if not found_op:
raise ValueError("unknown operation")

ret = question.split()
while len(ret) > 1:
try:
x, op, y, *tail = ret
if op not in OPS.values(): raise ValueError("syntax error")
if op not in OPS.values():
raise ValueError("syntax error")
ret = [int(x).__getattribute__(op)(int(y)), *tail]
except:
raise ValueError("syntax error")
return ret[0]

```

This approach begins by defining a [dictionary][dictionaries] of the word keys with their related [`dunder-methods`][dunder] methods.
Since only whole numbers are involved, the available `dunder-methods` are those for the [`int`][int] class/namespace.
This approach begins by defining a [dictionary][dictionaries] of the word keys with their related [dunder method][dunder] values.
Since only whole numbers are involved, the available dunder methods are those for the [`int`][int] class/namespace.
The supported methods for the `int()` namespace can be found by using `print(dir(int))` or `print(int.__dict__)` in a Python terminal.
See [`SO: Difference between dir() and __dict__`][dir-vs-__dict__] for more details.
See [this StackOverflow post][dir-vs-__dict__] for more details.

<br>

~~~~exercism/note
The built-in [`dir`](https://docs.python.org/3/library/functions.html?#dir) function returns a list of all valid attributes for an object.
The `dunder-method` [`<object>.__dict__`](https://docs.python.org/3/reference/datamodel.html#object.__dict__) is a mapping of an objects writable attributes.
The dunder method [`<object>.__dict__`](https://docs.python.org/3/reference/datamodel.html#object.__dict__) is a mapping of an object's writable attributes.
~~~~

<br>
Expand All @@ -56,35 +57,37 @@ The `OPS` dictionary is defined with all uppercase letters, which is the naming
It indicates that the value should not be changed.

The input question to the `answer()` function is cleaned using the [`removeprefix`][removeprefix], [`removesuffix`][removesuffix], and [`strip`][strip] string methods.
The method calls are [chained][method-chaining], so that the output from one call is the input for the next call.
If the input has no characters left,
it uses the [falsiness][falsiness] of an empty string with the [`not`][not] operator to return a `ValueError("syntax error")`.
The method calls are [chained][method-chaining], so the output from one call is the input for the next call.
If the input has no characters left, it uses the [falsiness][falsiness] of an empty string with the [`not`][not] operator to return a `ValueError("syntax error")`.

Next, the [`str.startswith()`][startswith] and [`isdigit`][isdigit] methods are used to see if the remaining characters in the input are either negative or positive digits.
Because "-" is used to denote negative numbers, `str.startswith("-")` is used in the first condition and `question[1:].isdigit()` is then used for the remaining string.
If the `str.isdigit()` checks pass, the [`int()`][int-constructor] constructor is used to return the string as an integer with the proper sign.
Next, the [`str.startswith()`][startswith] and [`str.isdigit()`][isdigit] methods are used to see if the remaining characters in the input are either negative or positive digits.
Because "-" is used to denote negative numbers, `str.startswith("-")` is used in the first condition and `question[1:].isdigit()` is used for the remaining string.
If the `str.isdigit()` checks pass, the [`int()` constructor][int-constructor] is used to return the string as an integer with the proper sign.

Next, the elements in the `OPS` dictionary are iterated over.
If the key name is in the input, then the [`str.replace`][replace] method is used to replace the name in the input with the `dunder-method` value.
If none of the key names are found in the input, a `ValueError("unknown operation")` is returned.
If the key name is in the input, the [`str.replace`][replace] method is used to replace the name in the input with the dunder method value.
If none of the key names are found in the input, a `ValueError("unknown operation")` is raised.

At this point, the input question is [`split()`][split] into a `list` of its words, which is then iterated over while its [`len()`][len] is greater than 1.

At this point the input question is [`split()`][split] into a `list` of its words, which is then iterated over while its [`len()`][len] is greater than 1.
Within a [`try-except`][exception-handling] block, the list is [unpacked][unpacking] (_see also [concept:python/unpacking-and-multiple-assignment]()_) into the variables `x`, `op`, `y`, and `*tail`.
If `op` is not in the `OPS` dictionary, a `ValueError("syntax error")` is raised.

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")`.
The `except` block will catch this error (or any other error raised inside the `try` block), and `raise` a `ValueError("syntax error")` instead.
(You can look at [exception chaining in the Python docs][exception-chaining] for further detail on this subject.)

Next, `x` is converted to an `int` and [`__getattribute__`][getattribute] is called for the `dunder-method` (`op`) to apply to `x`.
`y` is then converted to an `int` and passed as the second arguemnt to `op`.
Next, `x` is converted to an `int` and [`__getattribute__`][getattribute] is called for the dunder method (`op`) to apply to `x`.
`y` is then converted to an `int` and passed as the second argument to `op`.

Then `ret` is redefined to a `list` containing the result of the dunder method plus the remaining elements in `*tail`.

When the loop exhausts, the first element of the list is selected as the function return value.
When `ret` reaches `len() == 1` and the loop ends, the first element of `ret` is returned as the answer.

[const]: https://realpython.com/python-constants/
[dictionaries]: https://docs.python.org/3/tutorial/datastructures.html#dictionaries
[dir-vs-__dict__]: https://stackoverflow.com/a/14361362
[dunder]: https://www.tutorialsteacher.com/python/magic-methods-in-python
[exception-chaining]: https://docs.python.org/3/tutorial/errors.html#exception-chaining
[exception-handling]: https://docs.python.org/3/tutorial/errors.html#handling-exceptions
[falsiness]: https://www.pythontutorial.net/python-basics/python-boolean/
[getattribute]: https://docs.python.org/3/reference/datamodel.html?#object.__getattribute__
Expand All @@ -101,4 +104,3 @@ When the loop exhausts, the first element of the list is selected as the functio
[strip]: https://docs.python.org/3/library/stdtypes.html#str.strip
[startswith]: https://docs.python.org/3/library/stdtypes.html#str.startswith
[unpacking]: https://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/
[unpacking-and-multiple-assignment]: https://exercism.org/tracks/python/concepts/unpacking-and-multiple-assignment
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ while len(ret) > 1:
ret = [int(x).__getattribute__(op)(int(y)), *tail]
except:
raise ValueError("syntax error")
return ret[0]
return ret[0]
90 changes: 43 additions & 47 deletions exercises/practice/wordy/.approaches/functools-reduce/content.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Functools.reduce for Calculation

# `functools.reduce()` for Calculation

```python
from operator import add, mul, sub
Expand All @@ -12,25 +11,25 @@ OPERATORS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div}

def answer(question):
# Check for basic validity right away, and fail out with error if not valid.
if not question.startswith( "What is") or "cubed" in question:
if not question.startswith("What is") or "cubed" in question:
raise ValueError("unknown operation")
# Using the built-in filter() to clean & split the question..
question = list(filter(lambda x:
x not in ("What", "is", "by"),

# Use the built-in filter() to clean and split the question.
question = list(filter(lambda x:
x not in ("What", "is", "by"),
question.strip("?").split()))

# Separate candidate operators and numbers into two lists.
operations = question[1::2]

# Convert candidate elements to int(), checking for "-".
# All other values are replaced with None.
digits = [int(element) if
(element.isdigit() or element[1:].isdigit())
else None for element in question[::2]]
# If there is a mis-match between operators and numbers, toss error.
if len(digits)-1 != len(operations) or None in digits:
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, throw an error.
if len(digits) - 1 != len(operations) or None in digits:
raise ValueError("syntax error")

# Evaluate the expression from left to right using functools.reduce().
Expand All @@ -39,84 +38,81 @@ def answer(question):
```

This approach replaces the `while-loop` or `recursion` used in many solutions with a call to [`functools.reduce`][functools-reduce].
It requires that the question be separated into candidate digits and candidate operators, which is accomplished here via [list-slicing][sequence-operations] (_for some additional information on working with `lists`, see [concept: lists](/tracks/python/concepts/lists)_).
It requires that the question be separated into candidate digits and candidate operators, which is accomplished here via [list slicing][sequence-operations] (_for some additional information on working with `lists`, see [concept:python/lists]()_).

A nested call to `filter()` and `split()` within a `list` constructor is used to clean and process the question into an initial `list` of digit and operator strings.
However, this could easily be accomplished by either using [chained][method-chaining] string methods or a `list-comprehension`:

However, this could easily be accomplished by either using [chained][method-chaining] string methods or a list comprehension:

```python
# Alternative 1 is chaining various string methods together.
# The wrapping () invoke implicit concatenation for the chained functions
return (question.removeprefix("What is")
# The wrapping () invoke implicit concatenation for the chained functions.
question = (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.
```
.strip()).split() # <-- This split() turns the string into a list.


# Alternative 2 to the nested calls is to use a list comprehension:
question = [item for item in
question.strip("?").split()
if item not in ("What", "is", "by")] # <-- The [] of the comprehension invokes implicit concatenation.
```

Since "valid" questions are all in the form of `digit-operator-digit` (_and so on_), it is safe to assume that every other element beginning at index 0 is a "number", and every other element beginning at index 1 is an operator.
By that definition, the operators `list` is 1 shorter in `len()` than the digits list.
Anything else (_or having None/an unknown operation in the operations list_) is a `ValueError("syntax error")`.
By that definition, the `operators` list is 1 shorter in `len()` than the `digits` list.
Anything else (_or having `None`/an unknown operation in the operations list_) is a `ValueError("syntax error")`.


The final call to `functools.reduce` essentially performs the same steps as the `while-loop` implementation, with the `lambda-expression` passing successive items of the digits `list` to the popped and looked-up operation from the operations `list` (_made [callable][callable] by adding `()`_), until it is reduced to one number and returned.
The final call to `functools.reduce` essentially performs the same steps as the `while-loop` implementation, with the `lambda-expression` passing successive items of the `digits` list to the popped and looked-up operation from the operations `list` (_used as a [callable][callable] with `()`_), until it is reduced to one number and returned.
A `try-except` is not needed here because the error scenarios are already filtered out in the `if` check right before the call to `reduce()`.

`functools.reduce` is certainly convenient, and makes the solution much shorter.
But it is also hard to understand what is happening if you have not worked with a reduce or foldl function in the past.
`functools.reduce` is certainly convenient, and it makes the solution much shorter.
However, it is also hard to understand what is happening if you have not worked with a `reduce` or `foldl` function in the past.
It could be argued that writing the code as a `while-loop` or recursive function is easier to reason about for non-functional programmers.

<br>

## Variation 1: Use a Dictionary of `lambdas` instead of importing from operator
## Variation 1: Use a dictionary of `lambdas` instead of importing from `operator`


The imports from operator can be swapped out for a dictionary of `lambda-expressions` (or calls to `dunder-methods`), if so desired.
The imports from the `operator` module can be swapped out for a dictionary of `lambda-expressions` (or calls to `dunder-methods`), if so desired.
The same cautions apply here as were discussed in the [lambdas in a dictionary][approach-lambdas-in-a-dictionary] approach:


```python
from functools import reduce

# Define a lookup table for mathematical operations
OPERATORS = {"plus": lambda x, y: x + y,
"minus": lambda x, y: x - y,
"multiplied": lambda x, y: x * y,
"divided": lambda x, y: x / y}
OPERATORS = {
"plus": lambda x, y: x + y,
"minus": lambda x, y: x - y,
"multiplied": lambda x, y: x * y,
"divided": lambda x, y: x / y
}

def answer(question):

# Check for basic validity right away, and fail out with error if not valid.
if not question.startswith( "What is") or "cubed" in question:
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
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())
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.
# Look up each operation in the OPERATORS dictionary.
result = reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits)

return result
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +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]]
else None for element in question[::2]]
...
return reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits)
Loading
Loading