< Data Structures in Python | Contents | Classes in Python >
Functions in Python¶
Purpose: To help you get comfortable with the topics outlined below.
Recomended Usage
- Run each of the cells (Shift+Enter) and edit them as necessary to solidify your understanding
- Do any of the exercises that are relevant to helping you understand the material
Topics Covered
- Basic Functions
- Generators
- Lambda Expressions
- Partial Functions
- Nested Functions
- Positional and Keyword Arguments
Workbook Setup¶
The following magics will reload all modules before executing a new line and help make sure you follow PEP8 code style.
%load_ext autoreload
%autoreload 2
%load_ext pycodestyle_magic
%pycodestyle_on
For this lesson we will also need functools
import functools
Basic Functions¶
def function_name(arg_1, arg_2): pass
def my_function(arg_1):
print("arg_1 is: {}".format(arg_1))
my_function("test")
def my_function(arg_1):
return arg_1 + 1
result = my_function(3)
print(result)
Generators¶
Generators produce values one-at-a-time as opposed to functions which give them all at once. Its particularly useful when dealing with big data because we just access values one at a time.
def my_generator():
yield "a"
Generator functions¶
# Regular function
def function_a():
return "a"
# Generator function
def generator_a():
yield "a"
function_a()
generator_a()
next(generator_a())
Note: You can only use a generator ONCE (once you get to the end, you're done).
Generator Expressions¶
# Traditional list comprehension (uses brackets)
lc_example = [n**2 for n in [1, 2, 3, 4, 5]]
lc_example
# Generator expression (uses parentheses)
gen_exp1 = (n**2 for n in [1, 2, 3, 4, 5])
gen_exp1
There are two ways to get the next item from a generator; next() and a for loop
print(next(gen_exp1))
print(next(gen_exp1))
print(next(gen_exp1))
for item in gen_exp1:
print(item)
gen_exp2 = (n**2 for n in [1, 2, 3, 4, 5] if n >= 3)
print(next(gen_exp2))
print(next(gen_exp2))
One practical example could be to read in a very large file row by row.
def rowByRowGenerator():
file = "veryBIGFile.csv"
for row in open(file):
yield row
Lambda Expressions¶
Lambda expressions (sometimes called lambda functions) are small (single expression) anonymous functions created using the lambda keyword. They are especially useful in situations when you don't want/need to use a whole function definition but rather just have a small expression.
lambda arguments: expression
For example, the following are functionally equivalent:
def f(x): return 2*x
# ACTS THE SAME AS
f = lambda x: 2*x
Best Practice Note: Though the def and lambda expression above are equivalent, you generally want to assign a lambda expression to a name. It defeats the purpose of it, see best use cases below.
Lambda expressions as function arguments¶
Lambda expressions are really useful when a function takes another function as a parameter like in the map() function
map(func, *iterables) --> map object
original_list = [1, 2, 3, 4]
doubled_list = map(lambda x: x*2, original_list)
print('Original List: {}'.format(original_list))
print('Doubled List: {}'.format(list(doubled_list)))
You can also use it in the case of in the sort() function.
sort(*, key=None, reverse=False)
pairs = [(4, 'c'), (2, 'b'), (3, 'a'), (1, 'd')]
# Sort the pairs by the 0th element in each pair
pairs.sort(key=lambda pair: pair[0])
print(pairs)
# Sort the pairs by the 1th element in each pair
pairs.sort(key=lambda pair: pair[1])
print(pairs)
Partial Functions¶
After importing partial from functools you can use partial functions.
A partial function is really useful for being able to add some default arguments to a function so you don't need to repeat all the arguments all of the time.
The general syntax for a partial function is this:
variable = partial(function_name, function_params)
from functools import partial
# Say we have a regular function def that multiplies two numbers
def multiply(x, y):
print(x, y)
return x * y
# We can create a new function that multiplies by 2
dbl = partial(multiply, 2)
# Then dbl(4) is called with the 2 already there
print(dbl(4))
In this case we start with a normal function definition called multiply() that multiplies to inputs. We can then create a partial function definition that uses a spin off of that function called dble by always setting one param as 2.
Nested Functions¶
In Python, you can define nested or inner functions like in the examples below. They are most widely used in the case of decorators (see the "Decorators Workbook" for more).
def parent():
print("parent() funct")
def first_child():
print("first_child() funct")
def second_child():
print("second_child() funct")
first_child()
second_child()
parent()
We can call parent() and return the functions that handle each case
def parent(num):
def first_child():
return "Hi, I am the 1st child"
def second_child():
return "Hi, I am the 2nd child"
if num == 1:
return first_child
else:
return second_child
first = parent(1)
first
first()
second = parent(2)
second
second()
Positional and Keyword Arguments¶
Functions in Python can have positional and/or keyword arguments. They can be a fixed number of arguments or variable.
In Python3.8 there is a new function parameter syntax / to indicate that some function parameters must be specified positionally and cannot be used as keyword arguments. Check out the examples from the docs below
# Make sure you are using Python3 or you may get an error
import sys
print(sys.version)
def f(a, b, /, c, d, *, e, f):
print(a, b, c, d, e, f)
f(10, 20, 30, d=40, e=50, f=60)
f(10, b=20, c=30, d=40, e=50, f=60) # b cannot be a keyword argument
f(10, 20, 30, 40, 50, f=60) # e must be a keyword argument
Since the parameters to the left of / are not exposed as possible keywords, the parameters names remain available for use in **kwargs:
def f(a, b, /, **kwargs):
print(a, b, kwargs)
f(10, 20, a=1, b=2, c=3) # a and b are used in two ways
Packing and unpacking using *¶
When you see * or ** in front of a variable in Python they are used to unpack iterables and dictionaries respectively.
They are frequently used to unpack a variable number of function arguments using *args (arguments) and **kwargs (keyword arguments) in function definitions.
# *args is used to pass in a variable number of arguments
def sayThis1(*argv):
for arg in argv:
print(arg)
sayThis1('Hi', 'whats', 'going', 'on') # Pass in any number of args
# **kwargs is used to pass a variable number of keyword arguments
def sayThis2(**kwargs):
for key, value in kwargs.items():
print ("%s == %s" %(key, value))
sayThis2(first ='Love', mid ='is', last='good')
my_list = [1, 2, 3]
print(my_list) # Packed
print(*my_list) # Unpacked
Extract/unpack the body of a list¶
The asterisk can also be used to unpack the body of something.
my_list = [1, 2, 3, 4, 5, 6]
my_list
a, *b, c = my_list
print(a)
print(b)
print(c)
Merging lists and dicts by unpacking¶
list1 = [1, 2, 3]
list2 = [4, 5, 6]
merged_list = [*list1, *list2] # Unpack each list into a new list
print(merged_list)
dict1 = {"A": 1, "B": 2}
dict2 = {"C": 3, "D": 4}
merged_dict = {**dict1, **dict2}
print(merged_dict)
Unpack a string into chars¶
a = [*"SomeStringOfChars"]
print(a)
Troubleshooting Tips¶
If you run into issues running any of the code in this notebook, check your version of Python and Jupyter against mine below
import sys
print(sys.version)
3.7.6 (default, Dec 31 2019, 17:12:14)
[Clang 11.0.0 (clang-1100.0.33.16)]
!jupyter --version
jupyter core : 4.6.1
jupyter-notebook : 6.0.2
qtconsole : not installed
ipython : 7.9.0
ipykernel : 5.1.3
jupyter client : 5.3.4
jupyter lab : 1.2.3
nbconvert : 5.6.1
ipywidgets : not installed
nbformat : 4.4.0
traitlets : 4.3.3
# import sys
# print(sys.version)
# !jupyter --version
< Data Structures in Python | Contents | Classes in Python >