class: title-slide, left, bottom # Lecture 14 ---- ## **DANL 100: Programming for Data Analytics** ### Byeong-Hak Choe ### October 18, 2022 --- class: inverse, center, middle # Dictionaries and Sets <html><div style='float:left'></div><hr color='#EB811B' size=1px width=796px></html> --- # Dictionaries - A *dictionary* is similar to a list, but the order of items doesn't matter, and they aren't selected by an offset such as 0 or 1. - Instead, we specify a unique *key* to associate with each value. - This *key* is often a string, but it can actually be any of Python's immutable types: boolean, integer, float, tuple, string, and others. - Dictionaries are mutable, so we can add, delete, and change their key-value elements. --- # Dictionaries ### <p style="color:#00449E"> Create with `{}` </p> - To create a dictionary, we place curly brackets (`{}`) around comma-separated **key : value** pairs. ```python empty_dict = {} empty_dict ``` Let's make a small dictionary with quotes from Ambrose Bierce's *The Devil's Dictionary*: ```python bierce = {"day": "A period of twenty-four hours, mostly misspent", "positive": "Mistaken at the top of one's voice", "misfortune": "The kind of fortune that never misses"} bierce ``` --- # Dictionaries ### <p style="color:#00449E"> Create with `dict()` </p> - We can also create a dictionary by passing named arguments and values to the `dict()` function. ```python acme_customer = {'first': 'Wile', 'middle': 'E', 'last': 'Coyote'} acme_customer acme_customer = dict(first = "Wile", middle = "E", last = "Coyote") acme_customer ``` - The argument names in `dict()` need to be legal variable names (no spaces, no reserved words): ```python x = dict(name="Elmer", def="hunter") ``` --- # Dictionaries ### <p style="color:#00449E"> Convert with `dict()` </p> - We can use `dict()` to convert two-value sequences into a dictionary. - We might run into such key-value sequences at times, such as "Geneseo, 20, Brockport, 14." - The first item in each sequence is used as the key and the second as the value. .panelset[ .panel[.panel-name[lol] - *lol* (a list of two-item lists): ```python lol = [ ['a', 'b'], ['c', 'd'], ['e', 'f'] ] dict(lol) ``` ] .panel[.panel-name[lot] - *lot* (a list of two-item tuples): ```python lot = [ ('a', 'b'), ('c', 'd'), ('e', 'f') ] dict(lot) ``` ] .panel[.panel-name[tol] - *tol* (a tuple of two-item lists): ```python tol = ( ['a', 'b'], ['c', 'd'], ['e', 'f'] ) dict(tol) ``` ] .panel[.panel-name[los] - *los* (a list of two-character strings): ```python los = [ 'ab', 'cd', 'ef' ] dict(los) ``` ] .panel[.panel-name[tos] - *tos* (a tuple of two-character strings): ```python tos = ( 'ab', 'cd', 'ef' ) dict(tos) ``` ] ] --- # Dictionaries ### <p style="color:#00449E"> Add or Change an Item by `[ key ]` </p> - Adding an item to a dictionary is not complicated. - Just refer to the item by its *key* and assign a *value*. - If the key was already present in the dictionary, the existing value is replaced by the new one. - If the key is new, it's added to the dictionary with its value. --- # Dictionaries ### <p style="color:#00449E"> Add or Change an Item by `[ key ]` </p> .panelset[ .panel[.panel-name[st_city] - Let's make a dictionary of some pairs of US cities and states, using their states as *keys* and city names as *values*: ```python st_city = { 'MN': 'Rochester', 'WY': 'Laramie', 'CO': 'Denver' } st_city ``` ] .panel[.panel-name[Rocjester] - Here, we want to add Rochester, NY. - Here's an attempt to add it, but it spelled wrong: ```python st_city['NY'] = 'Rocjester' st_city ``` ] .panel[.panel-name[Rochester] - Here's some repair code: ```python st_city['NY'] = 'Rochester' st_city ``` ] .panel[.panel-name[city_st] - Remember that dictionary keys must be unique. - That's why we used state names for keys instead of city names. - If we use a key more than once, the last value wins: ```python city_st = { 'Rochester': 'MN', 'Laramie': 'WY', 'Denver': 'CO', 'Rochester': 'NY' } city_st ``` ] ] --- # Dictionaries ### <p style="color:#00449E"> Get an Item by `[key]` or with `get()` </p> .panelset[ .panel[.panel-name[`[key]`] - We specify the dictionary and key to get the corresponding value: ```python city_st = { 'Rochester': 'MN', 'Laramie': 'WY', 'Denver': 'CO', 'Rochester': 'NY' } city_st['Denver'] ``` ] .panel[.panel-name[`[key]`] - If the key is not present in the dictionary, we'll get an exception: ```python city_st['Buffalo'] ``` - There are two good ways to avoid this: `in` and `get()` ] .panel[.panel-name[`in`] - Test for the key at the outset by using `in`: ```python 'Buffalo' in city_st ``` ] .panel[.panel-name[`get()`] - Use the special dictionary `get()` function. - If the key exists, we get its value: ```python city_st.get('Denver') ``` ] .panel[.panel-name[`get()`] - Use the special dictionary `get()` function. - If the key does not exist and the optional value is provided, we get the optional value: ```python city_st.get('Buffalo', 'Not in city_st') ``` ] .panel[.panel-name[`get()`] - Use the special dictionary `get()` function. - If the key does not exist and the optional value is not provided, we get `None`, which displays nothing in the console. ```python city_st.get('Buffalo') ``` ] ] --- # Dictionaries .panelset[ .panel[.panel-name[`keys()`] ### <p style="color:#00449E"> Get All Keys with `keys()` </p> - We can use `keys()` to get all of the keys in a dictionary. ```python signals = {'green': 'go', 'yellow': 'go faster', 'red': 'smile for the camera'} signals.keys() list( signals.keys() ) # to turn the result into a list object. ``` ] .panel[.panel-name[`values()`] ### <p style="color:#00449E"> Get All Values with values() </p> - To obtain all the values in a dictionary, use `values()`: ```python signals = {'green': 'go', 'yellow': 'go faster', 'red': 'smile for the camera'} signals.values() list( signals.values() ) # to turn the result into a list object. ``` ] .panel[.panel-name[`items()`] ### <p style="color:#00449E"> Get All Key-Value Pairs with `items()` </p> - When we want to get all the key-value pairs from a dictionary, use the `items()` function: ```python signals = {'green': 'go', 'yellow': 'go faster', 'red': 'smile for the camera'} signals.items() list( signals.items() ) # to turn the result into a list object. ``` ] .panel[.panel-name[`items()`] ### <p style="color:#00449E"> Get Length with `len()` </p> - Count your key-value pairs: ```python signals = {'green': 'go', 'yellow': 'go faster', 'red': 'smile for the camera'} len(signals) ``` ] .panel[.panel-name[{**a, **b}] ### <p style="color:#00449E"> Combine Dictionaries with `{**a, **b}` </p> - Starting with Python 3.5, there's a new way to merge dictionaries, using the `**`: ```python first = {'a': 'agony', 'b': 'bliss'} second = {'b': 'bagels', 'c': 'candy'} {**first, **second} third = {'d': 'donuts'} {**first, **third, **second} ``` ] .panel[.panel-name[`update()`] ### <p style="color:#00449E"> Combine Dictionaries with `update()` </p> - We can use the `update()` function to copy the keys and values of one dictionary into another. - Let's update `st_city` with `other_city` ```python st_city = { 'MN': 'Rochester', 'WY': 'Laramie', 'CO': 'Denver', 'NY': 'Rochester' } other_city = {'MA': 'Boston'} st_city.update(other_city) ``` ] .panel[.panel-name[`update()`] ### <p style="color:#00449E"> Combine Dictionaries with `update()` </p> - What happens if the second dictionary has the same key as the dictionary into which it's being merged? ```python first = {'a': 1, 'b': 2} second = {'b': 'platypus'} first.update(second) first ``` ] ] --- # Dictionaries .panelset[ .panel[.panel-name[`del`] ### <p style="color:#00449E"> Delete an Item by Key with `del` </p> - We can delete an item in a dictionary with `del`: ```python del st_city['NY'] st_city ``` ] .panel[.panel-name[`pop()`] ### <p style="color:#00449E"> Get an Item by Key and Delete It with `pop()` </p> - `pop()` combines `get()` and `del`. - If we give `pop()` a key and it exists in the dictionary, it returns the matching value and deletes the key-value pair. - If the key doesn't exist but we give `pop()` a second default argument, all is well and the dictionary is not changed: ```python len(st_city) st_city.pop('MN') # Check len(st_city) st_city.pop('MN') # If it doesn't exist, it raises an exception: st_city.pop('MN', 'Not exist') ``` ] .panel[.panel-name[`clear()`] ### <p style="color:#00449E"> Delete All Items with `clear()` </p> - To delete all keys and values from a dictionary, use `clear()` or just reassign an empty dictionary (`{}`) to the name: ```python st_city.clear() st_city st_city = { 'MN': 'Rochester', 'WY': 'Laramie', 'CO': 'Denver', 'NY': 'Rochester' } st_city = {} st_city ``` ] .panel[.panel-name[`in`] ### <p style="color:#00449E"> Test for a Key with `in` </p> - If we want to know whether a key exists in a dictionary, use `in`: ```python st_city = { 'MN': 'Rochester', 'WY': 'Laramie', 'CO': 'Denver', 'NY': 'Rochester' } 'NY' in st_city 'WY' in st_city 'CA' in st_city ``` ] .panel[.panel-name[`=`] ### <p style="color:#00449E"> Assign with `=` </p> - If we make a change to a dictionary, it will be reflected in all the names that refer to it: ```python signals = { 'green': 'go', 'yellow': 'go faster', 'red': 'smile for the camera' } save_signals = signals signals['blue'] = 'confuse everyone' signals ``` - What are in `save_signals` now? ] .panel[.panel-name[`copy()`] ### <p style="color:#00449E"> Copy with `copy()` </p> - To actually copy keys and values from a dictionary to another dictionary and avoid this, we can use `copy()`: ```python signals = { 'green': 'go', 'yellow': 'go faster', 'red': 'smile for the camera' } original_signals = signals.copy() signals['blue'] = 'confuse everyone' signals ``` - What are in `original_signals` now? ] .panel[.panel-name[`(1)`] ### <p style="color:#00449E"> Copy Everything with `deepcopy()` </p> - Suppose that the value for *red* in `signals` was a list instead of a single string: ```python signals = { 'green': 'go', 'yellow': 'go faster', 'red': ['stop', 'smile'] } signals_copy = signals.copy() signals ``` - What are in `signals_copy`? ] .panel[.panel-name[`(2)`] ### <p style="color:#00449E"> Copy Everything with `deepcopy()` </p> - Let's change one of the values in the *red* list: ```python signals = { 'green': 'go', 'yellow': 'go faster', 'red': ['stop', 'smile'] } signals_copy = signals.copy() signals['red'][1] = 'sweat' signals ``` - What are in `signals_copy`? ] .panel[.panel-name[`deepcopy()`] ### <p style="color:#00449E"> Copy Everything with `deepcopy()` </p> - To avoid this, we can use `deepcopy()`: ```python import copy signals = { 'green': 'go', 'yellow': 'go faster', 'red': ['stop', 'smile'] } signals_deepcopy = copy.deepcopy(signals) signals signals_deepcopy signals['red'][1] = 'sweat' signals ``` - What are in `signals_deepcopy`? ] ] --- # Dictionaries .panelset[ .panel[.panel-name[comparison] ### <p style="color:#00449E"> Compare Dictionaries </p> - Much like lists and tuples, dictionaries can be compared with the simple comparison operators `==` and `!=`: ```python a = {1:1, 2:2, 3:3} b = {3:3, 1:1, 2:2} a == b a != b a <= b # does it work? ``` ] .panel[.panel-name[`for`] ### <p style="color:#00449E"> Iterate with `for` and `in` </p> - Iterating over a dictionary (or its `keys()` function) returns the keys. ```python accusation = {'room': 'ballroom', 'weapon': 'lead pipe', 'person': 'Col. Mustard'} for card in accusation: # or, for card in accusation.keys(): print(card) ``` ] .panel[.panel-name[`values()`] ### <p style="color:#00449E"> Iterate with `for` and `in` </p> - To iterate over the values rather than the keys, we use the dictionary's `values()` function: ```python accusation = {'room': 'ballroom', 'weapon': 'lead pipe', 'person': 'Col. Mustard'} for value in accusation.values(): print(value) ``` ] .panel[.panel-name[`items()`] ### <p style="color:#00449E"> Iterate with `for` and `in` </p> - To return both the key and value as a tuple, we can use the `items()` function: ```python accusation = {'room': 'ballroom', 'weapon': 'lead pipe', 'person': 'Col. Mustard'} for item in accusation.items(): print(item) ``` ] .panel[.panel-name[`items()`] ### <p style="color:#00449E"> Iterate with `for` and `in` </p> - For each tuple returned by `items()`, let's assign the first value (the key) to *card*, and the second (the value) to *contents*: ```python accusation = {'room': 'ballroom', 'weapon': 'lead pipe', 'person': 'Col. Mustard'} for card, contents in accusation.items(): print('Card', card, 'has the contents', contents) ``` ] .panel[.panel-name[comprehensions] ### <p style="color:#00449E"> Dictionary Comprehensions </p> - Dictionaries also have comprehensions. - The simplest form of dictionary comprehension looks like this: <img src="../lec_figs/int-py-dict-comprehension.png" width="80%" style="display: block; margin: auto;" /> ] .panel[.panel-name[comprehensions] ### <p style="color:#00449E"> Dictionary Comprehensions </p> - Let's run a loop over each of the seven letters in the string 'letters' and counting how many times that letter appears. ```python word = 'letters' letter_counts = {letter: word.count(letter) for letter in word} letter_counts ``` - How many times do we count 't' here? ] .panel[.panel-name[comprehensions w/ `set()`] ### <p style="color:#00449E"> Dictionary Comprehensions </p> - Let's run a loop over each of the seven letters in the string 'letters' and counting how many times that letter appears. ```python word = 'letters' letter_counts = {letter: word.count(letter) for letter in set(word)} letter_counts ``` - How many times do we count 't' here? ] .panel[.panel-name[comprehensions w/ if] ### <p style="color:#00449E"> Dictionary Comprehensions </p> - Dictionary comprehensions can also have *if* tests and multiple *for* clauses: <img src="../lec_figs/int-py-dict-comprehension-if.png" width="95%" style="display: block; margin: auto;" /> ```python vowels = 'aeiou' word = 'onomatopoeia' vowel_counts = {letter: word.count(letter) for letter in set(word) if letter in vowels} vowel_counts ``` ] ] --- # Sets - A set is like a dictionary with its values thrown away, leaving only the keys. - As with a dictionary, each key must be unique. - We use a set when we only want to know that something exists, and nothing else about it. - Use a dictionary if we want to attach some information to the key as a value. --- # Sets ### <p style="color:#00449E"> Common things to do with sets </p> <img src="../lec_figs/int-py-fig8-1.png" width="55%" style="display: block; margin: auto;" /> - The null or empty set is a set with zero elements. --- # Sets .panelset[ .panel[.panel-name[`set()`] ### <p style="color:#00449E"> Create with `set()` </p> - To create a set, we use the `set()` function or enclose one or more comma-separated values in curly brackets: ```python empty_set = set() empty_set even_numbers = {0, 2, 4, 6, 8} even_numbers odd_numbers = {1, 3, 5, 7, 9} odd_numbers ``` ] .panel[.panel-name[`set()`] ### <p style="color:#00449E"> Convert with `set()` </p> - We can create a set from a list, string, tuple, or dictionary, discarding any duplicate values. ```python set( 'letters' ) set( ['Dasher', 'Dancer', 'Prancer', 'Mason-Dixon'] ) set( ('Ummagumma', 'Echoes', 'Atom Heart Mother') ) set( {'apple': 'red', 'orange': 'orange', 'cherry': 'red'} ) ``` - Sets are unordered. ] .panel[.panel-name[`len()`] ### <p style="color:#00449E"> Get Length with `len()` </p> - Let's count our *reindeer*: ```python reindeer = set( ['Dasher', 'Dancer', 'Prancer', 'Mason-Dixon'] ) len(reindeer) ``` ] .panel[.panel-name[`add()`] ### <p style="color:#00449E"> Add an Item with `add()` </p> - Throw another item into a set with the set `add()` method ```python s = set((1,2,3)) s s.add(4) s ``` ] .panel[.panel-name[`remove()`] ### <p style="color:#00449E"> Delete an Item with `remove()` </p> - We can delete a value from a set by value: ```python s = set((1,2,3)) s.remove(3) s ``` ] .panel[.panel-name[`for`] ### <p style="color:#00449E"> Iterate with `for` and `in` </p> - We can iterate over all items in a set: ```python furniture = set(('sofa', 'ottoman', 'table')) for piece in furniture: print(piece) ``` ] ] --- # Sets ### <p style="color:#00449E"> Test for a Value with `in` </p> .panelset[ .panel[.panel-name[`drinks`] - Here is a dictionary called `drinks`. - Each key is the name of a mixed drink, and the corresponding value is a set of that drink's ingredients: - A set is just a bunch of *values*, and a dictionary contains *key : value* pairs. ```python drinks = { 'martini': {'vodka', 'vermouth'}, 'black russian': {'vodka', 'kahlua'}, 'white russian': {'cream', 'kahlua', 'vodka'}, 'manhattan': {'rye', 'vermouth', 'bitters'}, 'screwdriver': {'orange juice', 'vodka'} } ``` ] .panel[.panel-name[`vodka`] - Print drinks that contain vodka: ```python for name, contents in drinks.items(): if 'vodka' in contents: print(name) ``` ] .panel[.panel-name[`vodka`] - Print drinks that neither contain either `cream` nor `vermouth` but do `vodka`: ```python for name, contents in drinks.items(): if 'vodka' in contents \ and not ('vermouth' in contents or 'cream' in contents): print(name) ``` ] ] --- # Sets ### <p style="color:#00449E"> Combinations and Operators </p> .panelset[ .panel[.panel-name[`drinks`] - Here are some examples to consider for set combinations and operations. ```python drinks = { 'martini': {'vodka', 'vermouth'}, 'black russian': {'vodka', 'kahlua'}, 'white russian': {'cream', 'kahlua', 'vodka'} } bruss = drinks['black russian'] wruss = drinks['white russian'] a = {1, 2} b = {2, 3} ``` ] .panel[.panel-name[intersection] - We get the intersection (members common to both sets) with the special punctuation symbol `&`. - The set `intersection()` function does the same. ```python a & b a.intersection(b) bruss & wruss ``` ] .panel[.panel-name[union] - Let's get the union (members of either set) by using `|` or the set `union()` function: ```python a | b a.union(b) bruss | wruss ``` ] .panel[.panel-name[difference] - The difference (members of the first set but not the second) is obtained by using the character `-` or the `difference()` function: ```python a - b a.difference(b) bruss - wruss wruss - bruss ``` ] .panel[.panel-name[exclusive] - The *exclusive* or (items in one set or the other, but not both) uses `^` or `symmetric_difference()`: ```python a ^ b a.symmetric_difference(b) bruss ^ wruss ``` ] .panel[.panel-name[subset] - We can check whether one set is a subset of another (all members of the first set are also in the second set) by using `<=` or `issubset()`: ```python a <= b a.issubset(b) bruss <= wruss a <= a # any set is a subset of itself? a.issubset(a) ``` ] .panel[.panel-name[subset] - To be a *proper subset*, the second set needs to have all the members of the first and more. - Calculate it by using `<`: ```python a < b b < a bruss < wruss ``` ] .panel[.panel-name[superset] - A *superset* is the opposite of a subset (all members of the second set are also members of the first). This uses `>=` or `issuperset()`: ```python a >= b a.issuperset(b) wruss >= bruss a >= a # any set is a superset of itself? a.issuperset(a) ``` ] .panel[.panel-name[superset] - To be a *proper superset*, the first set needs to have all the members of the second and more. - Calculate it by using `>`: ```python a > b wruss > bruss a > a ``` ] .panel[.panel-name[combinations] - What if we want to check for combinations of set values? - Suppose that we want to find any drink that has orange juice or vermouth. - Let's use the *set intersection operator*, which is an ampersand (`&`): ```python for name, contents in drinks.items(): if contents & {'vermouth', 'orange juice'}: print(name) ``` ] .panel[.panel-name[combinations] - Let's rewrite the example from the previous section, in which we wanted vodka but neither cream nor vermouth: ```python for name, contents in drinks.items(): if 'vodka' in contents and not contents & {'vermouth', 'cream'}: print(name) ``` ] ] --- # Sets .panelset[ .panel[.panel-name[comprehensions] ### <p style="color:#00449E"> Set Comprehensions </p> - Sets also have comprehensions: <img src="../lec_figs/int-py-set-comprehension.png" width="66%" style="display: block; margin: auto;" /> <img src="../lec_figs/int-py-set-comprehension-if.png" width="83%" style="display: block; margin: auto;" /> ```python a_set = {number for number in range(1,6) if number % 3 == 1} ``` ] .panel[.panel-name[`frozenset()`] ### <p style="color:#00449E"> Create an Immutable Set with `frozenset()` </p> - If we want to create a set that can't be changed, call the `frozenset()` function with any iterable argument: ```python frozenset([3, 2, 1]) frozenset(set([2, 1, 3])) frozenset({3, 1, 2}) frozenset( (2, 3, 1) ) fs = frozenset([3, 2, 1]) fs fs.add(4) ``` ] ] --- # Data Structures So Far .panelset[ .panel[.panel-name[data structures] - To review, we make: - A list by using square brackets (`[]`) - A tuple by using commas and optional parentheses (`()`) - A dictionary or set by using curly brackets (`{}`) ```python marx_list = ['Groucho', 'Chico', 'Harpo'] marx_tuple = ('Groucho', 'Chico', 'Harpo') marx_dict = {'Groucho': 'banjo', 'Chico': 'piano', 'Harpo': 'harp'} marx_set = {'Groucho', 'Chico', 'Harpo'} ``` ] .panel[.panel-name[single item] - For all but sets, we access a single element with square brackets. - For the list and the tuple, the value between the square brackets is an integer offset. - For the dictionary, we access a single element with a key. ```python marx_list[2] marx_tuple[2] marx_dict['Harpo'] 'Harpo' in marx_set ``` - For the list, the tuple, and the dictionary, the result is a value. - For the set, the result is either there or it's not; there's no index or key. ] ] --- # Make Bigger Data Structures .panelset[ .panel[.panel-name[bigger data structures] - We worked up from simple booleans, numbers, and strings to lists, tuples, sets, and dictionaries. - We can combine these built-in data structures into bigger, more complex structures of our own. - Let's start with three different lists: ```python newyork = ['Geneseo', 'Rochester', 'Buffalo'] california = ['San Francisco', 'Los Angeles', 'San Diego'] texas = ['Dallas', 'Houston', 'Austin'] ``` ] .panel[.panel-name[tuple_of_lists] - We can make a tuple that contains each list as an element: ```python tuple_of_lists = newyork, california, texas tuple_of_lists ``` ] .panel[.panel-name[list_of_lists] - We can make a list that contains the three lists: ```python list_of_lists = [newyork, california, texas] list_of_lists ``` ] .panel[.panel-name[dict_of_lists] - Finally, let's create a dictionary of lists. - In this example, let's use the abbreviated name of the state as the key and the list of cities as the value: ```python dict_of_lists = {'NY': newyork, 'CA': california, 'TX': texas} dict_of_lists ``` ] ] --- # Dictionaries and Sets ### <p style="color:#00449E"> Class Exercises </p> *1*. Make an English-to-French dictionary called `e2f` and print it. Here are your starter words: `dog` is `chien`, `cat` is `chat`, and `walrus` is `morse`. *2*. Using your three-word dictionary `e2f`, print the French word for `walrus`. *3*. Make a French-to-English dictionary called `f2e` from `e2f` Use the `items` method. *4*. Print the English equivalent of the French word `chien`. *5*. Print the set of English words from `e2f`. --- # Dictionaries and Sets ### <p style="color:#00449E"> Class Exercises </p> *6*. Make a multilevel dictionary called `life`. Use these strings for the topmost keys: '`animals`', '`plants`', and '`other`'. Make the 'animals' key refer to another dictionary with the keys '`cats`', '`octopi`', and '`emus`'. Make the '`cats`' key refer to a list of strings with the values '`Henri`', '`Grumpy`', and '`Lucy`'. Make all the other keys refer to empty dictionaries. *7*. Print the top-level keys of `life`. *8*. Print the keys for `life['animals']`. *9*. Print the values for `life['animals']['cats']`. --- # Dictionaries and Sets ### <p style="color:#00449E"> Class Exercises </p> *10*. Use a dictionary comprehension to create the dictionary `squares`. Use `range(10)` to return the keys, and use the square of each key as its value. *11*. Use a set comprehension to create the set `odd` from the odd numbers in `range(10)`. --- class: inverse, center, middle # Functions <html><div style='float:left'></div><hr color='#EB811B' size=1px width=796px></html> --- # Functions - So far, all of our Python code examples have been little fragments. - These are good for small tasks, but no one wants to retype fragments all the time. - We need some way of organizing larger code into manageable pieces. - The first step to code reuse is the *function*: a named piece of code, separate from all others. - A function can take any number and type of input *parameters* and return any number and type of output *results*. - We can do two things with a function: - *Define* it, with zero or more parameters - *Call* it, and get zero or more results --- # Functions ### <p style="color:#00449E"> Define a Function with `def` </p> .panelset[ .panel[.panel-name[`def`] - To define a Python function, we type `def`, the function name, parentheses enclosing any input *parameters* to the function, and then finally, a colon (:). - Function names have the same rules as variable names (they must start with a letter or _ and contain only letters, numbers, or _). ] .panel[.panel-name[(1) def] - Let's define a very simple function that has no parameters. ```python def do_nothing(): pass # indention is needed here ``` - We use the `pass` statement when we want Python to do nothing. - It's the equivalent of *This page intentionally left blank.* ] .panel[.panel-name[(1) call] - We call the `do_nothing()` function just by typing its name and parentheses. ```python do_nothing() ``` ] ] --- # Functions ### <p style="color:#00449E"> Call a Function with Parentheses </p> .panelset[ .panel[.panel-name[(2)] - Let's define and call another function that has no parameters but prints a single word: ```python def make_a_sound(): print('quack') make_a_sound() ``` - When we called this function, Python ran the code inside its definition. - In this case, it printed a single word and returned to the main program. ] .panel[.panel-name[(3) `def` with `return`] - Let's try a function that has no parameters but *returns* a value: ```python def agree(): return True ``` ] .panel[.panel-name[(3) call] - We can call the `agree()` function and test its returned value by using `if`: ```python if agree(): print('Splendid!') else: print('That was unexpected.') ``` ] ] --- # Functions ### <p style="color:#00449E"> Arguments and Parameters </p> - Let's define the function `echo()` with one parameter called `anything`. - It uses the `return` statement to send the value of anything back to its caller twice, with a space between: ```python def echo(anything): return anything + ' ' + anything echo('Geneseo') ``` - The values we pass into the function when we call it are known as *arguments*. - When we call a function with arguments, the values of those arguments are copied to their corresponding *parameters* inside the function. --- # Functions ### <p style="color:#00449E"> Arguments and Parameters </p> ```python def echo(anything): return anything + ' ' + anything echo('Geneseo') ``` - The function `echo()` was called with the argument string `'Geneseo'`. - The value `'Geneseo'` was copied within `echo()` to the parameter `anything`, and then returned (in this case doubled, with a space) to the caller. --- # Functions ### <p style="color:#00449E"> Arguments and Parameters </p> - Let's write a function that takes an input argument and actually does something with it. - Call it `commentary`, have it take an input string parameter called `color`, and make it *return* the string description to its caller: ```python def commentary(color): if color == 'red': return "It's a tomato." elif color == "green": return "It's a green pepper." elif color == 'bee purple': return "I don't know what it is, but only bees can see it." else: return "I've never heard of the color " + color + "." ``` --- # Functions ### <p style="color:#00449E"> Arguments and Parameters </p> .panelset[ .panel[.panel-name[call commentary('blue')] - Call the function `commentary()` with the string argument `'blue'`. ```python comment = commentary('blue') ``` - The function does the following: - Assigns the value `'blue'` to the function's internal color parameter - Runs through the `if-elif-else` logic chain - Returns a string - The caller then assigns the string to the variable `comment`. ] .panel[.panel-name[print(comment)] - What did we get back? ```python print(comment) ``` - A function can take any number of input arguments (including zero) of any type. - It can return any number of output results (also including zero) of any type. ] .panel[.panel-name[print(do_nothing())] - If a function doesn't call `return` explicitly, the caller gets the result `None`. ```python def do_nothing(): pass print(do_nothing()) ``` ] ] --- # Functions ### <p style="color:#00449E"> `None` Is Useful </p> .panelset[ .panel[.panel-name[`None`] - `None` is a special Python value that holds a place when there is nothing to say. - It is not the same as the boolean value `False`, although it looks false when evaluated as a boolean. ```python thing = None if thing: print("It's some thing") else: print("It's no thing") ``` ] .panel[.panel-name[`is` `None`] - To distinguish `None` from a boolean `False` value, use Python's `is` operator: ```python thing = None if thing is None: print("It's some thing") else: print("It's no thing") ``` - We'll need `None` to distinguish a *missing* value from an *empty* value. - Zero-valued numbers, empty strings (`''`), lists (`[]`), tuples (`(,)`), dictionaries (`{}`), and sets (`set()`) are all `False`, but are not the same as `None`. ] .panel[.panel-name[`whatis(thing)`] - Let's write a quick function that prints whether its argument is `None`, `True`, or `False`: ```python def whatis(thing): if thing is None: print(thing, "is None") elif thing: print(thing, "is True") else: print(thing, "is False") ``` ] .panel[.panel-name[`whatis()`] - Let's run some sanity tests: ```python whatis(None) whatis(True) whatis(False) ``` ] .panel[.panel-name[`whatis()`] - How about some real values? ```python whatis(0) whatis(0.0) whatis('') whatis("") whatis('''''') whatis(()) whatis([]) whatis({}) whatis(set()) ``` ] .panel[.panel-name[`whatis()`] - How about some real values? ```python whatis(0.00001) whatis([0]) whatis(['']) whatis(' ') ``` ] ] --- # Functions .panelset[ .panel[.panel-name[Positional Arguments] ### <p style="color:#00449E"> Positional Arguments </p> - The most familiar types of arguments are positional arguments, whose values are copied to their corresponding parameters in order. - This function builds a dictionary from its positional input arguments and returns it: ```python def menu(wine, entree, dessert): return {'wine': wine, 'entree': entree, 'dessert': dessert} menu('chardonnay', 'chicken', 'cake') ``` ] .panel[.panel-name[Positional Arguments] ### <p style="color:#00449E"> Positional Arguments </p> - A downside of positional arguments is that we need to remember the meaning of each position. ```python menu('beef', 'bagel', 'bordeaux') ``` ] .panel[.panel-name[Keyword Arguments] ### <p style="color:#00449E"> Keyword Arguments </p> - To avoid positional argument confusion, we can specify arguments by the names of their corresponding parameters, even in a different order from their definition in the function: ```python menu(entree='beef', dessert='bagel', wine='bordeaux') # Specify the wine first, but use keyword arguments for the entree and dessert: menu('frontenac', dessert='flan', entree='fish') ``` - If we call a function with both positional and keyword arguments, the positional arguments need to come first. ] ] --- # Functions ### <p style="color:#00449E"> Specify Default Parameter Values </p> .panelset[ .panel[.panel-name[default value for parameter] - We can specify default values for parameters. - The default is used if the caller does not provide a corresponding argument. - Try calling `menu()` without the `dessert` argument: ```python def menu(wine, entree, dessert='pudding'): return {'wine': wine, 'entree': entree, 'dessert': dessert} menu('chardonnay', 'chicken') ``` ] .panel[.panel-name[argument] - We can specify default values for parameters. - If we provide an argument, it's used instead of the default: ```python def menu(wine, entree, dessert='pudding'): return {'wine': wine, 'entree': entree, 'dessert': dessert} menu('dunkelfelder', 'duck', 'doughnut') ``` ] ] --- # Functions ### <p style="color:#00449E"> Specify Default Parameter Values </p> .panelset[ .panel[.panel-name[buggy(arg, result=[])] - The `buggy()` function is expected to run each time with a fresh empty result list, add the arg argument to it, and then print a single-item list. ```python def buggy(arg, result=[]): result.append(arg) print(result) buggy('a') buggy('b') # expect ['b'] ``` - There's a bug: it's empty only the first time it's called. - The second time, result still has one item from the previous call. ] .panel[.panel-name[works(arg)] - It would have worked if it had been written like this: ```python def works(arg): result = [] result.append(arg) return result works('a') works('b') ``` ] .panel[.panel-name[nonbuggy(arg, result=None)] - The fix is to pass in something else to indicate the first call: ```python def nonbuggy(arg, result=None): if result is None: result = [] result.append(arg) print(result) nonbuggy('a') nonbuggy('b') ``` ] ] --- # Functions ### <p style="color:#00449E"> Explode/Gather Positional Arguments with `*` </p> .panelset[ .panel[.panel-name[(1)] - When an asterisk (`*`) is used inside the function with a parameter, it groups a variable number of positional arguments into a single tuple of parameter values. ```python def print_args(*args): print('Positional tuple:', args) print_args() print_args(3, 2, 1, 'wait!', 'uh...') ``` <!-- - In the following example, `args` is the parameter tuple that resulted from zero or more arguments that were passed to the function `print_args()`. --> <!-- - Whatever we give, it will be printed as the `args` tuple: --> ] .panel[.panel-name[(2)] - If our function has *required* positional arguments, as well, put them first; `*args` goes at the end and grabs all the rest: ```python def print_more(required1, required2, *args): print('Need this one:', required1) print('Need this one too:', required2) print('All the rest:', args) print_more('cap', 'gloves', 'scarf', 'monocle', 'mustache wax') ``` ] .panel[.panel-name[summary] - We can pass positional argument to a function, which will match them inside to positional parameters. - We can pass a tuple argument to a function, and inside it will be a tuple parameter. - We can pass positional arguments to a function, and gather them inside as the parameter `*args`, which resolves to the tuple args. ] .panel[.panel-name[summary] - We can also "explode" a tuple argument called `args` to positional parameters `*args` inside the function, which will be regathered inside into the tuple parameter `args`: ```python def print_args(*args): print('Positional tuple:', args) print_args(2, 5, 7, 'x') args = (2,5,7,'x') print_args(args) print_args(*args) ``` ] .panel[.panel-name[summary] - We can only use the `*` syntax in a function call or definition: ```python *args ``` - Outside the function, `*args` explodes the tuple `args` into comma-separated positional parameters. - Inside the function, `*args` gathers all of the positional arguments into a single args tuple. ] ] --- # Functions ### <p style="color:#00449E"> Explode/Gather Positional Arguments with `**` </p> .panelset[ .panel[.panel-name[(1)] - We can use two asterisks (`**`) to group keyword arguments into a dictionary, where the argument names are the keys, and their values are the corresponding dictionary values. ```python def print_kwargs(**kwargs): print('Keyword arguments:', kwargs) print_kwargs() print_kwargs(wine='merlot', entree='mutton', dessert='macaroon') ``` - Inside the function, `kwargs` is a dictionary parameter. ] .panel[.panel-name[summary] - We can pass keyword arguments to a function, which will match them inside to keyword parameters. - We can pass a dictionary argument to a function, and inside it will be dictionary parameters. ] .panel[.panel-name[summary] - We can pass one or more keyword arguments (*name=value*) to a function, and gather them inside as `**kwargs`, which resolves to the dictionary parameter called `kwargs`. - Outside a function, `**kwargs` explodes a dictionary `kwargs` into *name=value* arguments. - Inside a function, `**kwargs` gathers *name=value* arguments into the single dictionary parameter `kwargs`. ] ] --- # Functions ### <p style="color:#00449E"> Keyword-Only Arguments </p> .panelset[ .panel[.panel-name[keyword-only argument] - *a keyword-only argument* is an argument that can only be provided as a keyword argument when a function is called. - It is recommended to be provided as *name=value*. ```python def print_data(data, *, start=0, end=100): for value in (data[start:end]): print(value) ``` - The single `*` in the definition above means that the parameters `start` and `end` must be provided as named arguments if we don’t want their default values. ] .panel[.panel-name[call with keyword-only argument] ```python data = ['a', 'b', 'c', 'd', 'e', 'f'] print_data(data) print_data(data, start=4) print_data(data, end=2) ``` ] .panel[.panel-name[name only] ```python def print_data2(data, *, start, end): for value in (data[start:end]): print(value) print_data2(data) print_data2(data, start=4) print_data2(data, end=2) print_data2(data, start = 2, end = 4) ``` - `start` and `end` are required arguments, because they doesn't have a default value and they must be specified as a keyword argument when we call the `print_data2()` function. ] ] --- # Functions ### <p style="color:#00449E"> Mutable and Immutable Arguments </p> - Remember that if you assigned the same list to two variables, you could change it by using either one? And that you could not if the variables both referred to something like an integer or a string? <!-- - We need to watch for the same behavior when passing arguments to functions. --> - If an argument is mutable, its value can be changed *from inside the function* via its corresponding parameter. ```python outside = ['one', 'fine', 'day'] def mangle(arg): arg[1] = 'terrible!' outside mangle(outside) outside ```