[Darts] : Add Approaches (#3386)

* Initial docs for darts approaches.

* Initial rough draft of darts approaches.

* Removing .articles for now.

* Wrapped up approaches content details and removed performance info.

* Cleaned up typos and changed toss to throw.
This commit is contained in:
BethanyG
2024-11-30 11:37:26 -08:00
committed by GitHub
parent 01fb2575e8
commit db94cb0307
14 changed files with 625 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
# Using Boolean Values as Integers
```python
def score(x_coord, y_coord):
radius = (x_coord**2 + y_coord**2)
return (radius<=1)*5 + (radius<=25)*4 + (radius<=100)*1
```
In Python, the [Boolean values `True` and `False` are _subclasses_ of `int`][bools-as-ints] and can be interpreted as `0` (False) and `1` (True) in a mathematical context.
This approach leverages that interpretation by checking which areas the throw falls into and multiplying each Boolean `int` by a scoring multiple.
For example, a throw that lands on the 25 (_or 5 if using `math.sqrt(x**2 + y**2)`_) circle should have a score of 5:
```python
>>> (False)*5 + (True)*4 + (True)*1
5
```
This makes for very compact code and has the added boost of not requiring any `loops` or additional data structures.
However, it is considered bad form to rely on Boolean interpretation.
Instead, the Python documentation recommends an explicit conversion to `int`:
```python
def score(x_coord, y_coord):
radius = (x_coord**2 + y_coord**2)
return int(radius<=1)*5 + int(radius<=25)*4 + int(radius<=100)*1
```
Beyond that recommendation, the terseness of this approach might be harder to reason about or decode — especially if a programmer is coming from a programming langauge that does not treat Boolean values as `ints`.
Despite the "radius" variable name, it is also more difficult to relate the scoring "rings" of the Dartboard to the values being checked and calculated in the `return` statement.
If using this code in a larger program, it would be strongly recommended that a docstring be provided to explain the Dartboard rings, scoring rules, and the corresponding scores.
[bools-as-ints]: https://docs.python.org/3/library/stdtypes.html#boolean-type-bool

View File

@@ -0,0 +1,3 @@
def score(x_coord, y_coord):
radius = (x_coord**2 + y_coord**2)
return (radius<=1)*5 + (radius<=25)*4 +(radius<=100)*1

View File

@@ -0,0 +1,50 @@
{
"introduction": {
"authors": ["bethanyg"],
"contributors": []
},
"approaches": [
{
"uuid": "7d78f598-8b4c-4f7f-89e1-e8644e934a4c",
"slug": "if-statements",
"title": "Use If Statements",
"blurb": "Use if-statements to check scoring boundaries for a dart throw.",
"authors": ["bethanyg"]
},
{
"uuid": "f8f5533a-09d2-4b7b-9dec-90f268bfc03b",
"slug": "tuple-and-loop",
"title": "Use a Tuple & Loop through Scores",
"blurb": "Score the Dart throw by looping through a tuple of scores.",
"authors": ["bethanyg"]
},
{
"uuid": "a324f99e-15bb-43e0-9181-c1652094bc4f",
"slug": "match-case",
"title": "Use Structural Pattern Matching ('Match-Case')",
"blurb": "Use a Match-Case (Structural Pattern Matching) to score the dart throw.)",
"authors": ["bethanyg"]
},
{
"uuid": "966bd2dd-c4fd-430b-ad77-3a304dedd82e",
"slug": "dict-and-generator",
"title": "Use a Dictionary with a Generator Expression",
"blurb": "Use a generator expression looping over a scoring dictionary, getting the max score for the dart throw.",
"authors": ["bethanyg"]
},
{
"uuid": "5b087f50-31c5-4b84-9116-baafd3a30ed6",
"slug": "booleans-as-ints",
"title": "Use Boolean Values as Integers",
"blurb": "Use True and False as integer values to calculate the score of the dart throw.",
"authors": ["bethanyg"]
},
{
"uuid": "0b2dbcd3-f0ac-45f7-af75-3451751fd21f",
"slug": "dict-and-dict-get",
"title": "Use a Dictionary with dict.get",
"blurb": "Loop over a dictionary and retrieve score via dct.get.",
"authors": ["bethanyg"]
}
]
}

View File

@@ -0,0 +1,72 @@
# Using a Dictionary and `dict.get()`
```python
def score(x_coord, y_coord):
point = (x_coord**2 + y_coord**2)
scores = {
point <= 100: 1,
point <= 25: 5,
point <= 1: 10
}
return scores.get(True, 0)
```
At first glance, this approach looks similar to the [Booleans as Integers][approach-boolean-values-as-integers] approach, due to the Boolean evaluation used in the dictionary keys.
However, this approach is **not** interpreting Booleans as integers and is instead exploiting three key properties of [dictionaries][dicts]:
1. [Keys must be hashable][hashable-keys] — in other words, keys have to be _unique_.
2. Insertion order is preserved (_as of `Python 3.7`_), and evaluation/iteration happens in insertion order.
3. Duplicate keys _overwrite_ existing keys.
If the first key is `True` and the third key is `True`, the _value_ from the third key will overwrite the value from the first key.
Finally, the `return` line uses [`dict.get()`][dict-get] to `return` a default value of 0 when a throw is outside the existing circle radii.
To see this in action, you can view this code on [Python Tutor][dict-get-python-tutor].
Because of the listed dictionary qualities, **_order matters_**.
This approach depends on the outermost scoring circle containing all smaller circles and that
checks proceed from largest --> smallest circle.
Iterating in the opposite direction will not resolve to the correct score.
The following code variations do not pass the exercise tests:
```python
def score(x_coord, y_coord):
point = (x_coord**2 + y_coord**2)
scores = {
point <= 1: 10,
point <= 25: 5,
point <= 100: 1,
}
return scores.get(True, 0)
#OR#
def score(x_coord, y_coord):
point = (x_coord**2 + y_coord**2)
scores = {
point <= 25: 5,
point <= 1: 10,
point <= 100: 1,
}
return scores.get(True, 0)
```
While this approach is a _very clever_ use of dictionary properties, it is likely to be very hard to reason about for those who are not deeply knowledgeable.
Even those experienced in Python might take longer than usual to figure out what is happening in the code.
Extensibility could also be error-prone due to needing a strict order for the `dict` keys.
This approach offers no space or speed advantages over using `if-statements` or other strategies, so is not recommended for use beyond a learning context.
[approach-boolean-values-as-integers]: https://exercism.org/tracks/python/exercises/darts/approaches/boolean-values-as-integers
[dicts]: https://docs.python.org/3/library/stdtypes.html#mapping-types-dict
[dict-get]: https://docs.python.org/3/library/stdtypes.html#dict.get
[dict-get-python-tutor]: https://pythontutor.com/render.html#code=def%20score%28x_coord,%20y_coord%29%3A%0A%20%20%20%20point%20%3D%20%28x_coord**2%20%2B%20y_coord**2%29%0A%20%20%20%20scores%20%3D%20%7B%0A%20%20%20%20%20%20%20%20point%20%3C%3D%20100%3A%201,%0A%20%20%20%20%20%20%20%20point%20%3C%3D%2025%3A%205,%0A%20%20%20%20%20%20%20%20point%20%3C%3D%201%3A%2010%0A%20%20%20%20%7D%0A%20%20%20%20%0A%20%20%20%20return%20scores.get%28True,%200%29%0A%20%20%20%20%0Aprint%28score%281,3%29%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
[hashable-keys]: https://www.pythonmorsels.com/what-are-hashable-objects/#dictionary-keys-must-be-hashable

View File

@@ -0,0 +1,5 @@
def score(x_coord, y_coord):
point = (x_coord**2 + y_coord**2)
scores = {point <= 100: 1, point <= 25: 5, point <= 1: 10}
return scores.get(True, 0)

View File

@@ -0,0 +1,69 @@
# Use a Dictionary and a Generator Expression
```python
def score(x_coord, y_coord):
throw = x_coord**2 + y_coord**2
rules = {1: 10, 25: 5, 100: 1, 200: 0}
return max(point for distance, point in
rules.items() if throw <= distance)
```
This approach is very similar to the [tuple and loop][approach-tuple-and-loop] approach, but iterates over [`dict.items()`][dict-items] and writes the `loop` as a [`generator-expression`][generator-expression] inside `max()`.
In cases where the scoring circles overlap, `max()` will return the maximum score available for the throw.
The generator expression inside `max()` is the equivalent of using a `for-loop` and a variable to determine the max score:
```python
def score(x_coord, y_coord):
throw = x_coord**2 + y_coord**2
rules = {1: 10, 25: 5, 100: 1}
max_score = 0
for distance, point in rules.items():
if throw <= distance and point > max_score:
max_score = point
return max_score
```
A `list` or `tuple` can also be used in place of `max()`, but then requires an index to return the max score:
```python
def score(x_coord, y_coord):
throw = x_coord**2 + y_coord**2
rules = {1: 10, 25: 5, 100: 1, 200: 0}
return [point for distance, point in
rules.items() if throw <= distance][0] #<-- have to specify index 0.
#OR#
def score(x_coord, y_coord):
throw = x_coord**2 + y_coord**2
rules = {1: 10, 25: 5, 100: 1, 200: 0}
return tuple(point for distance, point in
rules.items() if throw <= distance)[0]
```
This solution can even be reduced to a "one-liner".
However, this is not performant, and is difficult to read:
```python
def score(x_coord, y_coord):
return max(point for distance, point in
{1: 10, 25: 5, 100: 1, 200: 0}.items() if
(x_coord**2 + y_coord**2) <= distance)
```
While all of these variations do pass the tests, they suffer from even more over-engineering/performance caution than the earlier tuple and loop approach (_although for the data in this problem, the performance hit is slight_).
Additionally, the dictionary will take much more space in memory than using a `tuple` of tuples to hold scoring values.
In some circumstances, these variations might also be harder to reason about for those not familiar with `generator-expressions` or `list comprehensions`.
[approach-tuple-and-loop]: https://exercism.org/tracks/python/exercises/darts/approaches/tuple-and-loop
[dict-items]: https://docs.python.org/3/library/stdtypes.html#dict.items
[generator-expression]: https://dbader.org/blog/python-generator-expressions

View File

@@ -0,0 +1,8 @@
def score(x_coord, y_coord):
length = x_coord**2 + y_coord**2
rules = {1.0: 10, 25.0: 5, 100.0: 1, 200: 0}
score = max(point for
distance, point in
rules.items() if length <= distance)
return score

View File

@@ -0,0 +1,73 @@
# Use `if-statements`
```python
import math
# Checks scores from the center --> edge.
def score(x_coord, y_coord):
distance = math.sqrt(x_coord**2 + y_coord**2)
if distance <= 1: return 10
if distance <= 5: return 5
if distance <= 10: return 1
return 0
```
This approach uses [concept:python/conditionals]() to check the boundaries for each scoring ring, returning the corresponding score.
Calculating the euclidian distance is assigned to the variable "distance" to avoid having to re-calculate it for every if check.
Because the `if-statements` are simple and readable, they're written on one line to shorten the function body.
Zero is returned if no other check is true.
To avoid importing the `math` module (_for a very very slight speedup_), (x**2 +y**2) can be calculated instead, and the scoring rings can be adjusted to 1, 25, and 100:
```python
# Checks scores from the center --> edge.
def score(x_coord, y_coord):
distance = x_coord**2 + y_coord**2
if distance <= 1: return 10
if distance <= 25: return 5
if distance <= 100: return 1
return 0
```
# Variation 1: Check from Edge to Center Using Upper and Lower Bounds
```python
import math
# Checks scores from the edge --> center
def score(x_coord, y_coord):
distance = math.sqrt(x_coord**2 + y_coord**2)
if distance > 10: return 0
if 5 < distance <= 10: return 1
if 1 < distance <= 5: return 5
return 10
```
This variant checks from the edge moving inward, checking both a lower and upper bound due to the overlapping scoring circles in this direction.
Scores for any of these solutions can also be assigned to a variable to avoid multiple `returns`, but this isn't really necessary:
```python
# Checks scores from the edge --> center
def score(x_coord, y_coord):
distance = x_coord**2 + y_coord**2
points = 10
if distance > 100: points = 0
if 25 < distance <= 100: points = 1
if 1 < distance <= 25: points = 5
return points
```

View File

@@ -0,0 +1,8 @@
import math
def score(x_coord, y_coord):
distance = math.sqrt(x_coord**2 + y_coord**2)
if distance <= 1: return 10
if distance <= 5: return 5
if distance <= 10: return 1
return 0

View File

@@ -0,0 +1,146 @@
# Introduction
There are multiple Pythonic ways to solve the Darts exercise.
Among them are:
- Using `if-statements`
- Using a `tuple` (or `list` or `dict`) and a `for-loop`
- Using a `dict` (or `tuple` or `list`) and a `generator-expression`
- Using `boolean` values as `ints`
- Using a `dict` and `dict.get()`
- Using `match/case` (_Python 3.10+ only_)
<br>
## General guidance
The goal of the Darts exercise is to score a single throw in a Darts game.
The scoring areas are _concentric circles_, so boundary values need to be checked in order to properly score a throw.
The key is to determine how far from the center the dart lands (_by calculating sqrt(x**2 + y**2), or a variation_) and then determine what scoring ring it falls into.
**_Order matters_** - each bigger target circle contains all the smaller circles, so the most straightforward solution is to check the smallest circle first.
Otherwise, you must box your scoring by checking both a _lower bound_ and an _upper bound_.
Darts that fall on a _boundary_ are scored based on the area below the line (_closer to center_), so checking `<=` or `>=` is advised.
## Approach: Using `if` statements
```python
import math
# Checks scores from the center --> edge.
def score(x_coord, y_coord):
distance = math.sqrt(x_coord**2 + y_coord**2)
if distance <= 1: return 10
if distance <= 5: return 5
if distance <= 10: return 1
return 0
```
This approach uses [concept:python/conditionals]() to check the boundaries for each scoring ring, returning the corresponding score.
For more details, see the [if statements][approach-if-statements] approach.
## Approach: Using a `tuple` and a `loop`
```python
def score(x_coord, y_coord):
throw = x_coord**2 + y_coord**2
rules = (1, 10), (25, 5), (100, 1), (200, 0)
for distance, points in rules:
if throw <= distance:
return points
```
This approach uses a loop to iterate through the _rules_ `tuple`, unpacking each `distance` and corresponding`score`.
For more details, see the [tuple and loop][approach-tuple-and-loop] approach.
## Approach: Using a `dict` with a `generator-expression`
```python
def score(x_coord, y_coord):
throw = x_coord**2 + y_coord**2
rules = {1: 10, 25: 5, 100: 1, 200: 0}
return max(point for distance, point in
rules.items() if throw <= distance)
```
This approach is very similar to the [tuple and loop][approach-tuple-and-loop] approach, but iterates over [`dict.items()`][dict-items].
For more information, see the [dict with generator-expression][approach-dict-with-generator-expression] approach.
## Approach: Using Boolean Values as Integers
```python
def score(x_coord, y_coord):
radius = (x_coord**2 + y_coord**2)
return (radius<=1)*5 + (radius<=25)*4 +(radius<=100)*1
```
This approach exploits the fact that Boolean values are an integer subtype in Python.
For more information, see the [boolean values as integers][approach-boolean-values-as-integers] approach.
## Approach: Using a `Dictionary` and `dict.get()`
```python
def score(x_coord, y_coord):
point = (x_coord**2 + y_coord**2)
scores = {
point <= 100: 1,
point <= 25: 5,
point <= 1: 10
}
return scores.get(True, 0)
```
This approach uses a dictionary to hold the distance --> scoring mappings and `dict.get()` to retrieve the correct points value.
For more details, read the [`Dictionary and dict.get()`][approach-dict-and-dict-get] approach.
## Approach: Using `match/case` (structural pattern matching)
```python
from math import hypot, ceil
def score(x, y):
match ceil(hypot(x, y)):
case 0 | 1: return 10
case 2 | 3 | 4 | 5: return 5
case 6 | 7 | 8 | 9 | 10: return 1
case _: return 0
```
This approach uses `Python 3.10`'s structural pattern matching with `return` values on the same line as `case`.
A fallthrough case (`_`) is used if the dart throw is outside the outer circle of the target (_greater than 10_).
For more details, see the [structural pattern matching][approach-struct-pattern-matching] approach.
## Which approach to use?
Many of these approaches are a matter of personal preference - there are not significant memory or performance differences.
Although a strong argument could be made for simplicity and clarity — many listed solutions (_while interesting_) are harder to reason about or are over-engineered for the current scope of the exercise.
[approach-boolean-values-as-integers]: https://exercism.org/tracks/python/exercises/darts/approaches/boolean-values-as-integers
[approach-dict-and-dict-get]: https://exercism.org/tracks/python/exercises/darts/approaches/dict-and-dict-get
[approach-dict-with-generator-expression]: https://exercism.org/tracks/python/exercises/darts/approaches/dict-with-gnerator-expresson
[approach-if-statements ]: https://exercism.org/tracks/python/exercises/darts/approaches/if-statements
[approach-struct-pattern-matching]: https://exercism.org/tracks/python/exercises/darts/approaches/struct-pattern-matching
[approach-tuple-and-loop]: https://exercism.org/tracks/python/exercises/darts/approaches/tuple-and-loop
[dict-items]: https://docs.python.org/3/library/stdtypes.html#dict.items

View File

@@ -0,0 +1,85 @@
# Use `match/case` (Structural Pattern Matching)
```python
from math import hypot, ceil
def score(x, y):
throw = ceil(hypot(x, y))
match throw:
case 0 | 1: return 10
case 2 | 3 | 4 | 5: return 5
case 6 | 7 | 8 | 9 | 10: return 1
case _: return 0
#OR#
def score(x, y):
match ceil(hypot(x, y)):
case 0 | 1: return 10
case 2 | 3 | 4 | 5: return 5
case 6 | 7 | 8 | 9 | 10: return 1
case _: return 0
```
This approach uses `Python 3.10`'s [`structural pattern matching`][structural-pattern-matching] with `return` values on the same line as `case`.
Because the match is numeric, each case explicitly lists allowed values using the `|` (OR) operator.
A fallthrough case (`_`) is used if the dart throw is greater than 10 (_the outer circle radius of the target_).
This is equivalent to using `if-statements` to check throw values although some might argue it is clearer to read.
An `if-statement` equivalent would be:
```python
from math import hypot, ceil
def score(x, y):
throw = ceil(hypot(x, y))
if throw in (0, 1): return 10
if throw in (2, 3, 4, 5): return 5
if throw in (6, 7, 8, 9, 10): return 1
return 0
```
One can also use `<`, `>`, or `<=` and `>=` in structural pattern matching, although the syntax becomes almost identical to using them with `if-statements`, but more verbose:
```python
from math import hypot, ceil
def score(x, y):
throw = ceil(hypot(x, y))
match throw:
case throw if throw <= 1: return 10
case throw if throw <= 5: return 5
case throw if throw <= 10: return 1
case _: return 0
```
Finally, one can use an [assignment expression][assignment-expression] or [walrus operator][walrus] to calculate the throw value rather than calculating and assigning a variable on a separate line.
This isn't necessary (_the first variations shows this clearly_) and might be harder to reason about/understand for some programmers:
```python
from math import hypot, ceil
def score(x, y):
match throw := ceil(hypot(x, y)):
case throw if throw <= 1: return 10
case throw if throw <=5: return 5
case throw if throw <=10: return 1
case _: return 0
```
Using structural pattern matching for this exercise doesn't offer any clear performance advantages over the `if-statement`, but might be "cleaner", more "organized looking", or easier for others to scan/read.
[assignment-expression]: https://docs.python.org/3/reference/expressions.html#grammar-token-python-grammar-assignment_expression
[structural-pattern-matching]: https://peps.python.org/pep-0636/
[walrus]: https://peps.python.org/pep-0572/

View File

@@ -0,0 +1,8 @@
from math import hypot, ceil
def score(x, y):
match ceil(hypot(x, y)):
case 0 | 1: return 10
case 2 | 3 | 4 | 5: return 5
case 6 | 7 | 8 | 9 | 10: return 1
case _: return 0

View File

@@ -0,0 +1,55 @@
# Use a tuple with a loop
```python
def score(x_coord, y_coord):
throw = x_coord**2 + y_coord**2
rules = (1, 10), (25, 5), (100, 1), (200, 0)
for distance, points in rules:
if throw <= distance:
return points
```
This approach uses a loop to iterate through the _rules_ `tuple`, unpacking each (`distance`, `points`) pair (_For a little more on unpacking, see [Tuple Unpacking Improves Python Code Readability][tuple-unpacking]_).
If the calculated distance of the throw is less than or equal to a given distance, the score for that region is returned.
A `list` of `lists`, a `list` of `tuples`, or a dictionary could be used here to the same effect:
```python
def score(x_coord, y_coord):
throw = x_coord**2 + y_coord**2
rules = [[1, 10], [25, 5], [100, 1]]
for distance, points in rules:
if throw <= distance:
return points
return 0
#OR#
def score(x_coord, y_coord):
throw = x_coord**2 + y_coord**2
rules = [(1, 10), (25, 5), (100, 1), (200, 0)]
for distance, points in rules:
if throw <= distance:
return points
#OR#
def score(x_coord, y_coord):
throw = x_coord**2 + y_coord**2
rules = {1: 10, 25: 5, 100: 1}
for distance, points in rules.items():
if throw <= distance:
return points
return 0
```
This approach would work nicely in a scenario where you expect to be adding more scoring "rings", since it is cleaner to edit the data structure than to add additional `if-statements` as you would have to in the [`if-statement` approach][approach-if-statements ].
For the three rings as defined by the current exercise, it is a bit over-engineered to use a data structure + `loop`, and results in a slight (_**very** slight_) slowdown over using `if-statements`.
[tuple-unpacking]: https://treyhunner.com/2018/03/tuple-unpacking-improves-python-code-readability/#Unpacking_in_a_for_loop
[approach-if-statements ]: https://exercism.org/tracks/python/exercises/darts/approaches/if-statements

View File

@@ -0,0 +1,7 @@
def score(x_coord, y_coord):
distance = x_coord**2 + y_coord**2
rules = (1.0, 10), (25.0, 5), (100.0, 1), (200.0, 0)
for distance, point in rules:
if length <= distance:
return point