Skip to content

Commit d27d526

Browse files
HumphreyYangYang, Humphrey (Data61, Clayton)mmcky
authored
[More Language Features] Adding * Operators (#258)
* add splat * update examples * redo examples * Add more explanations * simplify code * fill in more details * fix typos * update examples * update comments * GBM version * A simpler example * update wordings * change the parameter slightly * delete l.356 Co-authored-by: Yang, Humphrey (Data61, Clayton) <[email protected]> Co-authored-by: mmcky <[email protected]> Co-authored-by: Humphrey Yang <[email protected]>
1 parent 62357ca commit d27d526

File tree

1 file changed

+173
-0
lines changed

1 file changed

+173
-0
lines changed

lectures/python_advanced_features.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,179 @@ tags: [raises-exception]
290290
max(y)
291291
```
292292

293+
## `*` and `**` Operators
294+
295+
`*` and `**` are convenient and widely used tools to unpack lists and tuples and to allow users to define functions that take arbitrarily many arguments as input.
296+
297+
In this section, we will explore how to use them and distinguish their use cases.
298+
299+
300+
### Unpacking Arguments
301+
302+
When we operate on a list of parameters, we often need to extract the content of the list as individual arguments instead of a collection when passing them into functions.
303+
304+
Luckily, the `*` operator can help us to unpack lists and tuples into [*positional arguments*](https://63a3119f7a9a1a12f59e7803--epic-agnesi-957267.netlify.app/functions.html#keyword-arguments) in function calls.
305+
306+
To make things concrete, consider the following examples:
307+
308+
Without `*`, the `print` function prints a list
309+
310+
```{code-cell} python3
311+
l1 = ['a', 'b', 'c']
312+
313+
print(l1)
314+
```
315+
316+
While the `print` function prints individual elements since `*` unpacks the list into individual arguments
317+
318+
```{code-cell} python3
319+
print(*l1)
320+
```
321+
322+
Unpacking the list using `*` into positional arguments is equivalent to defining them individually when calling the function
323+
324+
```{code-cell} python3
325+
print('a', 'b', 'c')
326+
```
327+
328+
However, `*` operator is more convenient if we want to reuse them again
329+
330+
```{code-cell} python3
331+
l1.append('d')
332+
333+
print(*l1)
334+
```
335+
336+
Similarly, `**` is used to unpack arguments.
337+
338+
The difference is that `**` unpacks *dictionaries* into *keyword arguments*.
339+
340+
`**` is often used when there are many keyword arguments we want to reuse.
341+
342+
For example, assuming we want to draw multiple graphs using the same graphical settings,
343+
it may involve repetitively setting many graphical parameters, usually defined using keyword arguments.
344+
345+
In this case, we can use a dictionary to store these parameters and use `**` to unpack dictionaries into keyword arguments when they are needed.
346+
347+
Let's walk through a simple example together and distinguish the use of `*` and `**`
348+
349+
```{code-cell} python3
350+
import numpy as np
351+
import matplotlib.pyplot as plt
352+
353+
# Set up the frame and subplots
354+
fig, ax = plt.subplots(2, 1)
355+
plt.subplots_adjust(hspace=0.7)
356+
357+
# Create a function that generates synthetic data
358+
def generate_data(β_0, β_1, σ=30, n=100):
359+
x_values = np.arange(0, n, 1)
360+
y_values = β_0 + β_1 * x_values + np.random.normal(size=n, scale=σ)
361+
return x_values, y_values
362+
363+
# Store the keyword arguments for lines and legends in a dictionary
364+
line_kargs = {'lw': 1.5, 'alpha': 0.7}
365+
legend_kargs = {'bbox_to_anchor': (0., 1.02, 1., .102),
366+
'loc': 3,
367+
'ncol': 4,
368+
'mode': 'expand',
369+
'prop': {'size': 7}}
370+
371+
β_0s = [10, 20, 30]
372+
β_1s = [1, 2, 3]
373+
374+
# Use a for loop to plot lines
375+
def generate_plots(β_0s, β_1s, idx, line_kargs, legend_kargs):
376+
label_list = []
377+
for βs in zip(β_0s, β_1s):
378+
379+
# Use * to unpack tuple βs and the tuple output from the generate_data function
380+
# Use ** to unpack the dictionary of keyword arguments for lines
381+
ax[idx].plot(*generate_data(*βs), **line_kargs)
382+
383+
label_list.append(f'$β_0 = {βs[0]}$ | $β_1 = {βs[1]}$')
384+
385+
# Use ** to unpack the dictionary of keyword arguments for legends
386+
ax[idx].legend(label_list, **legend_kargs)
387+
388+
generate_plots(β_0s, β_1s, 0, line_kargs, legend_kargs)
389+
390+
# We can easily reuse and update our parameters
391+
β_1s.append(-2)
392+
β_0s.append(40)
393+
line_kargs['lw'] = 2
394+
line_kargs['alpha'] = 0.4
395+
396+
generate_plots(β_0s, β_1s, 1, line_kargs, legend_kargs)
397+
plt.show()
398+
```
399+
400+
In this example, `*` unpacked the zipped parameters `βs` and the output of `generate_data` function stored in tuples,
401+
while `**` unpacked graphical parameters stored in `legend_kargs` and `line_kargs`.
402+
403+
To summarize, when `*list`/`*tuple` and `**dictionary` are passed into *function calls*, they are unpacked into individual arguments instead of a collection.
404+
405+
The difference is that `*` will unpack lists and tuples into *positional arguments*, while `**` will unpack dictionaries into *keyword arguments*.
406+
407+
### Arbitrary Arguments
408+
409+
When we *define* functions, it is sometimes desirable to allow users to put as many arguments as they want into a function.
410+
411+
You might have noticed that the `ax.plot()` function could handle arbitrarily many arguments.
412+
413+
If we look at the [documentation](https://github.com/matplotlib/matplotlib/blob/v3.6.2/lib/matplotlib/axes/_axes.py#L1417-L1669) of the function, we can see the function is defined as
414+
415+
```
416+
Axes.plot(*args, scalex=True, scaley=True, data=None, **kwargs)
417+
```
418+
419+
We found `*` and `**` operators again in the context of the *function definition*.
420+
421+
In fact, `*args` and `**kargs` are ubiquitous in the scientific libraries in Python to reduce redundancy and allow flexible inputs.
422+
423+
`*args` enables the function to handle *positional arguments* with a variable size
424+
425+
```{code-cell} python3
426+
l1 = ['a', 'b', 'c']
427+
l2 = ['b', 'c', 'd']
428+
429+
def arb(*ls):
430+
print(ls)
431+
432+
arb(l1, l2)
433+
```
434+
435+
The inputs are passed into the function and stored in a tuple.
436+
437+
Let's try more inputs
438+
439+
```{code-cell} python3
440+
l3 = ['z', 'x', 'b']
441+
arb(l1, l2, l3)
442+
```
443+
444+
Similarly, Python allows us to use `**kargs` to pass arbitrarily many *keyword arguments* into functions
445+
446+
```{code-cell} python3
447+
def arb(**ls):
448+
print(ls)
449+
450+
# Note that these are keyword arguments
451+
arb(l1=l1, l2=l2)
452+
```
453+
454+
We can see Python uses a dictionary to store these keyword arguments.
455+
456+
Let's try more inputs
457+
458+
```{code-cell} python3
459+
arb(l1=l1, l2=l2, l3=l3)
460+
```
461+
462+
Overall, `*args` and `**kargs` are used when *defining a function*; they enable the function to take input with an arbitrary size.
463+
464+
The difference is that functions with `*args` will be able to take *positional arguments* with an arbitrary size, while `**kargs` will allow functions to take arbitrarily many *keyword arguments*.
465+
293466
## Decorators and Descriptors
294467

295468
```{index} single: Python; Decorators

0 commit comments

Comments
 (0)