def echo(message):
print(message, message)
print(f"echo = {echo}")
print(f"type(echo) = {type(echo)}")
echo = <function echo at 0x104281940>
type(echo) = <class 'function'>
The style of programming we’ve been doing is called imperative or procedural. Statements run in sequence and change a program’s state.
From the “procedural” point of view, a function (procedure) is a set of statements that can be called more than once, we use parameters to make our procedures more reusable.
This is the “recipe” model of programming, a procedure is a recipe: a series of steps to follow to achieve a result.
Our first paradigm, procedural programming leans heavily on the constructs we’ve seen: loops, conditionals, and the use of functions to break large procedures into smaller ones.
Some languages make a distinction between procedures and functions. In Python we don’t make this distinction, but we will soon see another style of programming where we’ll think differently about how we use functions.
Benefits of procedures (functions):
Languages like LISP, Haskell, and Racket are purely functional & differ significantly from procedural & object-oriented languages.
Functional programming uses a definition of functions more compatible with the mathematical definition. Instead of the recipe model of procedural programming, mathematical functions take input(s) and return an output.
These functions do not have the concept of “state”: the same call with the same parameters always results in the same result. that is, calling a function in math creates a mapping from inputs to outputs.
When we call sin(x)
we do not speak of it modifying its inputs, just returning a value.
Similarly, when we workin a functional style we’ll often write smaller functions that we chain together, instead of long procedures that rely on internal state.
Python has many features that stem from pure functional languages & support functional programming:
functools
A key feature of Python that makes it possible to write code in the functional style is the fact that functions are objects. (Everything in Python is an object.)
This means functions don’t have special rules about how they can be used compared to other types, any variable can reference a function. (Remember, a variable is an association between a name & object.)
def echo(message):
print(message, message)
print(f"echo = {echo}")
print(f"type(echo) = {type(echo)}")
echo = <function echo at 0x104281940>
type(echo) = <class 'function'>
# we can assign names to objects, including functions
= echo
x "hello") x(
hello hello
id(x), id(echo)
(4364704064, 4364704064)
# we can also store functions in other types, like list
= [print, echo, print, echo]
func_list for i, func in enumerate(func_list):
func(i)
0
1 1
2
3 3
# dictionaries too
= {False: print, True: echo}
func_mapping
= True
print_twice True]("twice")
func_mapping[
= False
print_twice "once") func_mapping[print_twice](
twice twice
once
# we can pass functions into other functions
def add(a, b):
return a + b
def sub(a, b):
return a - b
def perform_op(op_func, a, b):
return op_func(a, b)
print("add, 3, 4 = ", perform_op(add, 3, 4))
print("sub, 3, 4 = ", perform_op(sub, 3, 4))
add, 3, 4 = 7
sub, 3, 4 = -1
# and we can return functions from other functions
def get_op(name):
if name == "div":
def f(a, b):
return a / b
elif name == "mod":
def f(a, b):
return a % b
return f
= get_op("mod")
fn 100, 5)
fn(#perform_op(fn, 10, 3)
0
sorted
exampleIt isn’t uncommon in Python for functions to take other functions, let’s look at sorted
help(sorted)
Help on built-in function sorted in module builtins:
sorted(iterable, /, *, key=None, reverse=False)
Return a new list containing all items from the iterable in ascending order.
A custom key function can be supplied to customize the sort order, and the
reverse flag can be set to request the result in descending order.
= [("Nick", 1), ("Nick", -100), ("Yusong", 9000), ("Emma", 100)]
d
def second_key(item):
return item[1]
def negate(item):
return -item[1]
# default sort
sorted(d)
[('Emma', 100), ('Nick', -100), ('Nick', 1), ('Yusong', 9000)]
sorted(d, key=negate)
[('Yusong', 9000), ('Emma', 100), ('Nick', 1), ('Nick', -100)]
sorted(d, key=second_key)
[('Nick', -100), ('Nick', 1), ('Emma', 100), ('Yusong', 9000)]
Python also provides another way to generate function objects.
These are called lambda functions (aka anonymous functions), which:
The body of an lambda function is a single expression, not a block of statements. The body is similar to a return statement in a def statement.
lambda arg1, arg2: expression
# essentially the same as
def __(arg1, arg2):
return expression
(0 or more arguments, but must have an expression)
Remember that expressions evaluate to a value, and can be assigned to a variable.
Expresssions are valid in assignment, function calls, sequence values, etc. (Anywhere a value is needed.)
When it comes to lambda
: * a lambda
defines a function that maps input to a single expression, def
can be used if statements are needed * a lambda
is itself an expression, it can be used anywhere other expresssions are needed
As an expression, lambda
can be used as a parameter:
lambda a, b: a * b, 5, 6) perform_op(
30
= ["abc", "Abb", "aaa", "ABC", "AAB"]
words sorted(words)
['AAB', 'ABC', 'Abb', 'aaa', 'abc']
sorted(words, key=lambda s: s.upper())
['aaa', 'AAB', 'Abb', 'abc', 'ABC']
# can be assigned to a variable
= lambda a, b: a * b
mul 5, 6)
mul(
# same as
def mul2(a, b):
return a * b
type(mul), type(mul2)
(function, function)
General rule: If you’re giving a lambda a name, use a function.
Python has several built in methods that are useful when writing programs with a functional mindset.
map
, filter
, functools
map(function, iterable1, [...iterableN])
Returns a new iterable that calls function
with parameters from iterable1 ... iterableN
.
def add_two(x):
print("called add_two", x)
return x + 2
for x in map(add_two, [1, 2, 3]):
print(x)
called add_two 1
3
called add_two 2
4
called add_two 3
5
help(map)
Help on class map in module builtins:
class map(object)
| map(function, iterable, /, *iterables)
|
| Make an iterator that computes the function using arguments from
| each of the iterables. Stops when the shortest iterable is exhausted.
|
| Methods defined here:
|
| __getattribute__(self, name, /)
| Return getattr(self, name).
|
| __iter__(self, /)
| Implement iter(self).
|
| __next__(self, /)
| Implement next(self).
|
| __reduce__(self, /)
| Return state information for pickling.
|
| ----------------------------------------------------------------------
| Static methods defined here:
|
| __new__(*args, **kwargs)
| Create and return a new object. See help(type) for accurate signature.
= list(map(add_two, [1, 2, 3]))
x print(x)
called add_two 1
called add_two 2
called add_two 3
[3, 4, 5]
# commonly used with lambdas
for x in map(lambda x, y: x+y, ("A", "B", "C"), ["!", "?", "."]):
print(x)
A!
B?
C.
# number of parameters must match number of iterables
for x in map(lambda x, y, z: x+(y*z), ("A", "B", "C"), ["!", "?", "."], [2, 3, 4]):
print(x)
A!!
B???
C....
# operator module contains all of the common operators in function form
import operator
20, 5) operator.sub(
15
map
returns a special kind of iterable, can be wrapped in things other than list.
set(map(operator.sub, [20, 19], [10, 9]))
{10}
# can use anywhere you can use an iterable
tuple(map(lambda x: x * 3, ("A", "B", "C")))
('AAA', 'BBB', 'CCC')
filter(function, iterable)
returns an iterable that contains all items from iterable for which function(item)
returns True
We call this kind of function a predicate.
list(filter(lambda s: s.isupper(), ["a", "ABC", "AbCdeF", "XYZ", ""]))
['ABC', 'XYZ']
list(map(lambda s: s*2, filter(str.isupper, ["a", "ABC", "AbCdeF", "XYZ"])))
['ABCABC', 'XYZXYZ']
list(filter(str.isupper, map(lambda s: s.title(), ["a", "ABC", "AbCdeF", "XYZ"])))
['A']
list(map(lambda s: s.lower(), filter(lambda s: s.isupper(), ["a", "ABC", "AbCdeF", "XYZ"])))
['abc', 'xyz']
https://docs.python.org/3/library/functools.html
import functools
for name in dir(functools) if name[0].islower()] [name
['cache',
'cached_property',
'cmp_to_key',
'get_cache_token',
'lru_cache',
'namedtuple',
'partial',
'partialmethod',
'recursive_repr',
'reduce',
'singledispatch',
'singledispatchmethod',
'total_ordering',
'update_wrapper',
'wraps']
functools.reduce(function, iterable[, initializer])
Apply function
to pairs of items successively and return a single value as the result. You can optionally specify the initial value.
import functools
import operator
# accumulator = 0
# for item in my_list:
# accumulator += item
# 1st iteration: Call operator.add(1,2) -> 3
# 2nd iteration: Call operator.add(3,3) -> 6
# 3rd iteration: Call operator.add(6,4) -> 10
# final result = 10
reduce(operator.add, [1,2,3,4]) functools.
10
= ["Ben", "Martha", "Susan"]
names # 1st iteration: call f(0, "Ben") -> 0 + len("Ben") -> 3
# 2nd iteration: call f(3, "Martha") -> 3 + len("Martha") -> 9
# 3rd iteration: call f(9, "Susan") -> 9 + len("Susan") -> 14
reduce(lambda accumulator, new_val: accumulator + len(new_val),
functools.
names, 0)
14
# What happens if you pass in an initial value
# 1st iteration: Call operator.mul(2,1) -> 2
# 2nd iteration: Call operator.mul(2,2) -> 4
# 3rd iteration: Call operator.mul(4,3) -> 12
# 4th iteration: Call operator.mul(12,4) -> 48
# Final result = 48
reduce(operator.mul, [1,2,3,4], 2) functools.
48
reduce(lambda a,b: a+b, [1, 2, 3]) functools.
6
functools.partial(func, *args, **kwargs)
functools.partial
returns a new function that “binds” any passed args & kwargs, and leaves other parameters unbound.
import operator
2, 10) operator.mul(
20
import functools
= functools.partial(operator.mul, -1)
negate 5) negate(
-5
list(map(negate, [1, 2, 3, 4]))
[-1, -2, -3, -4]
def calls_twice(f):
print(f())
print(f())
= functools.partial(operator.mul, 4, 4)
g #print(g())
calls_twice(g)
16
16
= functools.partial(print, sep="!")
print_ex "a", "b", "c") print_ex(
a!b!c
# ERROR: parameters must be valid
= functools.partial(print, foo="x") print_foo
# another way to deal with functions we're calling with the same args repeatedly
def request_page(url, verify, cache=True, send_cookies=False, https_only=True):
pass
= functools.partial(request_page, verify=True, https_only=True) secure_request
"", verify=False) secure_request(