Programming Python

Tasks studies - laboratory


Project maintained by dawidolko Hosted on GitHub Pages — Theme by dawidolko

Scripting Language Lab4

Functional Programming

Functional programming is a programming style that is based on functions.
A key part of functional programming is higher-order functions.
Higher-order functions take other functions as arguments or return them as results.

def apply_twice(func, arg):
    return func(func(arg))

def add_five(x):
    return x + 5

print(apply_twice(add_five, 10))

The function apply_twice takes another function as an argument and calls it twice inside.

Pure Functions

Functional programming aims to use pure functions. Pure functions have no side effects and return a value that depends only on their arguments. This is how mathematical functions work: for example, cos(x) for the same value of x always returns the same result. Below are examples of pure and impure functions.

Pure function:

def pure_function(x, y):
    temp = x + 2 * y
    return temp / (2 * x + y)

Impure function:

some_list = []

def impure(arg):
    some_list.append(arg)

The function above is not pure because it modifies the state of the some_list.

Using pure functions has both advantages and disadvantages.
Pure functions are:

Lambda Expressions

Creating functions normally (using def) assigns them to a variable automatically.
This differs from creating other objects (such as strings and integers), which can be created on the fly without assigning them to a variable.
The same is possible for functions, provided they are created using a lambda expression.
Functions created this way are called anonymous functions.
This approach is most commonly used when passing a simple function as an argument to another function.
The syntax is shown in the example below and consists of the keyword lambda, followed by a list of arguments, a colon, and an expression to compute and return.

def my_func(f, arg):
    return (f(arg))

print(my_func(lambda x: 2 * x * x, 5))

Lambda functions are not as powerful as named functions.
They can only perform operations that require a single expression—usually equivalent to a single line of code.

# Named function
def polynomial(x):
    return x ** 2 + 5 * x + 4

print(polynomial(-4))

# Lambda function
print((lambda x: x ** 2 + 5 * x + 4)(-4))

Lambda functions can be assigned to variables and used like normal functions.

double = lambda x: x * 2
print(double(7))

However, there is rarely a good reason for this—it’s usually better to define a function using def.

map and filter Functions

The built-in map and filter functions are very useful higher-order functions that operate on lists (or other iterable objects).

The map function takes a function and an iterable as arguments and returns a new iterable with the function applied to each element.

def add_five(x):
    return x + 5

nums = [11, 22, 33, 44, 55]
result = list(map(add_five, nums))
print(result)

We could achieve the same result more easily using lambda syntax:

nums = [11, 22, 33, 44, 55]

result = list(map(lambda x: x + 5, nums))
print(result)

To convert the result to a list, we used the list() function.

The filter function iterates through an iterable and removes elements that do not match the predicate (a function that returns a boolean value).

nums = [11, 22, 33, 44, 55]
res = list(filter(lambda x: x % 2 == 0, nums))
print(res)

Similar to map, the result must be explicitly converted to a list if you want to print it.

Generators

Generators are a type of iterable like lists or tuples.
Unlike lists, they do not allow indexing with arbitrary indices but can still be iterated over using a for loop.
They can be created using functions and the yield statement.

def countdown():
    i = 5
    while i > 0:
        yield i
        i -= 1

for i in countdown():
    print(i)

The yield statement is used to define a generator, replacing the return statement of a function to provide results to the caller without destroying local variables.

Since they process one element at a time, generators do not have memory limitations like lists.
In fact, they can be infinite!

def infinite_sevens():
    while True:
        yield 7

for i in infinite_sevens():
    print(i)

In short, generators allow declaring a function that behaves like an iterator, meaning it can be used in a for loop.

Finite generators can be converted into lists by passing them as arguments to the list function.

def numbers(x):
    for i in range(x):
        if i % 2 == 0:
            yield i

print(list(numbers(11)))

Using generators increases efficiency due to lazy (on-demand) value generation, reducing memory usage.

Decorators

Decorators allow modifying functions using other functions.
This is useful when you need to extend the functionality of functions without modifying them.

def decor(func):
    def wrap():
        print("==================")
        func()
        print("==================")
    return wrap

def print_text():
    print("Hello world!")

decorated = decor(print_text)
decorated()

Python allows function wrapping with a decorator by using the @ symbol before the function definition.

@decor
def print_text2():
    print("Spam")

print_text2()

Recursion

Recursion is a very important concept in functional programming.
The foundation of recursion is self-referencing by calling itself.
A classic example of recursion is computing the factorial of a number.

def factorial(x):
    if x == 1:
        return 1
    else:
        return x * factorial(x - 1)

print(factorial(5))

Recursion can also be indirect, where one function calls another, which calls the first function, and so on.

def is_even(x):
    if x == 0:
        return True
    else:
        return is_odd(x - 1)

def is_odd(x):
    return not is_even(x)

print(is_even(23))
print(is_odd(17))

Sets

Sets are data structures similar to lists or dictionaries.
They are created using curly braces {} or the set() function.
They allow checking if an item is contained within them.

num_set = {1, 2, 3, 4, 5}
word_set = set(["spam", "eggs"])

print(3 in num_set)
print("spam" not in word_set)

Sets differ from lists in several ways:

To add elements, use add().
To remove elements, use remove() or pop().

nums = {1, 2, 1, 3, 4, 5, 1}
print(nums)
nums.add(-7)
nums.remove(3)
print(nums)

itertools Module

The itertools module is part of Python’s standard library and contains useful functions for functional programming.

nums = list(accumulate(range(9)))
print(nums)
print(list(takewhile(lambda x: x <= 6, nums)))

Tasks to Complete

2-7 must be added to GitHub.

Python