0% found this document useful (0 votes)
9 views28 pages

Understanding Functional Programming Concepts

Functional programming is a paradigm centered around pure functions, recursion, first-class functions, and immutability, originating from lambda calculus. Key concepts include pure functions that produce consistent outputs without side effects, higher-order functions that accept or return functions, and built-in higher-order functions like map and filter for efficient data manipulation. Advantages of functional programming include predictability, modularity, ease of concurrency, and improved code optimization and maintainability.

Uploaded by

anaanfzd
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
9 views28 pages

Understanding Functional Programming Concepts

Functional programming is a paradigm centered around pure functions, recursion, first-class functions, and immutability, originating from lambda calculus. Key concepts include pure functions that produce consistent outputs without side effects, higher-order functions that accept or return functions, and built-in higher-order functions like map and filter for efficient data manipulation. Advantages of functional programming include predictability, modularity, ease of concurrency, and improved code optimization and maintainability.

Uploaded by

anaanfzd
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

Unit 4

Functional programming: Functional programming is a programming paradigm in which code is


structured primarily in the form of functions. The origins of this programming style arise from a branch of
mathematics known as lambda calculus, which is the study of functions and their mathematical
properties. In contrast to the popular object-oriented and procedural approaches, functional programming
offers a different way of thinking when solving a problem.

Concepts of Functional Programming: Any Functional programming language is expected to follow


these concepts.

 Pure Functions: These functions have two main properties. First, they always produce the same
output for the same arguments irrespective of anything else. Secondly, they have no side-effects
i.e. they do not modify any argument or global variables or output something.
 Recursion: There are no “for” or “while” loop in functional languages. Iteration in functional
languages is implemented through recursion.
 Functions are First-Class and can be Higher-Order: First-class functions are treated as first-class
variables. The first-class variables can be passed to functions as a parameter, can be returned
from functions or stored in data structures.
 Variables are Immutable: In functional programming, we can’t modify a variable after it’s been
initialized. We can create new variables – but we can’t modify existing variables.

Pure Functions: Pure functions have two properties.


 It always produces the same output for the same arguments. For example, 3+7 will always be 10
no matter what.
 It does not change or modifies the input variable.
The second property is also known as immutability. The only result of the Pure Function is the value it
returns. They are deterministic. Programs done using functional programming are easy to debug because
pure functions have no side effects or hidden I/O. Pure functions also make it easier to write
parallel/concurrent applications. When the code is written in this style, a smart compiler can do many
things – it can parallelize the instructions, wait to evaluate results when needing them, and memorize the
results since the results never change as long as the input doesn’t change.
Example of a Pure Function:
def add(a, b):
return a + b
In this example, add() is a pure function because:
 It doesn’t change any external state.
 For the same input a and b, it always produces the same output.
Benefits of Pure Functions:
 Predictable: Since they always produce the same output for the same input, they are easier to
reason about.
 Testable: Testing pure functions is simpler because there are no external dependencies.
 Optimization: In functional programming, pure functions can be memoized (cached), improving
performance.
Example of an Impure Function:
x = 10

def increment(value):
global x
x += value
return x
Here, increment() is an impure function because it modifies the global variable x, introducing side effects.

Functions are First-Class and can be Higher-Order:


First-class objects are handled uniformly throughout. They may be stored in data structures, passed as
arguments, or used in control structures. A programming language is said to support first-class functions
if it treats functions as first-class objects.
Properties of first class functions:
 A function is an instance of the Object type.
 You can store the function in a variable.
 You can pass the function as a parameter to another function.
 You can return the function from a function.
 You can store them in data structures such as hash tables, lists, …

# Python program to demonstrate


# higher order functions
def shout(text):
return [Link]()
def whisper(text):
return [Link]()
def greet(func):
# storing the function in a variable
greeting = func("Hi, I am created by a function passed as an argument.")
print(greeting)

greet(shout)
greet(whisper)

Higher Order Functions


Before moving on to read about higher-order functions, let's understand how functions can be used as
first-class objects.
In languages that do not support functional programming, the functions can either be defined or called.
However, in languages that support functional programming, there are functions that are treated as first-
class objects or first-class citizens.
Here, we can use functions as any other data, i.e. we can store them in variables, and we can also pass
them to other functions and return them from other functions just like we do with other variables. Let's
look at some code that demonstrates these.
# a simple function that prints text
def function():
print("I'm just a function!")

# function call
function()
# assigning the function to a variable
another_name_for_function = function()
# using the newly assigned variable to call the function
another_name_for_function()
Output:
I'm just a function!
I'm just a function!
In line number 6, we're just creating a new reference to the pure function function() that is
named another_name_for_function. Now you can call the function() using the variable as well, as we did
in line 7. Take a look at a few more examples.
def another_func():
print("Just another function!")
# printing a few objects, along with a function as an object
# printing the function will return a function object
print("dog", another_func, 31)
Output:
dog <function another_func at 0x7f81b4d29bf8> 31
You can display a function using the print() method as above.
# creating a list containing a function
some_objects = ['dog', another_func, 31]
print(some_objects[1])
# calling the function from the list showing object behavior of functions
print(some_objects[1]())
Output:
<function another_func at 0x7f735844b280>
Just another function!
It is also possible to store a function in a list much like any other object and call it.
Now, coming to higher-order functions. We know that functions can be passed to other functions are
parameters and that is exactly what we do in higher-order functions. Higher-order functions essentially
either accept other functions as parameters or return other functions. Let's look at some code to
demonstrate higher-order functions.
# a non-generic function that only writes the input text
def write_multiple(text: str, n: int):
for i in range(n):
print(text)
# this function call will print the input text 6 times
write_multiple("word", 6)
Output:
word
word
word
word
word
word
This function simply takes some text as input and prints it a certain number of times, which is also taken
in input. What if we wanted to perform some other function other than printing, and generalize our
function write_multiple to performing other functions than just printing? We could simply leave this
choice to the user, and pass the print function whenever we wanted to print the text and other methods for
performing other functions.
# writing a higher-order generic function that takes an action (to be performed on the input text) and a
'number of times' as input
def higher_order_write(text: str, n: int, action):
# for 'n' number of times
for i in range(n):
# perform the action on text
action(text)
# this function call will perform the 'print' action 4 times on the input text
higher_order_write("words", 4, print)
Output:
word
word
word
word
Let's say we have a task, wherein we have to create a function that adds the numbers 2, 5, and 10 to
certain numbers in a list. We can surely create functions such
as increment2 and increment5 and increment10 for this task the following way.
def increment2(nums: [int]) -> [int]:
new_nums = []
# looping over the original list
for num in nums:
# appending new values in the new list
new_nums.append(num + 2)
# returning a new list
return new_nums
print(increment2([12, 13]))
Output:
[14, 15]
 First-class functions refer to the general capability of functions to be treated as objects in the
language, allowing them to be assigned, passed, and returned.
 Higher-order functions specifically refer to functions that operate on other functions, either
by taking them as arguments or returning them, thus enabling functional programming
paradigms like function composition and callbacks.
# Define two functions
def add(x, y):
return x + y

def subtract(x, y):


return x - y

# Higher-order function that takes another function as an argument


def calculate(func, a, b):
return func(a, b)

# Use the higher-order function


result1 = calculate(add, 10, 5) # Passing the 'add' function
result2 = calculate(subtract, 10, 5) # Passing the 'subtract' function

print(result1) # Output: 15
print(result2) # Output: 5

Built-in Higher-order Functions: The following built-in higher-order functions are some of the
commonly used functions that return an iterator instead of a list or other data types for memory/space
efficiency reasons.
The map function:
This function gives us the ability to apply any function to every element on an object that is iterable (an
object over which we can iterate, or use a loop). For example, if we wanted to append a greeting to
multiple names in a list, we could do the following:
names = ['Bhavya', 'Abhijeet', 'Alkesh', 'Muskaan']

def add_greeting(name: str):


return "Hello " + name

# Use map to create an iterator of greetings


names_and_greetings = map(add_greeting, names)

# Printing the map object will show it is an iterator


print(names_and_greetings)

# Iterating over the map object and printing each greeting


for name in names_and_greetings:
print(name)
Output:
<map object at 0x7f87d27c9340>
Hello Bhavya
Hello Abhijeet
Hello Alkesh
Hello Muskaan

The filter Function


This function, as the name suggests, filters out some values from an iterable according to a specified
condition. The filter function returns either True or False.
Let's say we want to filter out the numbers that are even from a list of numbers, we can do it the following
way.
# a function that returns true for an even number given as input and false for an odd number
def is_even(num: int) -> bool:
return num % 2 == 0
nums = [1, 2, 3, 4, 5, 6, 7, 8]
# the filter function will extract values from the list that give 'True'
evens = filter(is_even, nums)
print(list(evens)) # since the filter will return an iterable object we print it as a type list
Output:
[2, 4, 6, 8]
You can even combine the two functions - map and filter and use them for expensive data manipulations!

Lambda Expressions OR Anonymous function: Lambda expressions are another concept of functional
programming. They are essentially anonymous functions. While creating functions in python, we usually
use the def keyword and give the function a unique name. However, lambda expressions allow us to skip
that process and write small functions much more quickly.
The syntax of a lambda expression is as follows:
lambda [arguments] : expression
For example, we can write a higher-order function that returns the n th power of a number using a lambda
expression.
# creating a higher-order function that returns a lambda expression
def higher_order_power(n: int) -> int:
# this lambda expression returns the nth power of x
return lambda x: x ** n
power3 = higher_order_power(3)
print(power3(4)) # prints the cube of 4

Output:64
The higher-order function returned to us the 3rd power of 4 which is 64.
x = lambda a, b : a * b
print(x(5, 6))
def myfunc(n):
return lambda a : a * n
mydoubler = myfunc(2)
print(mydoubler(11))

Let's look at an example of a lambda function to see how it works. We'll compare it to a regular user-
defined function.
Assume I want to write a function that returns twice the number I pass it. We can define a user-defined
function as follows:
def f(x):
return x * 2

f(3)
>> 6
Now for a lambda function. We'll create it like this:
lambda x: x * 3
As I explained above, the lambda function does not have a return keyword. As a result, it will return the
result of the expression on its own. The x in it also serves as a placeholder for the value to be passed into
the expression. You can change it to whatever you want.
Now if you want to call a lambda function, you will use an approach known as immediately invoking the
function. That looks like this:
(lambda x : x * 2)(3)

>> 6
The reason for this is that since the lambda function does not have a name you can invoke (it's
anonymous), you need to enclose the entire statement when you want to call it.
When Should You Use a Lambda Function?
You should use the lambda function to create simple expressions. For example, expressions that do not
include complex structures such as if-else, for-loops, and so on.
So, for example, if you want to create a function with a for-loop, you should use a user-defined function.

Reduce () function: Unlike map and filter functions, reduce() is not a built-in function, but is defined in
built-in functools module. It also needs two arguments, a function and an iterable. However it returns a
single value. The argument function is applied to two successive items in the list from left to right. Result
of the function in first call becomes first argument and third item in list becomes second. Cumulative
result is the return value of reduce() function.
In the example below, add() function is defined to return addition of two numbers. This function is used
in reduce() function along with a range of numbers between 0 to 100. Output is sum of first 100 numbers.

from functools import reduce

numbers = [1, 2, 3, 4, 5]

# Defining a function to add two numbers


def add(x, y):
return x + y
# Using reduce to sum the numbers
result = reduce(add, numbers)
print(result) # Output: 15

In this example:

 The add() function takes two arguments and returns their sum.
 reduce() applies add() cumulatively to the elements of numbers: (((1 + 2) + 3) +
4) + 5, resulting in 15.

Lambda function can also be used as a filter. Following program use lambda function that filters all
vowels from given string.
string='''
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
'''
consonants=list(filter(lambda x: x not in ['a','e','i','o','u'],string))
print (''.join(consonants))

Lambda function as argument to map() function. The lambda function itself takes two arguments
taken from two lists and returns first number raised to second. The resulting mapped object is then parsed
to output list.
powersmap=map(lambda x,y: x**y, [10,20,30], [4,3,2])
print (list(powersmap))

Using map with filter:


list(map(square, filter(is_odd, numbers)))
[1, 9, 49, 121]

Advantages of Functional Programming in Python

 Predictability: It’s crucial to have predictable code since it makes testing and debugging easier.
When a function includes side effects, it can be more challenging to predict how it would act in
certain circumstances. Pure functions are essential to functional programming because they make
it simple to predict a function’s result given a specific input. Proving advantage to Developer.
 Modularity: Functional programming facilitates the simpler avoidance of errors brought on by
mutable states and side effects through the use of pure functions and immutability.
 Concurrency & Parallelism: Due to the simplicity of creating multi-threaded or multi-process
applications the functions can run concurrently without running the danger of colliding. Making
no issue for race situations and synchronization problems are not a concern because functions do
not depend on any external state. Additionally, Functional programming promotes the use of
immutable data structures and encourages writing pure functions, which makes it easier to write
parallel and concurrent code.
 Code optimization & Refactoring: Since functions have predictable outputs, the compiler or
interpreter can make assumptions about how the function will behave and optimize accordingly.
This makes functional programming a good choice for applications that need to be highly
performant.
 Readability, Reusability & Maintainability: Small, reusable functions can be used by developers
to build more complex programs thanks to functional programming. Additionally, it promotes the
use of functions and discourages the use of side effects, making the code easier to read and
understand. This reduces the complexity of the code and makes it simpler to maintain.
 Testability: With the use of pure functions, functional programming makes it easier to test code,
as it ensures that the output of a function only depends on its input.
 Avoidance of Bugs: By Using the above mention methods when combined helps to avoid bugs
easy for the developer to detect any bugs and resolve them.

Logic programming:

In logic programming, a program is not a sequence of instructions (like in C or Python


normally)
it’s a set of facts and rules that describe relationships between things.

The structure, logic, and control are the three main parts that define how a logic program
works.

Concept Defines Example

Structu Representation of facts &


parent('John', 'Mary')
re rules

grandparent(X, Y) :- parent(X, Z),


Logic Meaning of relationships
parent(Z, Y)

How rules are executed to


Control Looping, searching, or backtracking
find results

Structure → What information exists

Logic → What’s true and how facts connect

Control → How to search for and infer answers


You can remember it like this:

“Structure tells what we know, Logic tells what it means, Control tells how we use it.”

Writing code in Python, C, C++, Java, etc. we have observed paradigms like object-oriented
programming (OOPS), abstraction, looping constructs and numerous other states of programming. Logic
Programming is just another programming paradigm that works on relationships. These relationships are
built using facts and rules and are stored as a database of relations. It is a programming methodology that
works on formal and explicit logic of events.
Relation: Relations are the basis of logic programming. A relation can be defined as a fact that follows a
certain rule. For example, a relation given by [A -> B] is read as “if A is true, then B occurs”. In language
terms, this can be read as, “If you are an Engineer, then you are a Graduate” and infers that, “Engineers
are Graduates”. In programming languages, the semantics of writing relations changes based on the
language’s syntax, but this is the overall rationality behind what relations mean.
Facts: Every program that is built on logic needs facts. To achieve a defined goal, facts need to be
provided to the program. As the name suggests in general English, facts are merely the truth. True
statements that represent the program and the data. For instance, Washington is the capital of the USA.
Rules: Rules, like programming syntax are the constraints that help in drawing conclusions from a
domain. These are logical clauses that the program or the fact needs to follow to build a relation.

USE CASES OF LOGIC PROGRAMMING:

1. Logic Programming is extensively used in Natural Language Processing (NLP) since


understanding languages is about recognition of patterns that numbers cannot represent.

2. It is also used in prototyping models. Since expressions and patterns can be replicated using logic,
prototyping is made easy.

3. Pattern matching algorithms within image processing, speech recognition and various other
cognitive services also use logic programming for pattern recognition.

4. Scheduling and Resource Allocation are major operations tasks that logic programming can help
solve efficiently and completely.

5. Mathematical proofs are also easy to decode using logic programming.

Structure of Python Logic Programming: Let’s talk about facts and rules. Facts are true statements- say,
Bucharest is the capital of Romania. Rules are constraints that lead us to conclusions about the problem
domain. These are logical clauses that express facts. We use the following syntax to write a rule (as a
clause):
H :- B1, …, Bn.
We can read this as:
H if B1 and … and Bn.
Here, H is the head of the rule and B1, …, Bn is the body. A fact is a rule with no body:
An example would be:
fallible(X) :- human(X)
Every logic program needs facts based on which to achieve the given goal.
Rules are constraints that get us to conclusions.

How to Solve Problems with Logic Programming: Logic Programming uses facts and rules for solving
the problem. That is why they are called the building blocks of Logic Programming. A goal needs to be
specified for every program in logic programming. To understand how a problem can be solved in logic
programming, we need to know about the building blocks − Facts and Rules –

Facts: Actually, every logic program needs facts to work with so that it can achieve the given goal. Facts
basically are true statements about the program and data. For example, Delhi is the capital of India.

Rules: Actually, rules are the constraints which allow us to make conclusions about the problem domain.
Rules are basically written as logical clauses to express various facts. For example, if we are building any
game then all the rules must be defined.
Rules are very important to solve any problem in Logic Programming. Rules are basically logical
conclusion which can express the facts. Following is the syntax of rule −
A∶− B1,B2,...,Bn.
Here, A is the head and B1, B2, ... Bn is the body.
For example 1. Ancestor(X, Y):- father(X, Y).
 This rule means that if X is the father of Y, then X is also an ancestor of Y.
 In simple words, a father is considered an ancestor of their child.
2. Ancestor(X, Z):- father(X, Y), ancestor(Y, Z).
 This rule means that if X is the father of Y and Y is an ancestor of Z, then X is also an ancestor of
Z.
 In other words, if X is the father of Y and Y is an ancestor of Z, then X must be an ancestor of Z.
This captures the idea of recursively defining the ancestor relationship.

Kanren: Kanren is a library within PyPi that simplifies ways of making business logic out of code. The
logic, rules, and facts we discussed previously can be turned into code using ‘kanren’. It uses advanced
forms of pattern matching to understand the input expressions and build its own logic from the given
input.

Examples: kanren enables the expression of relations and the search for values which satisfy them. The
following code is the "Hello, world!" of logic programming. It asks for `1` number, `x`, such that `x == 5`
>>> from kanren import run, eq, membero, var, conde
>>> x = var()
>>> run(1, x, eq(x, 5))
(5,)
Multiple variables and multiple goals can be used simultaneously. The following code asks for a number
x such that `x == z` and `z == 3`
>>> z = var()
>>> run(1, x, eq(x, z),
eq(z, 3))
(3,)
kanren uses [unification], an advanced form of pattern matching, to match within expression trees.
The following code asks for a number, x, such that `(1, 2) == (1, x)` holds.
>>> run(1, x, eq((1, 2), (1, x)))
(2,)
The above examples use `eq`, a *goal constructor* to state that two expressions are equal. Other goal
constructors exist such as `membero(item, coll)` which states that `item` is a member of `coll`, a
collection.
The following example uses `membero` twice to ask for 2 values of x, such that x is a member of `(1, 2,
3)` and that x is a member of `(2, 3, 4)`.
>>> run(2, x, membero(x, (1, 2, 3)), # x is a member of (1, 2, 3)
membero(x, (2, 3, 4))) # x is a member of (2, 3, 4)
(2, 3)
### Logic Variables
As in the above examples, `z = var()` creates a logic variable. You may also, optionally, pass a token
name for a variable to aid in debugging:
>>> z = var('test')
>>> z
~test
Lastly, you may also use `vars()` with an integer parameter to create multiple logic variables at once:
>>> a, b, c = vars(3)
>>> a
~_1
>>> b
~_2
>>> c
~_3

### Representing Knowledge


kanren stores data as facts that state relationships between terms.
The following code creates a parent relationship and uses it to state facts about who is a parent of whom
within the Simpsons family.
>>> from kanren import Relation, facts
>>> parent = Relation()
>>> facts(parent, ("Homer", "Bart"),
... ("Homer", "Lisa"),
... ("Abe", "Homer"))
>>> run(1, x, parent(x, "Bart"))
('Homer',)
>>> run(2, x, parent("Homer", x))
('Lisa', 'Bart')

We can use intermediate variables for more complex queries. Who is Bart's grandfather?
>>> y = var()
>>> run(1, x, parent(x, y),
parent(y, 'Bart'))
('Abe',)
We can express the grandfather relationship separately. In this example we use `conde`, a goal constructor
for logical *and* and *or*.
>>> def grandparent(x, z):
... y = var()
... return conde((parent(x, y), parent(y, z)))
>>> run(1, x, grandparent(x, 'Bart'))
('Abe,')

QUESTION:

 Define a relation food and program these facts: avocado, carrot, tomato and broccoli are food.

 Define a relation color and program these facts: carrot is orange, avocado is green, broccoli is
green and tomato is red.

 Define a relation likes and program these facts: Jeff likes carrot, avocado and baseball, Bill likes
avocado and baseball, Steve likes tomato and baseball, Mary likes broccoli and Peter likes
baseball.

from kanren import Relation, fact, run, var

food = Relation()
color = Relation()
likes = Relation()

fact(food, "avocado")
fact(food, "carrot")
fact(food, "tomato")
fact(food, "broccoli")

fact(color, "avocado", "green")


fact(color, "carrot", "orange")
fact(color, "broccoli", "green")
fact(color, "tomato", "red")

fact(likes, "Jeff", "avocado")


fact(likes, "Jeff", "carrot")
fact(likes, "Jeff", "baseball")
fact(likes, "Bill", "avocado")
fact(likes, "Bill", "baseball")
fact(likes, "Steve", "tomato")
fact(likes, "Steve", "baseball")
fact(likes, "Mary", "broccoli")
fact(likes, "Peter", "baseball")
An example query:
Jeff likes some food and its color is green.
>>> x = var()
>>> run(2, x, likes("Jeff", x), color(x, "green"))
('avocado',)

PyDatalog: pyDatalog adds the logic programming paradigm to Python's toolbox, in a pythonic way.
You can now run logic queries on databases or Python objects, and use logic clauses to define python
classes. In particular, pyDatalog can be used as a query language:
 it can perform multi-database queries (from memory datastore, 11 relational databases, and
noSQL database with appropriate connectors)
 it is more expressive than SQL, with a cleaner syntax;
 it facilitates re-use of SQL code snippet (e.g. for frequent joins or formula);
Datalog = SQL + recursivity
Datalog is a truly declarative language derived from Prolog, with strong academic foundations. It
complements Python very well for:
 managing complex sets of related information (e.g. in data integration or the semantic web).
 simulating intelligent behavior (e.g. in games),
 performing recursive algorithms (e.g. in network protocol, code and graph analysis, parsing)
 solving discrete constraint problems.

Example 1: Factorial using PyDatalog

from pyDatalog import pyDatalog


pyDatalog.create_terms('factorial, N')

factorial[N] = N * factorial[N-1]
factorial[1] = 1

print(factorial[3] == N) # prints N=6

Step-by-Step Explanation
1. pyDatalog.create_terms('factorial, N')

This line creates logic variables factorial and N.

Think of factorial as a logical relation, not a normal Python function.

2. factorial[N] = N * factorial[N-1]

This defines a recursive logic rule:

“The factorial of N is N × factorial of (N−1).”

It’s just like in math:

factorial(N) :- N * factorial(N-1)

3. factorial[1] = 1
This defines the base case (the fact):

“Factorial of 1 is 1.”

4. print(factorial[3] == N)

This is a query.

It asks:

“What is N when factorial(3) = N ?”

The system automatically uses the rules to infer:


factorial(3) = 3 × factorial(2)factorial(2) = 2 × factorial(1)factorial(1) = 1
So factorial(3) = 3 × 2 × 1 = 6

Example 2: Power and Square Logic

from pyDatalog import pyDatalog

pyDatalog.create_terms("A, Pow2, Pow3")

def square(n):
return n*n

# Creating square as pyDatalog variable (optional)


pyDatalog.create_terms("square")

input_values = range(10)[::-1] # Reverse order (9 to 0)

print(A.in_(input_values) & (Pow2 == square(A)) & (Pow3 == A**3))

pyDatalog module deals with data using its terms. That means, we need to create pyDatalog terms using
create_terms() method of pyDatalog class in pyDatalog module. This method globally declares the
datalog constants, variables, and unprefixed predicates. Then, based on several operators, we can query
and filter the results based on the specified conditions. Of course, the relational operators have been
overridden (i.e., modified) to work as desired with pyDatalog variables.
Also, you may note that input_values has been reversed, as in_() method loop parses values in reverse
order as mentioned, so this pre-reversal will maintain the ascending order of output.

Variables and expressions: The next step is to declare the variables we'll use. They must start with an
upper-case letter:
pyDatalog.create_terms('X,Y')
Variables appear in logic queries, which return a printable result
# give me all the X so that X is 1
print(X==1)
X
-
1
Queries can contain several variables and several criteria ('&' is read 'and'):
# give me all the X and Y so that X is True and Y is False
print((X==True) & (Y==False))
X|Y
-----|------
True | False
Note the parenthesis around each equality: they are required to avoid confusion with (X==(True &
Y)==False).
Some queries return an empty result :
# give me all the X that are both True and False
print((X==True) & (X==False))
[]
Besides numbers and booleans, variables can represent strings. Furthermore, queries can contain python
expressions:
# give me all the X and Y so that X is a name and Y is 'Hello ' followed by the first letter of X
print((X==raw_input('Please enter your name : ')) & (Y=='Hello ' + X[0]))
Please enter your name : World
X|Y
------|--------
World | Hello W
In the second equality, X is said to be bound by the first equality, i.e. the first equality gives it a value,
making it possible to evaluate the expression in the second equality.
pyDatalog has no symbolic resolver (yet) ! If a variable in an expression is not bound, the query returns
an empty solution :
# give me all the X and Y so that Y is 1 and Y is X+1
print((Y==1) & (Y==X+1))
[]
To use your own functions in logic expressions, define them in Python, then ask pyDatalog to create
logical terms for them:
def twice(a):
return a+a
pyDatalog.create_terms('twice')
print((X==1) & (Y==twice(X)))
X|Y
--|--
1|2
Note that X must be bound before calling the function.
Similarly, pyDatalog variables can be passed to functions in the Python standard library:
# give me all the X and Y so that X is 2 and Y is the square root of X
import math
pyDatalog.create_terms('math')
print(X==2) & (Y==[Link](X))
X|Y
--|--------------
2 | 1.41421356237
Loops: Let's first declare the Variables we'll need:
A loop can be created by using the .in() method (we'll see that there are other ways to create loops later):
from pyDatalog import pyDatalog
pyDatalog.create_terms('X,Y,Z')
# give me all the X so that X is in the range 0..4
print(X.in_((0,1,2,3,4)))
X
-
0
1
3
2
4
Here is the procedural equivalent
for x in range(5):
print x
0
1
2
3
4

Parallel Programming: The more complex a program gets the more often it is handy to divide it into
smaller pieces. This does not refer to source code, only, but also to code that is executed on your machine.
One solution for this is the usage of subprocesses in combination with parallel execution. Thoughts
behind this are:
 A single process covers a piece of code that can be run separately
 Certain sections of code can be run simultaneously, and allow parallelization in principle
 Using the features of modern processors, and operating systems, for example every core of a
processor we have available to reduce the total execution time of a program
 To reduce the complexity of your program/code, and outsource pieces of work to specialized
agents acting as subprocesses
Using sub-processes requires you to rethink the way your program is executed, from linear to parallel. It
is similar to changing your work perspective in a company from an ordinary worker to a manager - you
will have to keep an eye on who is doing what, how long does a single step take, and what are the
dependencies between the intermediate results.
A possible use case is a main process, and a daemon running in the background (master/slave) waiting to
be activated. Also, this can be a main process that starts worker processes running on demand. In practice,
the main process is a feeder process that controls two or more agents that are fed portions of the data, and
do calculations on the given portion.
Keep in mind that parallelization is both costly, and time-consuming due to the overhead of the
subprocesses that is needed by your operating system. Compared to running two or more tasks in a linear
way, doing this in parallel you may save between 25 and 30 percent of time per subprocess, depending on
your use-case. For example, two tasks that consume 5 seconds each need 10 seconds in total if executed
in series, and may need about 8 seconds on average on a multi-core machine when parallelized. 3 of those
8 seconds may be lost to overhead, limiting your speed improvements.

How many maximum parallel processes can you run?


The maximum number of processes you can run at a time is limited by the number of processors in your
computer. If you don’t know how many processors are present in the machine, the cpu_count() function
in multiprocessing will show it.
import multiprocessing as mp
print("Number of processors: ", mp.cpu_count())

What is Synchronous and Asynchronous execution?


In parallel processing, there are two types of execution: Synchronous and Asynchronous.
A synchronous execution is one the processes are completed in the same order in which it was started.
This is achieved by locking the main program until the respective processes are finished.
Asynchronous, on the other hand, doesn’t involve locking. As a result, the order of results can get mixed
up but usually gets done quicker.
There are 2 main objects in multiprocessing to implement parallel execution of a function: The Pool Class
and the Process Class.
1. Pool Class
1. Synchronous execution
 [Link]() and [Link]()
 [Link]()
2. Asynchronous execution
 Pool.map_async() and Pool.starmap_async()
 Pool.apply_async())
2. Process Class
The Process class represents a single process. It is a way to create and manage a new process in Python.
You create an instance of the Process class, specifying the target function and any arguments it requires.
Each instance of Process runs independently and can be started with the start() method and terminated
with the join() method to wait for the process to finish.

There are two main ways to handle parallel programs:


 Shared Memory
In shared memory, the sub-units can communicate with each other through the same memory space. The
advantage is that you don’t need to handle the communication explicitly because this approach is
sufficient to read or write from the shared memory. But the problem arises when multiple process access
and change the same memory location at the same time. This conflict can be avoided using
synchronization techniques.
 Distributed memory
In distributed memory, each process is totally separated and has its own memory space. In this scenario,
communication is handled explicitly between the processes. Since the communication happens through a
network interface, it is costlier compared to shared memory.

Multi−Processing in Python : Multi−processing, as opposed to multi−threading, offers genuine


parallelism by using several processes, each with their own memory space. A high−level interface for
implementing multi−processing is provided by the multiprocessing module of Python. Multiprocessing is
appropriate for CPU−bound activities since each process runs in a distinct Python interpreter, avoiding
the GIL multi−threading restriction.
Using pool class:
The Pool class in multiprocessing can handle an enormous number of processes. It allows you to run
multiple jobs per process (due to its ability to queue the jobs). The memory is allocated only to the
executing processes, unlike the Process class, which allocates memory to all the processes. The Pool class
takes the number of worker processes to be present in the pool and spawns the processes.
Launching many processes using the Process class is practically infeasible as it could break the OS.
Hence comes Pool which shall handle the distribution of jobs to and collection of results from all the
spawned processes in the presence of a minimal number of worker processes (most preferably, the
number of worker processes is equal to CPU cores).
Pool doesn’t work in interactive interpreter and Python classes. It requires __main__ module to be
importable by the children.
Multiprocessing is used in the code [Link] burden is divided among the available processes by the
map() method once the pool class generates a pool of worker processes. The results list is a collection of
the results.
import multiprocessing
def square(number):
return number ** 2
numbers = [1, 2, 3, 4, 5]
with [Link]() as pool:
results = [Link](square, numbers)
print(results)

Step-by-Step Explanation

import multiprocessing
This imports the built-in Python library for parallel processing using multiple CPU cores.

def square(number): return number ** 2


This defines a function that squares a number.

numbers = [1, 2, 3, 4, 5]
A simple list of numbers we want to process.

with [Link]() as pool:


This creates a pool of worker processes — by default, one for each CPU core.
For example:

On a 4-core machine → 4 separate Python processes will run in parallel.

The with statement ensures that the pool is automatically closed and cleaned up after use.

results = [Link](square, numbers)


Here’s the key line

[Link]() works just like Python’s built-in map() function.


But instead of running one by one, it divides the data (numbers) among the available worker processes.

Each process runs:


square(1), square(2), square(3), square(4), square(5)
in parallel, not sequentially.
Then all results are collected into a single list:
[1, 4, 9, 16, 25]
Faster when the task is heavy.

Using Process class


The Process class in multiprocessing allocates all the tasks in the memory in one go. Every task created
using the Process class has to have a separate memory allocated.
Imagine a scenario wherein ten parallel processes are to be created where every process has to be a
separate system process.
Here’s an example (order of the output is non-deterministic):
The Process class initiated a process for numbers ranging from 0 to 10. target specifies the function to be
called, and args determines the argument(s) to be passed. start() method commences the process. All the
processes have been looped over to wait until every process execution is complete, which is detected
using the join() method. join() helps in making sure that the rest of the program runs only after the
multiprocessing is complete.
sleep() method helps in understanding how concurrent the processes are!

from multiprocessing import Process


def numbers(start_num):
for i in range(5):
print(start_num+i, end=' ')
if __name__ == '__main__':
p1 = Process(target=numbers, args=(1,))
p2 = Process(target=numbers, args=(10,))
[Link]()
[Link]()
# wait for the processes to finish
[Link]()
[Link]()

# output:
# 1 2 3 4 5 10 11 12 13 14
In this program, two separate processes run the numbers function at the same time with different
parameters. First, we create two Process objects and assign them the function they will execute when they
start running, also known as the target function. Second, we tell the processes to go ahead and run their
tasks. And third, we wait for the processes to finish running, then continue with our program.
While the output looks the same as if we ran the numbers function twice sequentially, we know that the
numbers in the output were printed by independent processes.
Difference between Process class and Pool:
Feature Process Class Pool Class
Full manual control over individual
Used for Managing many similar tasks easily
processes
How many processes you Automatically creates a group of worker
You decide manually (p1, p2, etc.)
create processes
Ideal for Few, different tasks Many, similar tasks
You must call start() and join() [Link]() handles start/join
Start/Join
yourself automatically
Returns results Harder — you need Queue or Pipe Easy — [Link]() returns list of results
Two different functions running in
Example use Same function on many data items
parallel

Benefits of Using Multiprocessing :Here are a few benefits of multiprocessing:

 better usage of the CPU when dealing with high CPU-intensive tasks
 more control over a child compared with threads
 easy to code
The first advantage is related to performance. Since multiprocessing creates new processes, you can make
much better use of the computational power of your CPU by dividing your tasks among the other cores.
Most processors are multi-core processors nowadays, and if you optimize your code you can save time by
solving calculations in parallel.
The second advantage looks at an alternative to multiprocessing, which is multithreading. Threads are not
processes though, and this has its consequences. If you create a thread, it’s dangerous to kill it or even
interrupt it as you would do with a normal process. Since the comparison between multiprocessing and
multithreading isn’t in the scope of this article, I encourage you to do some further reading on it.
The third advantage of multiprocessing is that it’s quite easy to implement, given that the task you’re
trying to handle is suited for parallel programming.

Exchanging objects between processes


Multiprocessing supports two types of communication channel between processes:
Queues: The Queue class is a near clone of queue. For example:
from multiprocessing import Process, Queue
def f(q):
[Link]([42, None, 'hello'])
if __name__ == '__main__':
q = Queue()
p = Process(target=f, args=(q,))
[Link]()
print([Link]()) # prints "[42, None, 'hello']"
[Link]()
Queues are thread and process safe.
Pipes: The Pipe() function returns a pair of connection objects connected by a pipe which by default is
duplex (two-way). For example:
from multiprocessing import Process, Pipe
def f(conn):
[Link]([42, None, 'hello'])
[Link]()
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target=f, args=(child_conn,))
[Link]()
print(parent_conn.recv()) # prints "[42, None, 'hello']"
[Link]()
The two connection objects returned by Pipe() represent the two ends of the pipe. Each connection object
has send() and recv() methods (among others). Note that data in a pipe may become corrupted if two
processes (or threads) try to read from or write to the same end of the pipe at the same time. Of course
there is no risk of corruption from processes using different ends of the pipe at the same time.

Synchronization between processes:


Multiprocessing contains equivalents of all the synchronization primitives from threading. For instance
one can use a lock to ensure that only one process prints to standard output at a time:
from multiprocessing import Process, Lock
def f(l, i):
[Link]()
try:
print('hello world', i)
finally:
[Link]()
if __name__ == '__main__':
lock = Lock()
for num in range(10):
Process(target=f, args=(lock, num)).start()
Without using the lock output from the different processes is liable to get all mixed up.

Key differences between Data Parallelisms and Task Parallelisms :


Data Parallelisms Task Parallelisms

1. Same task are performed on different subsets 1. Different task are performed on the same or different data.
of same data.

2. Synchronous computation is performed. 2. Asynchronous computation is performed.

3. As there is only one execution thread 3. As each processor will execute a different thread or process
operating on all sets of data, so the speedup is on the same or different set of data, so speedup is less.
more.
Data Parallelisms Task Parallelisms

4. Amount of parallelization is proportional to 4. Amount of parallelization is proportional to the number of


the input size. independent tasks is performed.

5. It is designed for optimum load balance on 5. Here, load balancing depends upon on the e availability of
multiprocessor system. the hardware and scheduling algorithms like static and
dynamic scheduling.

Network programming: Python plays an essential role in network programming. The standard library of
Python has full support for network protocols, encoding, and decoding of data and other networking
concepts, and it is simpler to write network programs in Python than that of C++.

There are two levels of network service access in Python. These are:
 Low-Level Access
 High-Level Access
In the first case, programmers can use and access the basic socket support for the operating system using
Python's libraries, and programmers can implement both connection-less and connection-oriented
protocols for programming.
Application-level network protocols can also be accessed using high-level access provided by Python
libraries. These protocols are HTTP, FTP, etc

Why Python for Network Programming?

There are several reasons for using Python for this course. The simplicity of python makes it the most
powerful language. Python is syntactically simplest to implement amongst its counterparts.
Also, you can do almost everything with python. 'Ohh.. Almost everything, Can we make a website using
python? Can we make face detection application using python? can we make our own personal assistant
using python? Can we make a penetration testing tool using python?
The answer to all of the above questions is a big YES!
The third party libraries support provided by python makes it limitless. There is a proper documentation
for the third party libraries as well, hence using them in your application becomes easier.
Lastly, python is powerful enough to make websites like Quora and provide the backbone for the Google
search engine, so yes, python is the perfect choice for network programming.

Sockets:

To understand what sockets are, let's start with the Internet Connection. The Internet Connection basically
connects two points across the internet for data sharing and other stuff. One process from
computer C1 can communicate to a process from computer C2, over an internet connection. It has
following properties:

 Reliable: It means until the cables connecting two computers are safe, data will be transferred
safely.
 Point-to-Point: Connection is established between 2 points.
 Full-Duplex: It means transfer of information can occur in both ways i.e. from client to server as
well as server to client simultaneously (at the same time).
Sockets are the endpoints of a bidirectional, point-to-point communication channel. Given an internet
connection, say between client (a browser) and the server we will have two sockets. A Client Socket and
a Server Socket.
Socket acts on two parts: IP Address + Port Number
For now, just focus on Connect and Bind methods.
Connect is used by the client socket to start a connection with the server. This request is fulfilled
by bind method of the server socket. If you are having problem with the code, don't worry. Every bit of it
will be explained separately with examples.
Before concluding, let's see some differences between the client and the server sockets:
 Unlike client sockets, server sockets are not short lived. For example: you might need
[Link] for a single request but [Link] has to be up 24*7 for any request which it
might receive from users across the globe.
 Unlike client socket which uses Ephermal Port for connection, server socket requires a standard
or well defined port for connection like: Port 80 for Normal HTTP Connection, Port 23 for Telnet
etc.
There are two type of sockets: SOCK_STREAM and SOCK_DGRAM. Below we have a comparison of
both types of sockets.

SOCK_STREAM SOCK_DGRAM

For TCP protocols For UDP protocols

Reliable delivery Unrelible delivery

Guaranteed correct ordering of packets No order guaranteed

Connection-oriented No notion of connection(UDP)

Bidirectional Not Bidirectional

Socket Module in Python :To create a socket, we must use [Link]() function available in the
Python socket module, which has the general syntax as follows:

S = [Link](socket_family, socket_type, protocol=0)


 socket_family: This is either AF_UNIX or AF_INET. We are only going to talk about INET
sockets in this tutorial, as they account for at least 99% of the sockets in use.

 socket_type: This is either SOCK_STREAM or SOCK_DGRAM.

 Protocol: This is usually left out, defaulting to 0.

Client Socket Methods: Following are some client socket methods:


connect( ): To connect to a remote socket at an address. An address format(host, port) pair is used
for AF_INET address family.

Server Socket Methods : Following are some server socket methods:

 bind( ):This method binds the socket to an address. The format of address depends on socket
family mentioned above(AF_INET).
 listen(backlog):This method listens for the connection made to the socket. The backlog is the
maximum number of queued connections that must be listened before rejecting the
connection.
 accept( ): This method is used to accept a connection. The socket must be bound to an
address and listening for connections. The return value is a pair(conn, address) where conn is
a new socket object which can be used to send and receive data on that connection, and
address is the address bound to the socket on the other end of the connection.

Few General Socket Methods:For the below defined socket object,

s = [Link](socket_family, socket_type, protocol=0)

TCP Socket Methods UDP Socket Methods

[Link]()→ Receives TCP messages [Link]()→ Receives UDP messages

[Link]()→ Transmits TCP messages [Link]()→ Transmits UDP messages

Some Basic Socket Methods

 close() This method is used to close the socket connection.

 gethostname() This method returns a string containing the hostname of the machine where the
python interpreter is currently executing. For example: localhost.

 gethostbyname() If you want to know the current machine's IP address, you may
use gethostbyname(gethostname())

Working with TCP Sockets:


In TCP there is a concept of handshake. So, What is a handshake? It's a way to ensure that the connection
has been established between interested hosts and therefore data transfer can be initiated.
In simple terms, when you make a phone call to someone you first say "Hello", and in return the person
replies with a "Hello". This ensures that the connection has been established between both the parties and
data(voice in this case)transfer can begin now. This is, for sure, the simplest example of handshake.
Unlike UDP, TCP protocol requires that you establish a connection first. So if A is to send a letter using
TCP protocol to B, he has to first establish a connection with B. Once a connection is established, A can
then send the first envelope and wait for B to acknowledge that he has received it. Once A gets the
acknowledgment from B that the envelope 1 is safely received, A can send envelope 2. The process
repeats until A tells B that he has sent all the envelopes.
On the basis of the above example we can device the properties of TCP:
 Reliable: TCP manages message acknowledgment, retransmission and timeout. Multiple attempts
to deliver the message are made. If it gets lost along the way, the server will re-request the lost
part.
 Ordered: The messages are delivered in a particular order in which they were meant to be.

Simple Server Program

#!/usr/bin/python
#This is tcp_server.py script
import socket #line 1: Import socket module
s = [Link]() #line 2: create a socket object
host = [Link]() #line 3: Get current machine name
port = 9999 #line 4: Get port number for connection
[Link]((host,port)) #line 5: bind with the address
print "Waiting for connection..."
[Link](5) #line 6: listen for connections

while True:
conn,addr = [Link]() #line 7: connect and accept from client
print 'Got Connection from', addr
[Link]('Server Saying Hi')
[Link]() #line 8: Close the connection
This script will do nothing as of now. It waits for a client to connect at the port specified. If we run this
script now, without having a client, it will wait for the connection,
Similarly, every website you visit has a server on which it is hosted, which is always waiting for clients to
connect. Now let's create a [Link] program and try to connect with our [Link].

Simple Client Program


Below is the [Link] program. The client tries to connect to server's port, 9999(well defined port). The
code line, [Link]((host, port)) opens up a TCP connection to the hostname on the port 9999.
#!/usr/bin/python
#This is tcp_client.py script
import socket
s = [Link]()
host = [Link]() # Get current machine name
port = 9999 # Client wants to connect to server's
# port number 9999
[Link]((host,port))
print [Link](1024) # 1024 is bufsize or max amount of data to be received at once
[Link]()
Now, run the [Link] script first(if you haven’t yet) and then run the [Link] script.

Flow Diagram of the Program

Working with UDP Sockets:


UDP is a connectionless protocol. In this protocol data is sent over the internet as datagrams. In the
previous example, 20 pages may refer to 20 datagrams. Let's have a look at some properties of the UDP
protocols.

 Unreliable: When a UDP message is sent, there is no way to know if it will reach its destination
or not; it could get lost along the way. In UDP, there is no concept of acknowledgment,
retransmission, or timeout (as in TCP).

 Not ordered: If two messages are sent to the same recipient, the order in which they arrive cannot
be predicted.

 Lightweight: There is no ordering of messages, no tracking connections, etc. Hence UDP


messages are used when the rate of data transmission required is more and relibility is not
important.

 Datagrams: Packets are sent individually and are checked for integrity only if they arrive.

SERVER PROGRAM:
#!usr/bin/python
import socket
sock = [Link](socket.AF_INET,socket.SOCK_DGRAM) # For UDP
udp_host = [Link]() # Host IP
udp_port = 12345 # specified port to connect
#print type(sock) ============> 'type' can be used to see type
# of any variable ('sock' here)
[Link]((udp_host,udp_port))
while True:
print "Waiting for client..."
data,addr = [Link](1024) #receive data from client
print "Received Messages:",data," from",addr
CLIENT PROGRAM:
#!usr/bin/python
import socket
sock = [Link](socket.AF_INET,socket.SOCK_DGRAM) # For UDP
udp_host = [Link]() # Host IP
udp_port = 12345 # specified port to connect
msg = "Hello Python!"
print "UDP target IP:", udp_host
print "UDP target Port:", udp_port
[Link](msg,(udp_host,udp_port)) # Sending message to UDP server

Flow Diagram of the Program

You might also like