@@ -290,6 +290,179 @@ tags: [raises-exception]
290290max(y)
291291```
292292293+## `*` 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
294467295468```{index} single: Python; Decorators