diff --git a/concepts/unpacking-and-multiple-assignment/.meta/config.json b/concepts/unpacking-and-multiple-assignment/.meta/config.json index 9b9e8da5a9b..ecf013c7acc 100644 --- a/concepts/unpacking-and-multiple-assignment/.meta/config.json +++ b/concepts/unpacking-and-multiple-assignment/.meta/config.json @@ -1,5 +1,5 @@ { - "blurb": "TODO: add blurb for this concept", - "authors": ["bethanyg", "cmccandless"], + "blurb": "Unpacking is the process of extracting individual elements of a collection, such as a list, tuple, or dictionary by iterating over them. Unpacked values can be assigned to variables within the same step. Multiple assignment is the ability to assign values to multiple variables in one line.", + "authors": ["meatball","bethanyg"], "contributors": [] } diff --git a/concepts/unpacking-and-multiple-assignment/about.md b/concepts/unpacking-and-multiple-assignment/about.md index c628150d565..68c8e0662bd 100644 --- a/concepts/unpacking-and-multiple-assignment/about.md +++ b/concepts/unpacking-and-multiple-assignment/about.md @@ -1,2 +1,355 @@ -#TODO: Add about for this concept. +# Unpacking and Multiple Assignment +Unpacking refers to the act of extracting the elements of a collection, such as a `list`, `tuple`, or `dict`, using iteration. +Unpacked values can be assigned to variables within the same step. +With unpacking, there are some special operators used: `*` and `**`. +When unpacking a list or tuple, the `*` operator can be used to assign all the remaining elements to a variable. +When unpacking a dictionary, the `**` operator can be used to assign all the remaining key-value pairs to a variable. + +When these operators are used without a collection they will _pack_ a number of values into a `list`, `tuple`, or dictionary. +It is common to use this kind of behavior when creating functions that take an arbitrary number of arguments. + +Multiple assignment is the ability to assign multiple variables in one line. +This is done by separating the variables with a comma. + +```exercism/caution +`*` and `**` should not be confused with `*` and `**`. While `*` and `**` are used for multiplication and exponentiation respectively, `*` and `**` are used as packing and unpacking operators. +``` + +## Multiple assignment + +[Multiple assignment][multiple assignment] is the ability to assign multiple variables in one line. +This allows for code to be more concise and readable. +There has to be x number of variables on the left side of the `=` sign and x number of values on the right side of the `=` sign. +To separate the values, use a comma `,`: + +```python +>>> a, b = 1, 2 +>>> a +1 +``` + +Multiple assignment is not limited to one data type but can instead be used with any data type. +For example: + +```python +>>> x, y, z = 1, "Hello", True +>>> x +1 + +>>> y +'Hello' + +>>> z +True +``` + +Multiple assignment also allows for the swapping of variables in `lists`. +This practice is pretty common in [linear sorting algorithms][sorting algorithms]. +For example: + +```python +>>> numbers = [1, 2] +>>> numbers[0], numbers[1] = numbers[1], numbers[0] +>>> numbers +[2, 1] +``` + +It is also possible to assign multiple variables to the same value: + +```python +>>> a = b = 1 +>>> a +1 +>>> b +1 +``` + +## Unpacking + +```exercism/note +The examples below use lists but the same concepts apply to tuples. +``` + +In Python, it is possible to [unpack a `list`/`tuple`/`dictionary`][unpacking] into distinct variables. +Since values appear within lists in a specific order, it is therefore possible to _unpack_ a `list` into variables in the same order. + +Unpacking a list into variables: + +```python +>>> fruits = ["apple", "banana", "cherry"] +>>> x, y, z = fruits +>>> x +"apple" +``` + +If there are values that are not needed then you can use `_` to ignore those values: + +```python +>>> fruits = ["apple", "banana", "cherry"] +>>> _, _, z = fruits +>>> z +"cherry" +``` + +You can also do [deep unpacking][deep unpacking] on a `list`, which assigns values from a `list` within a `list` (_this is also known as nested list unpacking_): + +```python +>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]] +>>> [[a, b], [c, d]] = fruits_vegetables +>>> a +"apple" + +>>> d +"potato" +``` + +Deep unpacking and normal unpacking can be mixed together: + +```python +>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]] +>>> [a, [c, d]] = fruits_vegetables +>>> a +["apple", "banana"] + +>>> c +"carrot" +``` + +If the unpacking has variables with incorrect placement and/or an incorrect number of values, you will get a `ValueError`: + +```python +>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]] +>>> [[a, b], [d]] = fruits_vegetables + +ValueError: too many values to unpack (expected 1) +``` + +### Unpacking a list/tuple with `*` + +When [unpacking a `list`/`tuple`][packing and unpacking] you can use the `*` operator to capture the remainder values. +This can be used instead of slicing the `list`/`tuple`, which in some situations could be more readable. +For example, we can extract the first element below and then pack the remaining values into a new `list` without the first element: + +```python +>>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"] +>>> x, *last = fruits +>>> x +"apple" + +>>> last +["banana", "cherry", "orange", "kiwi", "melon", "mango"] +``` + +We can also extract the values at the beginning and end of the `list` while grouping all the values in the middle: + +```python +>>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"] +>>> x, *middle, y, z = fruits +>>> y +"melon" + +>>> middle +["banana", "cherry", "orange", "kiwi"] +``` + +We can also use `*` in deep unpacking: + +```python +>>> fruits_vegetables = [["apple", "banana", "melon"], ["carrot", "potato", "tomato"]] +>>> [[a, *rest], b] = fruits_vegetables +>>> a +"apple" + +>>> rest +["banana", "melon"] +``` + +### Unpacking a dictionary + +[Unpacking a dictionary][packing and unpacking] is a bit different than unpacking a `list`/`tuple`. +Iteration over dictionaries defaults to the `keys`. +So when unpacking a `dict`, you can only unpack the `keys` and not the `values`: + +```python +>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} +>>> x, y, z = fruits_inventory +>>> x +"apple" +``` + +If you want to unpack the values then you can use the `values()` method: + +```python +>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} +>>> x, y, z = fruits_inventory.values() +>>> x +6 +``` + +If both `keys` and `values` are needed, use the `items()` method. +Using `items()` will generate tuples with `key-value` pairs. +This is because [`dict.items()` generates a `tuple`][items] and within it there is a `tuple` for each `key-value` pair:. + +```python +>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} +>>> x, y, z = fruits_inventory.items() +>>> x +("apple", 6) +``` + +## Packing + +As with unpacking, _packing_ uses the same `*` and `**` operators. +[Packing][packing and unpacking]] is the ability to group multiple values into one variable. +This is useful for when you want to _unpack_ values, make changes, and then _pack_ the results back into a variable. +It also makes it possible to perform merges on 2 or more `lists`/`tuples`/`dicts`. + +### Packing a list/tuple with `*` + +Packing a `list`/`tuple` is done by using the `*` operator +This will pack all the variables into a list/tuple. + +```python +>>> fruits = ["apple", "banana", "cherry"] +>>> more_fruits = ["orange", "kiwi", "melon", "mango"] +>>> combined_fruits_lists = [*fruits, *more_fruits] + +>>> combined_fruits_lists +["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"] +``` + +### Packing a dictionary with `**` + +Packing a dictionary is done by using the `**` operator. +This will pack all the variables into a dictionary. + +```python +>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} +>>> more_fruits_inventory = {"orange": 4, "kiwi": 1, "melon": 2, "mango": 3} +>>> combined_fruits_inventory = {**fruits_inventory, **more_fruits_inventory} + +>>> combined_fruits_inventory +{"apple": 6, "banana": 2, "cherry": 3, "orange": 4, "kiwi": 1, "melon": 2, "mango": 3} +``` + +## Usage of `*` and `**` with a function + +### Packing with function parameters + +When you have a function that accepts an arbitrary or large number of arguments, you can use [`*args` or `**kwargs`][args and kwargs] to pack or group those parameters together. +`*args` is used for packing/signaling an arbitrary number of positional (non-keyworded) arguments. +`**kwargs` is used for packing/signaling an arbitrary number of keyword arguments to a function. + +Usage of `*args`: + +```python +>>> def my_function(*args): +... print(args) + +>>> my_function(1, 2, 3) +(1, 2, 3) + +>>> my_function("Hello", "World") +("Hello", "World") + +>>> my_function(1, 2, 3, "Hello", "Mars") +(1, 2, 3, "Hello", "Mars") +``` + +Usage of `**kwargs`: + +```python +>>> def my_function(**kwargs): +... print(kwargs) + +>>> my_function(a=1, b=2, c=3) +{"a": 1, "b": 2, "c": 3} +``` + +`*args` and `**kwargs` can also be used in combination with one another: + +```python +>>> def my_function(*args, **kwargs): +... print(sum(args)) +... for key, value in kwargs.items(): +... print(f"{key} = {value}") + +>>> my_function(1, 2, 3, a=1, b=2, c=3) +6 +a = 1 +b = 2 +c = 3 +``` + +You can also write parameters before `*args` to allow for specific positional arguments. +Individual keyword arguments then have to appear before `**kwargs`. + +```exercism/caution +[Arguments have to be structured][Positional and keyword arguments] like this: + +`def my_function(, *args, , **kwargs)` + +If you don't follow this order then you will get an error. +``` + +```python +>>> def my_function(a, b, *args): +... print(a) +... print(b) +... print(args) + +>>> my_function(1, 2, 3, 4, 5) +1 +2 +(3, 4, 5) +``` + +Writing arguments in an incorrect order will result in an error: + +```python +>>>def my_function(*args, a, b): +... print(args) + +>>>my_function(1, 2, 3, 4, 5) +Traceback (most recent call last): + File "c:\something.py", line 3, in + my_function(1, 2, 3, 4, 5) +TypeError: my_function() missing 2 required keyword-only arguments: 'a' and 'b' +``` + +### Unpacking into function calls + +You can use `*` to unpack a `list`/`tuple` of arguments into a function call. +This is very useful for functions that don't accept an `iterable` or `iterator`: + +```python +>>> def my_function(a, b, c): +... print(c) +... print(b) +... print(a) + +numbers = [1, 2, 3] +>>> my_function(*numbers) +3 +2 +1 +``` + +Using `*` unpacking with the `zip()` function is another common use case. +Since `zip()` takes multiple iterables and returns a list of tuples with the values from each iterable grouped: + +```python +>>> values = (['x', 'y', 'z'], [1, 2, 3], [True, False, True]) +>>> a, *rest = zip(*values) +>>> rest +[('y', 2, False), ('z', 3, True)] +``` + +[multiple assignment]: https://www.geeksforgeeks.org/assigning-multiple-variables-in-one-line-in-python/ +[sorting algorithms]: https://realpython.com/sorting-algorithms-python/ +[unpacking]: https://www.geeksforgeeks.org/unpacking-arguments-in-python/?ref=rp +[deep unpacking]: https://mathspp.com/blog/pydonts/deep-unpacking#deep-unpacking +[packing and unpacking]: https://www.geeksforgeeks.org/packing-and-unpacking-arguments-in-python/ +[items]: https://www.geeksforgeeks.org/python-dictionary-items-method/ +[args and kwargs]: https://www.geeksforgeeks.org/args-kwargs-python/ diff --git a/concepts/unpacking-and-multiple-assignment/introduction.md b/concepts/unpacking-and-multiple-assignment/introduction.md index fcde74642ca..0bb09dee5cc 100644 --- a/concepts/unpacking-and-multiple-assignment/introduction.md +++ b/concepts/unpacking-and-multiple-assignment/introduction.md @@ -1,2 +1,22 @@ -#TODO: Add introduction for this concept. +# Unpacking and Multiple Assignment +Unpacking refers to the act of extracting the elements of a collection, such as a `list`, `tuple`, or `dict`, using iteration. +Unpacked values can be assigned to variables within the same step. +With unpacking, there are some special operators used: `*` and `**`. + + +When unpacking a list or tuple, the `*` operator can be used to assign all the remaining elements to a variable. +When unpacking a dictionary, the `**` operator can be used to assign all the remaining key-value pairs to a variable. +When these operators are used without a collection, they _pack_ a number of values into a `list`, `tuple`, or `dict`. + +It is common in Python to also exploit this unpacking/packing behavior when defining functions that take an arbitrary number of positional or keyword arguments. +You will often see these "special" parameters defined as `def some_function(*args, **kwargs)` + +[Multiple assignment][multiple assignment] is the ability to assign multiple variables in one line. +This allows for code to be more concise and readable, and is done by separating the variables with a comma. + +```exercism/caution +`*` and `**` should not be confused with `*` and `**`. While `*` and `**` are used for multiplication and exponentiation respectively, `*` and `**` are used as packing and unpacking operators. +``` + +[multiple assignment]: https://www.geeksforgeeks.org/assigning-multiple-variables-in-one-line-in-python/ diff --git a/concepts/unpacking-and-multiple-assignment/links.json b/concepts/unpacking-and-multiple-assignment/links.json index eb5fb7c38a5..8caff2f9cd0 100644 --- a/concepts/unpacking-and-multiple-assignment/links.json +++ b/concepts/unpacking-and-multiple-assignment/links.json @@ -1,18 +1,26 @@ [ { - "url": "http://example.com/", - "description": "TODO: add new link (above) and write a short description here of the resource." + "url": "https://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/", + "description": "Trey Hunner: Astrisks in Python - What they are and How to Use Them." }, { - "url": "http://example.com/", - "description": "TODO: add new link (above) and write a short description here of the resource." + "url": "https://treyhunner.com/2018/03/tuple-unpacking-improves-python-code-readability/", + "description": "Trey Hunner: Tuple Unpacking Improves Python Code Readability." }, { - "url": "http://example.com/", - "description": "TODO: add new link (above) and write a short description here of the resource." + "url": "https://stackabuse.com/unpacking-in-python-beyond-parallel-assignment/", + "description": "Stack Abuse: Unpacking in Python Beyond Parallel Assignment." }, { - "url": "http://example.com/", - "description": "TODO: add new link (above) and write a short description here of the resource." + "url": "https://dbader.org/blog/python-nested-unpacking", + "description": "Dan Bader: Python Nested Unpacking." + }, + { + "url": "https://peps.python.org/pep-3132/", + "description": "Pep 3132: Extended Iterable Unpacking." + }, + { + "url": "https://peps.python.org/pep-0448/", + "description": "PEP 0448: Additional Unpacking Generalizations." } ] diff --git a/config.json b/config.json index bb349eede74..61cc1285f03 100644 --- a/config.json +++ b/config.json @@ -136,6 +136,14 @@ "prerequisites": ["loops", "lists", "tuples"], "status": "beta" }, + { + "slug": "locomotive-engineer", + "name": "Locomotive Engineer", + "uuid": "e1b8b9c9-21c3-47b1-b645-5938b3110c78", + "concepts": ["unpacking-and-multiple-assignment"], + "prerequisites": ["loops", "lists", "tuples", "dicts"], + "status": "wip" + }, { "slug": "cater-waiter", "name": "Cater Waiter", diff --git a/exercises/concept/locomotive-engineer/.docs/hints.md b/exercises/concept/locomotive-engineer/.docs/hints.md new file mode 100644 index 00000000000..929448f286d --- /dev/null +++ b/exercises/concept/locomotive-engineer/.docs/hints.md @@ -0,0 +1,35 @@ +# Hints + +## General + +- To extract multiple arguments in the function parameters so can you pack them with the `*args` operator for list or tuples or `kwargs` for keyword-based arguments. +- To pack or unpack use the `*` or `**` operator. + +## 1. Create a list of all wagons + +- Multiple arguments in the function parameters can be packed with the `*args` operator. + +## 2. Fix list of wagons + +- Using unpacking with the `*` operator, lets you extract the first two elements of a list while keeping the rest intact. +- To add another list into an existing list, you can use the `*` operator to "spread" the list. + +## 3. Add missing stops + +- Using `**kwargs` as a function parameter will allow an arbitrary amount of keyword arguments to be passed. +- Using `**(dict)` as an argument will unpack a dictionary into keyword arguments. +- You can put keyword arguments in a `{}` or `dict()`. +- To get the values out of a dictionary, you can use the `.values()` method. + +## 4. Extend routing information + +- Using `**(dict)` as an argument will unpack a dictionary into keyword arguments. +- You can put keyword arguments in a `{}` or `dict()`. + +## 5. Fix the wagon depot + +- `zip(*iterators)` can use used to transpose a nested list. +- To extract data from zipped iterators, you can use a for loop. +- you can also unpack zipped iterators using `*`. + `[*content] = zip(iterator_1, iterator_2)` will unzip the `tuple` produced by `zip()` into a `list`. + diff --git a/exercises/concept/locomotive-engineer/.docs/instructions.md b/exercises/concept/locomotive-engineer/.docs/instructions.md new file mode 100644 index 00000000000..9b80b9ff32b --- /dev/null +++ b/exercises/concept/locomotive-engineer/.docs/instructions.md @@ -0,0 +1,110 @@ +# Instructions + +Your friend Linus is a Locomotive Engineer who drives cargo trains between cities. +Your friend is great at handling trains, although they aren't amazing at handling the logistics computers and would like your programming help organizing the train and correcting mistakes in the data. + +```exercism/note +This exercise could easily be solved using `list` slicing, indexing, and `dict` methods. +However, we'd like you to practice using Unpacking and Multiple Assignment to solve each of the tasks below. +``` + +## 1. Create a list of all wagons + +Your friend has been keeping track of each wagon identifier, but they're never sure how many wagons they are going to have to process at any given time. It would be much easier for the rest of the logistics program to have the data to be returned as a `list`. + +Implement a function `get_list_of_wagons` that accepts an arbitrary amount of positive integers which are the IDs of each wagon. +It should then return the given IDs as a `list`. + +```python +>>> get_list_of_wagons(1, 7, 12, 3, 14, 8, 5) +[1, 7, 12, 3, 14, 8, 3] +``` + +## 2. Fix the list of wagons + +At this point, you are starting to get a feel for your friend's data and how it's used in the logistics program. +The train ID system works by assigning the locomotive an ID of `1` and then assigning the remainder of the wagons a randomly chosen ID greater than `1`. + +But then your friend had to connect two new wagons to the train and forgot to update the system! +Now the first two wagons in the `list` have to be moved to the back of the train, or everything will be out of order. + +Additionally, your friend just found a second `list` that appears to contain missing wagon IDs, and would like you to merge it together with the main wagon ID `list`. +All they can remember is that once the new wagons are moved to the end, the values from the second list should be placed directly after the designated locomotive. + +Your friend would be really grateful to you for fixing their mistakes and consolidating the data. + +Implement a function `fix_list_of_wagons` that takes two `lists` containing wagon IDs as the arguments. +It should reposition the first two items of the first list to the end, and then insert the values from the second list behind the locomotive ID (`1`). +The function should then `return` the `list` with the modifications. + +```python +>>> fix_list_of_wagons([2, 5, 1, 7, 4, 12, 6, 3, 13], [3, 17, 6, 15]) +[1, 3, 17, 6, 15, 7, 4, 12, 6, 3, 13, 2, 5] +``` + +## 3. Add missing stops + +Now that all the wagon data is correct, your friend would like you to update the system's routing information. +During the journey, the train will stop at a few stations to pick up and/or drop off cargo. +Each journey will have a different amount of stops. To simplify setting up the routing program your friend would like you to add the missing stops to the routing `dict`. + +Implement a function `add_missing_stops` that accepts a routing `dict` followed by an arbitrary amount of additional keyword arguments. These arguments could be in the form of a `dict` which holds one or more stops in order, or a number of `stop_number=city` pairs. +The function should then return the routing `dict`, updated with a key that holds a list of all the stops in order. + +```python +>>> add_missing_stops({"from": "Berlin", "to": "Hamburg"}, {"stop_1": "Hamburg", "stop_2": "Hannover", "stop_3": "Frankfurt"}) +{"from": "Berlin", "to": "Hamburg", "stops": ["Hamburg", "Hannover", "Frankfurt"]} + +>>> add_missing_stops({"from": "New York", "to": "Miami"}, stop_1="Washington, DC", stop_2="Charlotte", stop_3="Atlanta", stop_4="Jacksonville", stop_5="Orlando") +{"from": "New York", "to": "Miami", "stops": ["Washington, DC", "Charlotte", "Atlanta", "Jacksonville", "Orlando"]} +``` + +## 4. Extend routing information + +Your friend has been working on the routing program and has noticed that the routing information is missing some important details. +Initial routing information has been constructed as a `dict`, and your friend would like you to update it with the additions provided. +Every route requires slightly different information, so your friend would really prefer a generic solution. + +Implement a function `extend_route_information` that accepts two `dicts`. +The first `dict` contains which cities the train route moves between. + +The second `dict` contains other routing details such as train speed or length. +The function should return a consolidated `dict` with all routing information. + +```exercism/note +The second dict can contain different properties than the ones shown in the example. +``` + +```python +>>> extend_route_information({"from": "Berlin", "to": "Hamburg"}, {"length": "100", "speed": "50"}) +{"from": "Berlin", "to": "Hamburg", "length": "100", "speed": "50"} +``` + +## 5. Fix the wagon depot + +When your friend was surveying the wagon depot they noticed that the wagons were not getting stored in the correct order. +In addition to an ID, each wagon has a color that corresponds to the type of cargo it carries. +Wagons are stored in the depot in grids, with each column in the grid grouped by wagon color. + +In the control system, it appears that the lists of wagons to be stored in the depot have their _rows_ grouped by color. But for the storage grid to work correctly, each row has to have three different colors, so that the columns align properly. +Your friend would like you to help them sort out the wagon depot lists, so that the wagons get stored correctly. + +Implement a function `fix_wagon_depot` that accepts a nested `list`. +The first `list` contains the first row of wagons, the second `list` contains the second row of wagons and the third `list` contains the third row of wagons. All rows are of equal length. +Every wagon within a row is represented by a `tuple` with (``, ``). + +Your function should return a `list` with the three row `lists` reordered with the wagons swapped into their correct positions. + +```python +>>> fix_wagon_depot([ + [(2, "red"), (4, "red"),(8, "red")], + [(5, "blue"),(9, "blue"),(13,"blue")], + [(3, "orange"),(7, "orange"), (11, "orange")], + ]) + +[ +[(2, "red"),(5, "blue"),(3, "orange")], +[(4, "red"),(9, "blue"),(7, "orange")], +[(8, "red"),(13,"blue"),(11, "orange")] +] +``` diff --git a/exercises/concept/locomotive-engineer/.docs/introduction.md b/exercises/concept/locomotive-engineer/.docs/introduction.md new file mode 100644 index 00000000000..779a6e0504b --- /dev/null +++ b/exercises/concept/locomotive-engineer/.docs/introduction.md @@ -0,0 +1,289 @@ +# Unpacking and Multiple Assignment + +Unpacking refers to the act of extracting the elements of a collection, such as a `list`, `tuple`, or `dict`, using iteration. +Unpacked values can be assigned to variables within the same step. +With unpacking, there are some special operators used: `*` and `**`. +When unpacking a list or tuple, the `*` operator can be used to assign all the remaining elements to a variable. +When unpacking a dictionary, the `**` operator can be used to assign all the remaining key-value pairs to a variable. + +When these operators are used without a collection they will _pack_ a number of values into a `list`, `tuple`, or dictionary. +It is common to use this kind of behavior when creating functions that take an arbitrary number of arguments. + +Multiple assignment is the ability to assign multiple variables in one line. +This is done by separating the variables with a comma. + +```exercism/caution +`*` and `**` should not be confused with `*` and `**`. While `*` and `**` are used for multiplication and exponentiation respectively, `*` and `**` are used as packing and unpacking operators. +``` + +## Multiple assignment + +[Multiple assignment][multiple assignment] is the ability to assign multiple variables in one line. +This allows for code to be more concise and readable. +There has to be x number of variables on the left side of the `=` sign and x number of values on the right side of the `=` sign. +To separate the values, use a comma `,`: + +```python +>>> a, b = 1, 2 +>>> a +1 +``` + +Multiple assignment is not limited to one data type but can instead be used with any data type. +For example: + +```python +>>> a, b, c = 1, "Hello", True +>>> a +1 + +>>> b +'Hello' + +>>> c +True +``` + +## Unpacking + +```exercism/note +The examples below use lists but the same concepts apply to tuples. +``` + +In Python, it is possible to [unpack a `list`/`tuple`/`dictionary`][unpacking] into distinct variables. +Since values appear within lists in a specific order, it is therefore possible to _unpack_ a `list` into variables in the same order. + +Unpacking a list into variables: + +```python +>>> fruits = ["apple", "banana", "cherry"] +>>> x, y, z = fruits +>>> x +"apple" +``` + +If there are values that are not needed then you can use `_` to ignore those values: + +```python +>>> fruits = ["apple", "banana", "cherry"] +>>> _, _, z = fruits +>>> z +"cherry" +``` + +You can also do [deep unpacking][deep unpacking] on a `list`, which assigns values from a `list` within a `list` (_this is also known as nested list unpacking_): + +```python +>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]] +>>> [[a, b], [c, d]] = fruits_vegetables +>>> a +"apple" + +>>> d +"potato" +``` + +Deep unpacking and normal unpacking can be mixed together: + +```python +>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]] +>>> [a, [c, d]] = fruits_vegetables +>>> a +["apple", "banana"] + +>>> c +"carrot" +``` + +If the unpacking has variables with incorrect placement and/or an incorrect number of values, you will get a `ValueError`: + +```python +>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]] +>>> [[a, b], [d]] = fruits_vegetables + +ValueError: too many values to unpack (expected 1) +``` + +### Unpacking a list/tuple with `*` + +When [unpacking a `list`/`tuple`][packing and unpacking] you can use the `*` operator to capture the remainder values. +This can be used instead of slicing the `list`/`tuple`, which in some situations could be more readable. +For example, we can extract the first element below and then pack the remaining values into a new `list` without the first element: + +```python +>>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"] +>>> x, *last = fruits +>>> x +"apple" + +>>> last +["banana", "cherry", "orange", "kiwi", "melon", "mango"] +``` + +We can also extract the values at the beginning and end of the `list` while grouping all the values in the middle: + +```python +>>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"] +>>> x, *middle, y, z = fruits +>>> y +"melon" + +>>> middle +["banana", "cherry", "orange", "kiwi"] +``` + +### Unpacking a dictionary + +[Unpacking a dictionary][packing and unpacking] is a bit different than unpacking a `list`/`tuple`. +Iteration over dictionaries defaults to the `keys`. +So when unpacking a `dict`, you can only unpack the `keys` and not the `values`: +```python +>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} +>>> x, y, z = fruits_inventory +>>> x +"apple" +``` + +If you want to unpack the values then you can use the `values()` method: + +```python +>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} +>>> x, y, z = fruits_inventory.values() +>>> x +6 +``` + +## Packing + +As with unpacking, _packing_ uses the same `*` and `**` operators. +[Packing][packing and unpacking]] is the ability to group multiple values into one variable. +This is useful for when you want to _unpack_ values, make changes, and then _pack_ the results back into a variable. +It also makes it possible to perform merges on 2 or more `lists`/`tuples`/`dicts`. + +### Packing a list/tuple with `*` + +Packing a `list`/`tuple` is done by using the `*` operator +This will pack all the variables into a list/tuple. + +```python +>>> fruits = ["apple", "banana", "cherry"] +>>> more_fruits = ["orange", "kiwi", "melon", "mango"] +>>> combined_fruits_lists = [*fruits, *more_fruits] + +>>> combined_fruits_lists +["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"] +``` + +### Packing a dictionary with `**` + +Packing a dictionary is done by using the `**` operator. +This will pack all the variables into a dictionary. + +```python +>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} +>>> more_fruits_inventory = {"orange": 4, "kiwi": 1, "melon": 2, "mango": 3} +>>> combined_fruits_inventory = {**fruits_inventory, **more_fruits_inventory} + +>>> combined_fruits_inventory +{"apple": 6, "banana": 2, "cherry": 3, "orange": 4, "kiwi": 1, "melon": 2, "mango": 3} +``` + +## Usage of `*` and `**` with a function + +### Packing with function parameters + +When you have a function that accepts an arbitrary or large number of arguments, you can use [`*args` or `**kwargs`][args and kwargs] to pack or group those parameters together. +`*args` is used for packing/signaling an arbitrary number of positional (non-keyworded) arguments. +`**kwargs` is used for packing/signaling an arbitrary number of keyword arguments to a function. + +Usage of `*args`: + +```python +>>> def my_function(*args): +... print(args) + +>>> my_function(1, 2, 3) +(1, 2, 3) + +>>> my_function("Hello", "World") +("Hello", "World") + +>>> my_function(1, 2, 3, "Hello", "Mars") +(1, 2, 3, "Hello", "Mars") +``` + +Usage of `**kwargs`: + +```python +>>> def my_function(**kwargs): +... print(kwargs) + +>>> my_function(a=1, b=2, c=3) +{"a": 1, "b": 2, "c": 3} +``` + +```exercism/caution +[Arguments have to be structured][Positional and keyword arguments] like this: + +`def my_function(, *args, , **kwargs)` + +If you don't follow this order then you will get an error. +``` + +```python +>>> def my_function(a, b, *args): +... print(a) +... print(b) +... print(args) + +>>> my_function(1, 2, 3, 4, 5) +1 +2 +(3, 4, 5) +``` + +Writing arguments in an incorrect order will result in an error: + +```python +>>>def my_function(*args, a, b): +... print(args) + +>>>my_function(1, 2, 3, 4, 5) +Traceback (most recent call last): + File "c:\something.py", line 3, in + my_function(1, 2, 3, 4, 5) +TypeError: my_function() missing 2 required keyword-only arguments: 'a' and 'b' +``` + +### Unpacking into function calls + +You can use `*` to unpack a `list`/`tuple` of arguments into a function call. +This is very useful for functions that don't accept an `iterable` or `iterator`: +```python +>>> def my_function(a, b, c): +... print(c) +... print(b) +... print(a) + +numbers = [1, 2, 3] +>>> my_function(*numbers) +3 +2 +1 +``` + +Using `*` unpacking with the `zip()` function is another common use case. +Since `zip()` takes multiple iterables and returns a list of tuples with the values from each iterable grouped: + +```python +>>> values = (['x', 'y', 'z'], [1, 2, 3], [True, False, True]) +>>> a, *rest = zip(*values) +>>> rest +[('y', 2, False), ('z', 3, True)] +``` + +[multiple assignment]: https://www.geeksforgeeks.org/assigning-multiple-variables-in-one-line-in-python/ +[unpacking]: https://www.geeksforgeeks.org/unpacking-arguments-in-python/?ref=rp +[deep unpacking]: https://mathspp.com/blog/pydonts/deep-unpacking#deep-unpacking +[packing and unpacking]: https://www.geeksforgeeks.org/packing-and-unpacking-arguments-in-python/ +[args and kwargs]: https://www.geeksforgeeks.org/args-kwargs-python/ diff --git a/exercises/concept/locomotive-engineer/.meta/config.json b/exercises/concept/locomotive-engineer/.meta/config.json new file mode 100644 index 00000000000..76ecea037cc --- /dev/null +++ b/exercises/concept/locomotive-engineer/.meta/config.json @@ -0,0 +1,11 @@ +{ + "blurb": "Learn about unpacking and multiple assignment in Python while helping Linus with his train control system.", + "icon": "tracks-on-tracks-on-tracks", + "authors": ["meatball","BethanyG"], + "files": { + "solution": ["locomotive_engineer.py"], + "test": ["locomotive_engineer_test.py"], + "exemplar": [".meta/exemplar.py"] + }, + "forked_from": ["javascript/train-driver"] +} diff --git a/exercises/concept/locomotive-engineer/.meta/design.md b/exercises/concept/locomotive-engineer/.meta/design.md new file mode 100644 index 00000000000..af4109a9b0c --- /dev/null +++ b/exercises/concept/locomotive-engineer/.meta/design.md @@ -0,0 +1,69 @@ +# Design + +## TODO: Add introduction for this concept. +## Goal + +This concept exercise is meant to teach an understanding/use of `unpacking` and the `*` (splat) and `**` (double splat) operators in Python. + +
+ +## Learning objectives + +- Understand/use `unpacking` through the use of `*` and `**` _prefix_ operators in various scenarios + - `*` and `**` as _prefixes_ ..... not to be confused with `*` (_multiply_) and `**` (_exponentiation_) as _infix_, or mathematical operators (**consider a link in the links doc or a mention in dig deeper.**) + - ~~what happens in the process of "unpacking" - form, ordering, & iteration~~ (this will go in a **dig deeper** or the link docs.) + - use in arguments to `functions` + - use in argument _capture_ for `functions` (_aka passing an arbitrary number of arguments -- *args * & **kwargs_) + - ~~use in defining `keyword only arguments`~~ (_topic moved to arguments exercise_). + - use in iterable (_mainly `tuple` and `list`_) unpacking & packing + - use in `dict` unpacking & packing +- Understand/use `unpacking` via `multiple assignment` + - using `multiple assignment ` in place of `indexing` + - using `multiple assigment` + `*` in place of `slicing` + - ~~using "list-like" syntax & "tuple-like" syntax~~ + - unpacking plus "leftovers" via `*` +- Differences between straight `multiple assignment` and `*` & `**` +- Deep unpacking + + +## Concepts + +- `unpacking` +- `unpacking generalizations` +- `multiple assignment` + + +## Topics that are Out of scope + +- `classes` +- `comprehensions` +- `comprehensions` in `lambdas` +- `map()`, `filter()` or `functools.reduce()` in a `comprehension` +- `function-arguments` beyond explaining briefly how `*`, `**` work in function arguments. +- `functools` beyond `functools.reduce()`(_this will get its own exercise_) +- `generators` +- using an `assignment expression` or "walrus" operator (`:=`) alone or in a `lambda` + + +## Prerequisites + +- `basics` +- `bools` +- `comparisons` +- `dicts` +- `lists` +- `numbers` +- `strings` +- `tuples` + + +## Representer + +This exercise does not require any specific logic to be added to the [representer][representer] + +## Analyzer + +This exercise does not require any specific logic to be added to the [analyzer][analyzer]. + +[analyzer]: https://github.com/exercism/python-analyzer +[representer]: https://github.com/exercism/python-representer \ No newline at end of file diff --git a/exercises/concept/locomotive-engineer/.meta/exemplar.py b/exercises/concept/locomotive-engineer/.meta/exemplar.py new file mode 100644 index 00000000000..861cce2fde9 --- /dev/null +++ b/exercises/concept/locomotive-engineer/.meta/exemplar.py @@ -0,0 +1,48 @@ +"""Functions which helps the locomotive engineer to keep track of the train.""" + + +def get_list_of_wagons(*args): + return list(args) + + +def fix_list_of_wagons(each_wagons_id, missing_wagons): + """Fix the list of wagons. + + :param each_wagons_id: list - the list of wagons. + :param missing_wagons: list - the list of missing wagons. + :return: list - list of wagons. + """ + first, second, loctomotive, *rest = each_wagons_id + return [loctomotive, *missing_wagons, *rest, first, second] + + +def add_missing_stops(route, **kwargs): + """Add missing stops to route dict. + + :param route: dict - the dict of routing information. + :param **kwards: arbitrary number of stops. + :return: dict - updated route dictionary. + """ + return {**route, "stops": list(kwargs.values())} + + +def extend_route_information(route, more_route_information): + """Extend the route information with the more_route_information. + + :param route: dict - the route information. + :param more_route_information: dict - extra route information. + :return: dict - extended route information. + """ + return {**route, **more_route_information} + + +def fix_wagon_depot(wagons_rows): + """Fix the list of rows of wagons. + + :param wagons_rows: list[tuple] - the list of rows of wagons. + :return: list[tuple] - list of rows of wagons. + """ + + [*row_one], [*row_two], [*row_three] = zip(*wagons_rows) + + return [row_one, row_two, row_three] diff --git a/exercises/concept/locomotive-engineer/locomotive_engineer.py b/exercises/concept/locomotive-engineer/locomotive_engineer.py new file mode 100644 index 00000000000..6809958b3f4 --- /dev/null +++ b/exercises/concept/locomotive-engineer/locomotive_engineer.py @@ -0,0 +1,38 @@ +"""Functions which helps the locomotive engineer to keep track of the train.""" + +# TODO: define the 'get_list_of_wagons' function + + +# TODO: define the 'fixListOfWagons()' function +def fix_list_of_wagons(each_wagons_id, missing_wagons): + """Fix the list of wagons. + + :parm each_wagons_id: list - the list of wagons. + :parm missing_wagons: list - the list of missing wagons. + :return: list - list of wagons. + """ + pass + + +# TODO: define the 'add_missing_stops()' function + + +# TODO: define the 'extend_route_information()' function +def extend_route_information(route, more_route_information): + """Extend the route information with the more_route_information. + + :param route: dict - the route information. + :param more_route_information: dict - extra route information. + :return: dict - extended route information. + """ + pass + + +# TODO: define the 'fix_wagon_depot()' function +def fix_wagon_depot(wagons_rows): + """Fix the list of rows of wagons. + + :param wagons_rows: list[tuple] - the list of rows of wagons. + :return: list[tuple] - list of rows of wagons. + """ + pass diff --git a/exercises/concept/locomotive-engineer/locomotive_engineer_test.py b/exercises/concept/locomotive-engineer/locomotive_engineer_test.py new file mode 100644 index 00000000000..537ac75b0e1 --- /dev/null +++ b/exercises/concept/locomotive-engineer/locomotive_engineer_test.py @@ -0,0 +1,82 @@ +import unittest +import pytest +from locomotive_engineer import (get_list_of_wagons, + fix_list_of_wagons, + add_missing_stops, + extend_route_information, + fix_wagon_depot) + +class InventoryTest(unittest.TestCase): + + @pytest.mark.task(taskno=1) + def test_get_list_of_wagons(self): + input_data = [(1,5,2,7,4), (1,5), (1,), (1,9,3), (1,10,6,3,9,8,4,14,24,7)] + output_data = [[1,5,2,7,4], [1,5], [1], [1,9,3], [1,10,6,3,9,8,4,14,24,7]] + + for variant, (input_data, output_data) in enumerate(zip(input_data, output_data), start=1): + with self.subTest(f"variation #{variant}", input_data=input_data, output_data=output_data): + self.assertEqual(get_list_of_wagons(*input_data), output_data) + + @pytest.mark.task(taskno=2) + def test_fix_list_of_wagons(self): # One extra case needed at first + input_data = [([3, 27, 1, 14, 10, 4, 12, 6, 23, 17, 13, 22, 28, 19], [8, 10, 5, 9, 36, 7, 20]), + ([4, 2, 1], [8, 6, 15]), + ([3, 14, 1, 25, 7, 19, 10], [8, 6, 4, 5, 9, 21, 2, 13]) + ] + output_data = [[1, 8, 10, 5, 9, 36, 7, 20, 14, 10, 4, 12, 6, 23, 17, 13, 22, 28, 19, 3, 27], + [1, 8, 6, 15, 4, 2], + [1, 8, 6, 4, 5, 9, 21, 2, 13, 25, 7, 19, 10, 3, 14] + ] + for variant, (input_data, output_data) in enumerate(zip(input_data, output_data), start=1): + with self.subTest(f"variation #{variant}", input_data=input_data, output_data=output_data): + self.assertEqual(fix_list_of_wagons(input_data[0], input_data[1]), output_data) + + @pytest.mark.task(taskno=3) + def test_add_missing_stops(self): + input_data = (({"from": "Berlin", "to": "Hamburg"}, {"stop_1": "Lepzig", "stop_2": "Hannover", "stop_3": "Frankfurt"}), + ({"from": "Paris", "to": "London"}, {"stop_1": "Lille"}), + ({"from": "New York", "to": "Philadelphia"},{}), + ({"from": "Gothenburg", "to": "Copenhagen"}, {"stop_1": "Kungsbacka", "stop_2": "Varberg", "stop_3": "Halmstad", "stop_4": "Angelholm", "stop_5": "Lund", "stop_6": "Malmo"}) + ) + output_data = [{"from": "Berlin", "to": "Hamburg", "stops": ["Lepzig", "Hannover", "Frankfurt"]}, + {"from": "Paris", "to": "London", "stops": ["Lille"]}, + {"from": "New York", "to": "Philadelphia", "stops": []}, + {"from": "Gothenburg", "to": "Copenhagen", "stops": ["Kungsbacka", "Varberg", "Halmstad", "Angelholm", "Lund", "Malmo"]} + ] + for variant, (input_data, output_data) in enumerate(zip(input_data, output_data), start=1): + with self.subTest(f"variation #{variant}", input_data=input_data, output_data=output_data): + self.assertEqual(add_missing_stops(input_data[0], **input_data[1]), output_data) + + @pytest.mark.task(taskno=4) + def test_extend_route_information(self): + input_data = [({"from": "Berlin", "to": "Hamburg"}, {"timeOfArrival": "12:00", "precipitation": "10", "temperature": "5", "caboose": "yes"}), + ({"from": "Paris", "to": "London"}, {"timeOfArrival": "10:30", "temperature": "20", "length": 15}), + ({"from": "Gothenburg", "to": "Copenhagen"}, {"precipitation": "1", "timeOfArrival": "21:20", "temperature": "-6"})] + output_data = [{"from": "Berlin", "to": "Hamburg", "timeOfArrival": "12:00", "precipitation": "10", "temperature": "5", "caboose": "yes"}, + {"from": "Paris", "to": "London", "timeOfArrival": "10:30", "temperature": "20", "length": 15}, + {"from": "Gothenburg", "to": "Copenhagen", "precipitation": "1", "timeOfArrival": "21:20", "temperature": "-6"} + ] + + for variant, (input_data, output_data) in enumerate(zip(input_data, output_data), start=1): + with self.subTest(f"variation #{variant}", input_data=input_data, output_data=output_data): + self.assertEqual(extend_route_information(input_data[0], input_data[1]), output_data) + + @pytest.mark.task(taskno=5) + def test_fix_wagon_depot(self): + input_data = ( + [[(2, "red"), (4, "red"), (8, "red")], [(5, "blue"), (9, "blue"), (13, "blue")], [(3, "orange"), (7, "orange"), (11, "orange")]], + [[(6, "blue"), (10, "blue"), (14, "blue")], [(7, "red"), (4, "red"), (2, "red")], [(3, "orange"), (11, "orange"), (15, "orange")]], + [[(7, "pink"), (4, "pink"), (2, "pink")],[(10, "green"), (6, "green"), (14, "green")], [(9, "yellow"), (5, "yellow"), (13, "yellow")]], + [[(3, "purple"), (11, "purple"), (15, "purple")], [(20, "black"), (16, "black"), (12, "black")], [(19, "white"), (17, "white"), (18, "white")]] + ) + + output_data = ( + [[(2, "red"), (5, "blue"), (3, "orange")],[(4, "red"), (9, "blue"), (7, "orange")], [(8, "red"), (13, "blue"), (11, "orange")]], + [[(6, "blue"), (7, "red"), (3, "orange")],[(10, "blue"), (4, "red"), (11, "orange")], [(14, "blue"), (2, "red"), (15, "orange")]], + [[(7, "pink"), (10, "green"), (9, "yellow")], [(4, "pink"), (6, "green"), (5, "yellow")], [(2, "pink"), (14, "green"), (13, "yellow")]], + [[(3, "purple"), (20, "black"), (19, "white")], [(11, "purple"), (16, "black"), (17, "white")], [(15, "purple"), (12, "black"), (18, "white")]] + ) + + for variant, (input_data, output_data) in enumerate(zip(input_data, output_data), start=1): + with self.subTest(f"variation #{variant}", input_data=input_data, output_data=output_data): + self.assertEqual(fix_wagon_depot(input_data), output_data) \ No newline at end of file