In functional programming, functions can be treated as objects. That is, they can be
Lets look at few example to understand what that means.
Before we can play with functions, lets check how python handles the variables and data. For that we have taken a variable a
and assigned it value 10.
In [1]:
a = 10
Now, lets check what happens if we pass it to functions id
and dir
in order to know more about them
In [2]:
print(id(a))
print(dir(a))
In the above example, value 10
is behaving like an object, with multiple attributes exposed to the world.
Now lets do the same to a function and validate if that also behaves the same.
In the below example, we are creating a function test_function
and then passed it to functions id
and dir
similar to what we did with the variable a
.
In [3]:
def test_function():
"""Test Function.
This is just a test function.
"""
pass
print(id(test_function))
print(dir(test_function))
We saw, that function also behaved the same way as an data, It also had a memory location identified for it and exposes various attributes to the world. Lets check few of them
In [4]:
print(test_function.__dir__())
print(test_function.__doc__)
print(test_function.__code__)
print(test_function.__class__)
One of the main feature of functional programming is pure functions
. Lets find out what is pure function
.
As per definition, pure function is a function which has no side effects and for the same input returns same output every time and is not dependent on any other information. As we have already discussed about them in previous chapter, we are going to skip it.
Python also supports higher-order functions, meaning that functions can
What that means is we can construct complex functions from existing functions and customize existing functions as per our needs.
Lets take example of simple example,
In [5]:
def done(val, func):
val = val * 2
return func(val)
def square(val):
return val**2
def increment(val):
return val + 1
In the above example, we have three functions, done
, square
and increment
.
done
takes two argument, val
and func
square
& increment
takes one val argument eachSince, both square
& increment
take one argument, they both can be passed to done
and in both the cases the behaviour of done
will change depending upon the behaviour
of passed function.
Lets try it out.
In [6]:
print(done(10, square))
the reason we got 400
is due to the fact that square returns square
of the number passed to it. In our case we passed 10
to done, which increased it to 20
before passing it to square
function and inturn received square of 20
which equals to 400
.
In [7]:
print(done(10, increment))
the reason we got 21
is due to the fact that increment
returns one move value than the number passed to it. In our case we passed 10
to done
, which increased it to 20
before passing it to increment
function and inturn received 21
from increment
.
Python allows function(s) to be defined within the scope of another function. In this type of setting the inner function is only in scope inside the outer function, thus inner functions are returned (without executing) or passed into another function for more processing.
In the below example, a new instance of the function inner()
is created on each call to outer()
. That is because it is defined during the execution of outer()
. The creation of the second instance has no impact on the first.
So, we have two functions, outer
and inner
. outer
function returns the instance of inner
function and inner function performs some operations on the values provided and returns them.
In [8]:
def outer(a):
"""
Outer function
"""
y = 0
def inner(x):
"""
inner function
"""
y = x*x*a
return(y)
print(a)
return inner
my_out = outer(10)
In the above example, my_out
contains the address of the instance of inner
function when value of a
is 102
. Lets check that out by just printing my_out
In [9]:
print(my_out)
Now, lets perform some operations on it by passing values to my_out
.
In [10]:
for i in range(5):
print(my_out(i))
In above for loop
execution, value of a
has remained constant and value of x
has changed as shown in the below calculations.
In [11]:
0 * 0 * 10 == 0
Out[11]:
In [12]:
1 * 1 * 10 == 10
Out[12]:
In [13]:
2 * 2 * 10 == 40
Out[13]:
In [14]:
3 * 3 * 10 == 90
Out[14]:
In [15]:
4 * 4 * 10 == 160
Out[15]:
Note in all the above exeuction, we have used the same instance of outer. Now lets create another instance of outer and try the above code.
Also note, that we have returned the address of inner functions instance and not executed the inner function while returning. The returned inner function gets executed later in the code.
In [16]:
my_out_2 = outer(2)
In [17]:
for i in range(5):
print(my_out_2(i))
Now, we have two instances of outer
with different values of a
thus returns they both return different values for same set of code (except where we updated the value of a
).
Lets take another example, and see what happens when we have identifiers with same name in differnet scopes
In [18]:
x = 0
def outer():
x = 1
def inner():
x = 2
print("inner:", x)
inner()
print("outer:", x)
outer()
print("global:", x)
Lets take the above example, we have two functions, outer
& inner
. We also have x
variable which is present as global
and also present in both the functions.
If we want to access x
of outer
function from inner
function than global
keyword not help. Fortunately, Python provides a keyword nonlocal
which allows inner
functions to access variables to outer
functions as shown in below example.
The details of nonlocal are details in https://www.python.org/dev/peps/pep-3104/
In [19]:
x = 0
def outer():
x = 1
def inner():
nonlocal x
x = 2
print("inner:",x, "id:", id(x))
inner()
print("outer:",x, "id:", id(x))
outer()
print("global:",x, "id:", id(x))
In [20]:
def outer(a):
"""
Outer function
"""
PI = 3.1415
def inner(x):
"""
inner function
"""
nonlocal PI
print(PI)
y = x*PI*a
return("y =" + str(y))
print(a)
return inner
In [21]:
ten = outer(10)
second = outer(20)
print("*"*20)
print(ten)
print(ten(10))
print("*"*20)
print(second)
print(second(10))
You use inner functions to protect them from anything happening outside of the function, meaning that they are hidden from the global scope.
In [22]:
# Encapsulation
def increment(current):
def inner_increment(x): # hidden from outer code
return x + 1
next_number = inner_increment(current)
return [current, next_number]
print(increment(10))
NOTE: We can not access directly the inner function as shown below
In [23]:
try:
increment.inner_increment(109)
except Exception as e:
print(e)
In [24]:
# Keepin’ it DRY
import os
def process(file_name):
if isinstance(file_name, str):
with open(file_name, 'r') as f:
for line in f.readlines():
print(line)
else:
for line in file_name:
print(line)
process(["test", "test3", "t33"])
process(os.path.join("files", "process_me.txt"))
In [25]:
# Keepin’ it DRY
import os
def process(data):
def do_stuff(file_process):
for line in file_process:
print(line)
if isinstance(data, str):
with open(data, 'r') as f:
do_stuff(f)
else:
do_stuff(data)
process(["test", "test3", "t33"])
process(os.path.join("files", "process_me.txt"))
or have similar logic which can be replaced by a function, such as mathematical functions, or code base which can be clubed by using some parameters.
In [26]:
def square(n):
return n**2
def cube(n):
return n**3
print(square(2))
In [27]:
def sqr(a, b):
return a**b
??? why code
In [28]:
def test():
print("TEST TEST TEST")
def yes(name):
print("Ja, ", name)
return True
return yes
d = test()
print("*" * 14)
a = d("Murthy")
print("*" * 14)
print(a)
In [29]:
def power(exp):
def subfunc(a):
return a**exp
return subfunc
square = power(2)
hexa = power(6)
print(square)
print(hexa)
print(square(5)) # 5**2
print()
print(hexa(3)) # 3**6
print(power(6)(3))
# subfunc(3) where exp = 6
# SQuare
# exp -> 2
# Square(5)
# a -> 5
# 5**2
# 25
In [30]:
def a1(m):
x = m * 2
def b(v, t=None):
if t:
print(x, m, t)
return v + t
else:
print(x, m, v)
return v + x
return b
n = a1(2)
print(n(3))
print(n(3, 10))
Below code will not work as f1
is not returning anything :). This is to show what can happen with one silly tab. Also it is one of the most common mistake.
In [31]:
def f1(a):
def f2(b):
return f2
def f3(c):
return f3
def f4(d):
return f4
def f5(e):
return f5
try:
print (f1(1)(2)(3)(4)(5))
except Exception as e:
print(e)
The correct code is below
In [32]:
def f1(a):
def f2(b):
def f3(c):
def f4(d):
def f5(e):
print(e)
return f5
return f4
return f3
return f2
f1(1)(2)(3)(4)(5)
They are techniques for implementing lexically scoped name binding with first-class functions. It is a record, storing a function together with an environment. a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.
A closure—unlike a plain function—allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.
In [33]:
def f(x):
def g(y):
return x + y
return g
def h(x):
return lambda y: x + y
a = f(1)
b = h(1)
print(a, b)
print(a(5), b(5))
print(f(1)(5), h(1)(5))
both a and b are closures—or rather, variables with a closure as value—in both cases produced by returning a nested function with a free variable from an enclosing function, so that the free variable binds to the parameter x of the enclosing function. However, in the first case the nested function has a name, g, while in the second case the nested function is anonymous. The closures need not be assigned to a variable, and can be used directly, as in the last lines—the original name (if any) used in defining them is irrelevant. This usage may be deemed an "anonymous closure".
1: Copied from : "https://en.wikipedia.org/wiki/Closure_(computer_programming)"
In [34]:
def make_adder(x):
def add(y):
return x + y
return add
plus10 = make_adder(10)
print(plus10(12)) # make_adder(10).add(12)
print(make_adder(10)(12))
Closures can avoid the use of global values and provides some form of data hiding. It can also provide an object oriented solution to the problem.
When there are few methods (one method in most cases) to be implemented in a class, closures can provide an alternate and more elegant solutions. But when the number of attributes and methods get larger, better implement a class.
In functional programming, functions can be treated as objects. That is, they can assigned to a variable, can be passed as arguments or even returned from other functions.
In [35]:
a = 10
def test_function():
pass
print(id(a), dir(a))
print(id(test_function), dir(test_function))
The lambda operator or lambda function is a way to create small anonymous functions, i.e. functions without a name. These functions are throw-away functions, i.e. they are just needed where they have been created. Lambda functions are mainly used in combination with the functions filter(), map() and reduce(). The lambda feature was added to Python due to the demand from Lisp programmers.
The general syntax of a lambda function is quite simple:
lambda argument_list: expression
The argument list consists of a comma separated list of arguments and the expression is an arithmetic expression using these arguments. You can assign the function to a variable to give it a name. The following example of a lambda function returns the sum of its two arguments:
The simplest way to initialize a pure function in python is by using lambda
keyword. It helps in defining an one-line function.
Functions initialized with lambda are also called anonymous functions.
In [36]:
# Example lambda keyword
product_func = lambda x, y: x * y
print(product_func(10, 20))
print(product_func(120, 2))
In the above example higher-order function that takes two inputs- A function F(x)
and a multiplier m
.
In [37]:
concat = lambda x, y: [x, y]
print(concat([1, 2, 3], 4))
In [38]:
print(concat({}, (2, 4)))
In [39]:
product_func = lambda x, y: x * y
sum_func = lambda F, m: lambda x, y: F(x, y) + m
In [40]:
### TODO: some expl.
In [41]:
print(sum_func(product_func, 6)(2, 4))
14 = 2 * 4 + 6
F -> product_func
m => 6
x -> 2
y -> 4
2 * 4 + 6 = 8 + 6 = 14
In [42]:
print(sum_func)
In [43]:
print(sum_func(product_func, 5))
In [44]:
print(sum_func(product_func, 5)(3, 5))
We use lambda functions when we require a nameless function for a short period of time.
In Python, we generally use it as an argument to a higher-order function (a function that takes in other functions as arguments). Lambda functions are used along with built-in functions like filter(), map() etc.
Functions are first-class objects in Python, meaning they have attributes and can be referenced and assigned to variables.
In [45]:
def square(x):
"""
This returns the square of the requested number `x`
"""
return x**2
print(square(10))
print(square(100))
In the above example, we created a function square
and tested it against two values 10
and 100
. Now lets assign a variable to the above function and play with it.
In [46]:
# Assignation to another variable
power = square
print(power(100))
print(square)
print(power)
print(id(square))
print(id(power))
In the above execution, we can see that both power
and square
are pointing to same function square
.
In [47]:
# attributes present
print("*"*30)
print(power.__name__)
print("*"*30)
print(square.__code__)
print("*"*30)
print(square.__doc__)
we can see that functions also have attributes, we can see the list of attributes exposed by using the code with syntax dir(<func_name>)
.
In [48]:
print(dir(square))
We can also add attributes to a function. In the below example we are addting attribute d
to the function
In [49]:
square.d = 10
print(dir(square))
In the above example higher-order function that takes two inputs- A function F(x)
and a multiplier m
.
Python provides many functions which can also act as higher order functions. We are going to cover few of them in this section.
Max returns the largest item in an iterable or the largest of two or more arguments.
If one positional argument is provided, iterable must be a non-empty iterable (such as a non-empty string, tuple or list). The largest item in the iterable is returned. If two or more positional arguments are provided, the largest of the positional arguments is returned.
The optional key argument specifies a one-argument ordering function like that used for list.sort(). The key argument, if supplied, must be in keyword form (for example, max(a,b,c,key=func)).
Example: Basic Example
In [50]:
marks = [[1, 2], [2, 1], [2, 4], [3, 0], [3, 4], [3, 2]]
max(marks)
Out[50]:
In [51]:
marks = [1, 2, 4, 2, (5)]
max(marks)
Out[51]:
In [52]:
try:
marks = [1, 2, 4, 2, (5, 1)]
max(marks)
except Exception as e:
print(e)
In [53]:
try:
marks = [1, 2, 4, 2, [5]]
max(marks)
except Exception as e:
print(e)
Now lets take an excercise, below are the marks for students for 8 semesters, and we need to find what is the highest marks in 3rd semester.
In [54]:
import random
student_count = 10000
max_marks = 100
min_marks = 0
semester = 8
marks = [[random.randint(min_marks, max_marks) for _ in range(semester)] for _ in range(student_count)]
In [55]:
print(marks[:3])
we can achive it by using itemgetter
from operator
modules, which we have used in the past.
In [56]:
import operator
print("Max number is each semester are as follows:")
for a in range(semester):
print('\t', max(marks, key = operator.itemgetter(a))[a])
As we have passed operator.itemgetter
function as key, we can pass some custom function as well.
Now lets assume a situation, were we have to calculate total marks, which are calculated as sum of 10% of first 6 semesters and 100% of 7th & 8th semester and we wants to find the highest mark obtained for the year 1994 batch.
In [57]:
# %%timeit
def marks_sum_v1(marks_list):
total = 0
for a in range(5):
total += marks_list[a]
total *= 0.1
for a in range(6, 8):
total += marks_list[a]
return total
marks_sum_v1(max(marks, key = marks_sum_v1))
Out[57]:
In [58]:
print('Maximum marks:\t', marks_sum_v1(max(marks, key = marks_sum_v1)))
In [59]:
# %%timeit
def marks_sum_v2(marks_list):
total = 0
total = sum(marks_list[a] * 0.1 for a in range(5))
total += sum([marks_list[a] for a in range(6, 8)])
return total
marks_sum_v2(max(marks, key=marks_sum_v2))
Out[59]:
In [60]:
# %%timeit
def marks_sum_v2(marks_list):
total = sum((sum(marks_list[a] for a in range(6, 8)),
sum(marks_list[a] * 0.1 for a in range(5))))
return total
marks_sum_v2(max(marks, key=marks_sum_v2))
Out[60]:
Similarly we can also use lambda in key variable.
Lets, take another example, we are conducting games, where teams have to perform few tasks and after each task they are provided some points. The team with highest score wins.
Its our job to write an script which will take the scores (which are stored as list of lists) and provided what is the maximum score.
In our solution, we will use lambda
function to get the total of scores
In [61]:
scores = [[14, 19, 96, 91, 32, 65, 87, 27], [11, 37, 22, 93, 75, 11, 95, 95],
[14, 54, 92, 72, 13, 17, 44, 73], [17, 31, 82, 80, 40, 4, 11, 8],
[86, 83, 85, 93, 85, 42, 22, 87], [44, 61, 17, 87, 21, 35, 90, 10],
[75, 27, 67, 88, 22, 84, 4, 51], [28, 25, 66, 22, 46, 56, 76, 47],
[24, 98, 16, 20, 92, 5, 40, 12]]
results = []
for a in scores:
results.append(sum(a))
print(results)
print(max(results))
In [62]:
sum(max(scores, key=lambda x: sum(x)))
Out[62]:
Similer to max
, min
also provide the same functionality. thus is not covering in it in details except one example
In [63]:
import random
student_count = 10000
max_marks = 100
min_marks = 0
semester = 8
marks = [[random.randint(min_marks, max_marks) for _ in range(semester)] for _ in range(student_count)]
In [64]:
def marks_sum_v1(marks_list):
total = 0
for a in range(5):
total += marks_list[a]
total *= 0.1
for a in range(6, 8):
total += marks_list[a] * 1
return total
d = round(marks_sum_v1(min(marks, key = marks_sum_v1)),2)
print(d)