Introduction to functional programming in
Python
Francesco Bruni

@brunifrancesco 

Original notebook @ https://github.com/brunifrancesco/cds_talk
Who am I
• MSc Student in Telecommunication Engineering @ Poliba (Italy)

• Despite a Java background, I prefer Python whenever possible

• Data science addicted
What we'll talk about
	 •	 Why functional programming

	 •	 Why Python

	 •	 Functional features in Python

	 ▪	 First class functions

	 ▪	 Immutable vs mutable data

	 ▪	 Lazy evaluation

	 ▪	 Recursion vs loop

	 ▪	 (Manual) Currying

	 ▪	 Monads

	 •	 Useful functional programming resources

	 •	 Conclusions
Why functional programming
	 •	 Think in terms of functions

	 •	 State as function evaluation, not variables assignment

	 •	 Testing is (more) easy

	 •	 A new viewpoint is required
import random

def sum_imperative():

res = 0

for n in [random.random() for _ in range(100)]:

res += n

return res

def sum_functional():

return sum(random.random() for _ in range(100))
Why Python
There are a lot of languages out there..
Pros
	 •	 Mature enough

	 •	 Multiparadigm and multipurpose

	 •	 Easy to learn

	 •	 Interactive shell

	 •	 Multiple implementantions (C, Java, Python, .NET, Ruby, Js)

	 •	 Extending requires few lines of code
Cons
	 •	 Python 3 vs Python 2

	 •	 No typed variables

	 •	 CPython is the widest used implementation

	 •	 Changing implementation/version is not (always) a free iussues transition
Functional features in Python
	 •	 Not all functional patterns apply to Python

	 •	 Hybrid approach is often requested

	 •	 Other libraries needs to be (eventually) integrated
First class functions
	 •	 Functions are objects too

	 •	 They have fields too and some basic methods
def func_1(x="32"):

assert(x)

def func_2():

return "wow!"

func_3 = lambda some_param: str(some_param)
High order functions
	 •	 Functions which accept functions as params;

	 •	 Functions which return other functions;

	 •	 Python std lib examples for mapping: filter, map, sorted,

	 •	 Python std lib examples for reducing: max, min, sum

Task: given the previous generated list, filter some number out and sort the result by the
second digit.
def filter_out():



result = python_better_approach()



exclude = map(lambda item: item ** 2, range(30))

result = filter(lambda item: item not in exclude, result)



return sorted(result, key=lambda item: str(item)[1])



filter(lambda item: item not in map(lambda item: item ** 2, range(30)),
python_better_approach(size=100))
Class for function composition
Handle class creation via a new class syntax, allowing a "static" configuration
from collections.abc import Callable

class ExcludeFunction(Callable):

def __init__(self, exclude_function):

self.exclude_function= exclude_function



def __call__(self, value):

return None if not value else self.exclude_function(value)

result = python_better_approach(size=100)

exclude_function_1 = lambda item: item ** 2

ex_func = ExcludeFunction(exclude_function_1)

result = filter(lambda item: not item == ex_func(item), result)

assert(sorted(result, key=lambda item: str(item)[1]) == filter_out())
Immutable vs mutable data structure
	 •	 Since state is not given by variable assignement, there's no need of mutables data

	 •	 (Almost) no classes are required

	 •	 Instead of lists, tuples are reccomanded

	 •	 Instead of classes, namedtuple can be used
from collections.abc import Callable

class ExcludeFunction(Callable):

def __init__(self, exclude_function):

self.exclude_function= exclude_function



def __call__(self, value):

return None if not value else self.exclude_function(value)

result = python_better_approach(size=100)

exclude_function_1 = lambda item: item ** 2

ex_func = ExcludeFunction(exclude_function_1)

result = filter(lambda item: not item == ex_func(item), result)

assert(sorted(result, key=lambda item: str(item)[1]) == filter_out())
Strict vs not strict evaluation
	 •	 Strict evaluation requires that all operators needs to be evaluated

	 •	 Non strict (or lazy) evaluation, evaluates expression if and when requested
Use case: Python iterables structures
• Iterable: objects than can be iterated;

• Iterator: objects than can be iterated and consumed only once, with two main problems:

	 ▪	 They track their state in a stateful object

	 ▪	 Their values are generated a priori

	 ▪	 They aren't lazy

• Generators: lazy evaluated data structures:

	 ▪	 They track state in function, instead of object

	 ▪	 They don't act as normal functions

	 ▪	 They generate next member when invoked
def _iter(size=100):

return iter(python_better_approach(size=size))

def lazy_python_approach(size=100):

for item in range(10, size):

if not item % 2 and not str(item).endswith("4"):

yield item

def lazy_new_python_approach(size=100):

yield from (r for r in range(10, size) if not r % 2 and not str(r).endswith("4"))
Recursion vs loop
	 •	 Functional programming requires a recursion approach vs loop iteration

	 •	 Python suffers by recursion limit

	 •	 Python does not offer any tail call optimization
def fact_loop(n):

if n == 0: return 1

f= 1

for i in range(2,n):

f *= i

return f

def fact(n):

if n == 0: return 1

else: return n*fact(n-1)
Currying
Multiple arguments functions mapped to single arguments functions
from random import randrange

def mult(a, b, c ):

def wrapper_1(b):

def wrapper_2(c):

return a*b*c

return wrapper_2(c)

return wrapper_1(b)

def mult_2(a):

def wrapper_1(b):

def wrapper_2(c):

return a*b*c

return wrapper_2

return wrapper_1
Partials
Python provides "partial" functions for manual currying
from functools import reduce

import operator

import random

def two_random_sum():

return reduce(operator.sum, [random.random()]*2)

def three_random_product():

return reduce(operator.mul, [random.random()]*3)

def four_random_sub():

return reduce(operator.sub, [random.random()]*4)

def five_random_pow ():

return reduce(operator.pow, [random.random()]*5)
def handle_random_numbers(size, function):

return reduce(function, [random.random()]*size)

two_random_sum = partial(handle_random_numbers, size=2)

three_random_sum = partial(handle_random_numbers, size=3)

two_random_pow = partial(handle_random_numbers, size=2, function=operator.pow)

five_random_product = partial(handle_random_numbers, size=5, function=operator.mul)
Decorator Pattern
Return a modified version of a decorated function
from functools import wraps

from functools import partial

def get_ned_data(n):

def get_doubled_data(func, *args, **kwargs):

@wraps(func)

def _inner(*args, **kwargs):

kwargs["new_param"] = kwargs["some_param"]*n

return func(*args, **kwargs)

return _inner

return get_doubled_data
@get_ned_data(n=2)

def double_func(*args, **kwargs):

assert(kwargs["new_param"] == kwargs["some_param"]*2)

@get_ned_data(n=3)

def triple_func(*args, **kwargs):

assert(kwargs["new_param"] == kwargs["some_param"]*3)



double_func(some_param=3)

triple_func(some_param=5)
Monads
	 •	 How to handle errors in functional programming?

	 •	 Practical example: parsing and transforming data coming from HTTP request

	 •	 Python "std" libs have no support
from fn.monad import optionable

def get(request, *args, **kwargs):

@optionable

def _get_values(data):

return data.get("values", None)

_split = lambda item: item.split(",")

_strip = lambda item: item.replace(" ", "")

_filter = lambda item: list(filter(lambda i: i, item))

values = _get_values(request.GET).map(_strip).map(_split).map(_filter).get_or(["v1,v2"])

return values
from collections import namedtuple

def test():

_req_class = namedtuple("Request", ("GET"))



request_1 = _req_class(dict(values="v1, v2,v3"))

request_2 = _req_class(dict(values="v1,v2,v3 "))

request_3 = _req_class(dict(values="v1, v2,v3, "))



assert(get(request_1) == ['v1', 'v2', 'v3'])

assert(get(request_2) == ['v1', 'v2', 'v3'])

assert(get(request_3) == ['v1', 'v2', 'v3'])
@get_object(MeterInstance)

def post(self, request, *args, **kwargs):

@optionable

def get_data(data):

return data.get("data", None)

return get_data(request.DATA)

.map(

lambda item:

handle_error_configuration(item, kwargs["contatore"]))

.map(

lambda item:

Response(dict(detail=item), status=200))

.get_or(Response("BadRequest", status=400))
Useful libraries
• Fn.py (https://github.com/kachayev/fn.py) (A bit of Scala, ported to Python)

• Underscore.py (ported from underscore_js)
Summary
	 •	 Functional programming for writing more succint code

	 •	 Change viewpoint: think in terms of functions

	 •	 Python seems for dummies until you show some cool features

	 •	 Python can be slow but with a fine tuning can be more efficient
Questions?

Introduction to Functional Programming

  • 1.
    Introduction to functionalprogramming in Python Francesco Bruni @brunifrancesco Original notebook @ https://github.com/brunifrancesco/cds_talk
  • 2.
    Who am I •MSc Student in Telecommunication Engineering @ Poliba (Italy) • Despite a Java background, I prefer Python whenever possible • Data science addicted
  • 3.
    What we'll talkabout • Why functional programming • Why Python • Functional features in Python ▪ First class functions ▪ Immutable vs mutable data ▪ Lazy evaluation ▪ Recursion vs loop ▪ (Manual) Currying ▪ Monads • Useful functional programming resources • Conclusions
  • 4.
    Why functional programming • Think in terms of functions • State as function evaluation, not variables assignment • Testing is (more) easy • A new viewpoint is required
  • 5.
    import random def sum_imperative(): res= 0 for n in [random.random() for _ in range(100)]: res += n return res def sum_functional(): return sum(random.random() for _ in range(100))
  • 6.
    Why Python There area lot of languages out there..
  • 7.
    Pros • Matureenough • Multiparadigm and multipurpose • Easy to learn • Interactive shell • Multiple implementantions (C, Java, Python, .NET, Ruby, Js) • Extending requires few lines of code
  • 8.
    Cons • Python3 vs Python 2 • No typed variables • CPython is the widest used implementation • Changing implementation/version is not (always) a free iussues transition
  • 9.
    Functional features inPython • Not all functional patterns apply to Python • Hybrid approach is often requested • Other libraries needs to be (eventually) integrated
  • 10.
    First class functions • Functions are objects too • They have fields too and some basic methods
  • 11.
    def func_1(x="32"): assert(x) def func_2(): return"wow!" func_3 = lambda some_param: str(some_param)
  • 12.
    High order functions • Functions which accept functions as params; • Functions which return other functions; • Python std lib examples for mapping: filter, map, sorted, • Python std lib examples for reducing: max, min, sum Task: given the previous generated list, filter some number out and sort the result by the second digit.
  • 13.
    def filter_out(): result =python_better_approach() exclude = map(lambda item: item ** 2, range(30)) result = filter(lambda item: item not in exclude, result) return sorted(result, key=lambda item: str(item)[1]) filter(lambda item: item not in map(lambda item: item ** 2, range(30)), python_better_approach(size=100))
  • 14.
    Class for functioncomposition Handle class creation via a new class syntax, allowing a "static" configuration
  • 15.
    from collections.abc importCallable class ExcludeFunction(Callable): def __init__(self, exclude_function): self.exclude_function= exclude_function def __call__(self, value): return None if not value else self.exclude_function(value) result = python_better_approach(size=100) exclude_function_1 = lambda item: item ** 2 ex_func = ExcludeFunction(exclude_function_1) result = filter(lambda item: not item == ex_func(item), result) assert(sorted(result, key=lambda item: str(item)[1]) == filter_out())
  • 16.
    Immutable vs mutabledata structure • Since state is not given by variable assignement, there's no need of mutables data • (Almost) no classes are required • Instead of lists, tuples are reccomanded • Instead of classes, namedtuple can be used
  • 17.
    from collections.abc importCallable class ExcludeFunction(Callable): def __init__(self, exclude_function): self.exclude_function= exclude_function def __call__(self, value): return None if not value else self.exclude_function(value) result = python_better_approach(size=100) exclude_function_1 = lambda item: item ** 2 ex_func = ExcludeFunction(exclude_function_1) result = filter(lambda item: not item == ex_func(item), result) assert(sorted(result, key=lambda item: str(item)[1]) == filter_out())
  • 18.
    Strict vs notstrict evaluation • Strict evaluation requires that all operators needs to be evaluated • Non strict (or lazy) evaluation, evaluates expression if and when requested
  • 19.
    Use case: Pythoniterables structures • Iterable: objects than can be iterated; • Iterator: objects than can be iterated and consumed only once, with two main problems: ▪ They track their state in a stateful object ▪ Their values are generated a priori ▪ They aren't lazy • Generators: lazy evaluated data structures: ▪ They track state in function, instead of object ▪ They don't act as normal functions ▪ They generate next member when invoked
  • 20.
    def _iter(size=100): return iter(python_better_approach(size=size)) deflazy_python_approach(size=100): for item in range(10, size): if not item % 2 and not str(item).endswith("4"): yield item def lazy_new_python_approach(size=100): yield from (r for r in range(10, size) if not r % 2 and not str(r).endswith("4"))
  • 21.
    Recursion vs loop • Functional programming requires a recursion approach vs loop iteration • Python suffers by recursion limit • Python does not offer any tail call optimization
  • 22.
    def fact_loop(n): if n== 0: return 1 f= 1 for i in range(2,n): f *= i return f def fact(n): if n == 0: return 1 else: return n*fact(n-1)
  • 23.
    Currying Multiple arguments functionsmapped to single arguments functions
  • 24.
    from random importrandrange def mult(a, b, c ): def wrapper_1(b): def wrapper_2(c): return a*b*c return wrapper_2(c) return wrapper_1(b) def mult_2(a): def wrapper_1(b): def wrapper_2(c): return a*b*c return wrapper_2 return wrapper_1
  • 25.
    Partials Python provides "partial"functions for manual currying
  • 26.
    from functools importreduce import operator import random def two_random_sum(): return reduce(operator.sum, [random.random()]*2) def three_random_product(): return reduce(operator.mul, [random.random()]*3) def four_random_sub(): return reduce(operator.sub, [random.random()]*4) def five_random_pow (): return reduce(operator.pow, [random.random()]*5)
  • 27.
    def handle_random_numbers(size, function): returnreduce(function, [random.random()]*size) two_random_sum = partial(handle_random_numbers, size=2) three_random_sum = partial(handle_random_numbers, size=3) two_random_pow = partial(handle_random_numbers, size=2, function=operator.pow) five_random_product = partial(handle_random_numbers, size=5, function=operator.mul)
  • 28.
    Decorator Pattern Return amodified version of a decorated function
  • 29.
    from functools importwraps from functools import partial def get_ned_data(n): def get_doubled_data(func, *args, **kwargs): @wraps(func) def _inner(*args, **kwargs): kwargs["new_param"] = kwargs["some_param"]*n return func(*args, **kwargs) return _inner return get_doubled_data
  • 30.
    @get_ned_data(n=2) def double_func(*args, **kwargs): assert(kwargs["new_param"]== kwargs["some_param"]*2) @get_ned_data(n=3) def triple_func(*args, **kwargs): assert(kwargs["new_param"] == kwargs["some_param"]*3) double_func(some_param=3) triple_func(some_param=5)
  • 31.
    Monads • Howto handle errors in functional programming? • Practical example: parsing and transforming data coming from HTTP request • Python "std" libs have no support
  • 32.
    from fn.monad importoptionable def get(request, *args, **kwargs): @optionable def _get_values(data): return data.get("values", None) _split = lambda item: item.split(",") _strip = lambda item: item.replace(" ", "") _filter = lambda item: list(filter(lambda i: i, item)) values = _get_values(request.GET).map(_strip).map(_split).map(_filter).get_or(["v1,v2"]) return values
  • 33.
    from collections importnamedtuple def test(): _req_class = namedtuple("Request", ("GET")) request_1 = _req_class(dict(values="v1, v2,v3")) request_2 = _req_class(dict(values="v1,v2,v3 ")) request_3 = _req_class(dict(values="v1, v2,v3, ")) assert(get(request_1) == ['v1', 'v2', 'v3']) assert(get(request_2) == ['v1', 'v2', 'v3']) assert(get(request_3) == ['v1', 'v2', 'v3'])
  • 34.
    @get_object(MeterInstance) def post(self, request,*args, **kwargs): @optionable def get_data(data): return data.get("data", None) return get_data(request.DATA) .map( lambda item: handle_error_configuration(item, kwargs["contatore"])) .map( lambda item: Response(dict(detail=item), status=200)) .get_or(Response("BadRequest", status=400))
  • 35.
    Useful libraries • Fn.py(https://github.com/kachayev/fn.py) (A bit of Scala, ported to Python) • Underscore.py (ported from underscore_js)
  • 36.
    Summary • Functionalprogramming for writing more succint code • Change viewpoint: think in terms of functions • Python seems for dummies until you show some cool features • Python can be slow but with a fine tuning can be more efficient
  • 37.