python learning - functional programming

Functional programming

brief introduction

  1. Function is a kind of encapsulation supported by Python built-in. We can decompose complex tasks into simple tasks by disassembling large pieces of code into functions and calling functions layer by layer. This decomposition can be called process oriented programming. Function is the basic unit of process oriented programming.
  2. Functional programming is a programming paradigm with a high degree of abstraction. The functions written in pure functional programming language have no variables. Therefore, as long as the input of any function is determined, the output is determined. This pure function is called no side effects.
  3. A programming language that allows the use of variables. Because the variable state inside the function is uncertain, the same input may get different outputs. Therefore, this function has side effects

A feature of functional programming is that it allows the function itself to be passed into another function as a parameter and return a function

Python provides partial support for functional programming. Because Python allows variables, Python is not a purely functional programming language.

Higher order function

Higher order function

Variables can point to functions

# Print abs directly and point to the function name
print(abs(-10))
print(abs) # <built-in function abs>
# Try to assign the function name to the variable - > the function itself can also be assigned to the variable, that is, the variable can point to the function.
a = abs
# print(a)  # a()=abs()  <built-in function abs>
# Calling functions through variables
print(a(-20))

# The function name is also a variable: abs() is a function whose name can be regarded as a variable. It points to a function that can calculate the absolute value!
# If you point abs to other objects, you do not have the ability to calculate the absolute value
abs=[1,2,3]
print(abs(-1)) # Typeerror: 'list' object is not callable - > now abs does not point to absolute value function, but points to list
# The point is only valid in this file: to restore the abs function, restart the Python interactive environment; 
# ABS is actually defined in the import builtins module. The definition of ABS to be overwritten is: import builtins; builtins.abs = [1,2,3]

Incoming function

# Since the variable can point to the function and the parameter of the function can receive the variable, one function can receive another function as the parameter. This function is called high-order function
def add(x, y, f):
    return f(x) + f(y)

map/reduce

  1. Map() the map() function receives two parameters, one is a function and the other is iteratable,
  2. map applies the incoming function to each element of the sequence in turn, and returns the result as a new Iterator.
li = [1,2,3,4,5];
def f1(x):
    return x ** 2
# map(f1,li) returns Iterator, which is an inert calculation. The list() function is used to calculate the print
print(type(map(f1,li)),list(map(f1,li))) # <class 'map'> [1, 4, 9, 16, 25]

  1. reduce() reduce applies a function to a sequence [x1, x2, x3,...]. This function must receive two parameters. Reduce continues to accumulate the result with the next element of the sequence
# reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
from functools import reduce
def fn(x, y):
    return x + y;
print(reduce(fn,[1,3,5,7,9]))

def fn(x, y):
    return x * 10 + y
def char2num(s):
    digits = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
    return digits[s]
print(reduce(fn, map(char2num, '13579')))

# integration
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def str2int(s):
    def fn(x, y):
        return x * 10 + y
    def char2num(s):
        return DIGITS[s]
    return reduce(fn, map(char2num, s))
# Simplify with lamda expressions
def char2num(s):
    return DIGITS[s]
def str2int(s):
    return reduce(lambda x, y: x * 10 + y, map(char2num, s))
print(str2int('123456'))

practice

# Use the map() function to change the non-standard English name entered by the user into the first letter uppercase and other lowercase standard names. Input: ['adam ',' Lisa ',' Bart '], output: ['adam', 'Lisa', 'Bart']
def normalize(name):
    pass

L1 = ['adam', 'LISA', 'barT']
L2 = list(map(normalize, L1))
print(L2)


# The sum() function provided by Python can accept a list and sum. Please write a prod() function that can accept a list and use reduce() to accumulate:
def prod(L):
    pass

print('3 * 5 * 7 * 9 =', prod([3, 5, 7, 9]))
if prod([3, 5, 7, 9]) == 945:
    print('Test successful!')
else:
    print('Test failed!')


# Use map and reduce to write a str2float function to convert the string '123.456' into a floating point number 123.456
def str2float(s):
    a = list(map(int,[x for x in s if x != '.']))
    b = reduce(lambda x,y:10*x+y,a)
    for n in range(len(s)):
        if s[n] == '.':
            break
        n+=1
    k = len(s)-n-1
    c = b/(10**k)
    return c

print('str2float(\'123.456\') =', str2float('123.456'))
if abs(str2float('123.456') - 123.456) < 0.00001:
    print('Test successful!')
else:
    print('Test failed!')

filter() function

The filter() function is used to filter the sequence.
filter() also receives a function and a sequence. Unlike map(), filter() applies the incoming functions to each element in turn,
The element is then retained or discarded depending on whether the return value is True or False.

def is_odd(n):
    return n % 2 == 1
print(list(filter(is_odd,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,78])))

def not_empty(s):
    return s and s.strip()
print(list(filter(not_empty,['A', '', 'B', None, 'C', '  '])))
  • The filter() function returns an Iterator, that is, an inert sequence, so to force filter() to complete the calculation results, you need to use the list() function to obtain all the results and return list.
# Prime number: Ehrlich sieve method
def _odd_iter():
    n = 1
    while True:
        n = n + 2
        yield n
def _not_divisible(n):
    return lambda x: x % n > 0        
def primes():
    yield 2
    it = _odd_iter() # Initial sequence
    while True:
        n = next(it) # Returns the first number of the sequence
        yield n
        it = filter(_not_divisible(n), it) # Construct new sequence
        
        
# Print prime numbers within 1000:
for n in primes():
    if n < 1000:
        print(n)
    else:
        break
 

practice

# The number of cycles refers to the same number of reads from left to right and from right to left, such as 12321909. Please use filter() to filter the number of times:
def is_palindrome(n):
    pass
# Test:
output = filter(is_palindrome, range(1, 1000))
print('1~1000:', list(output))
if list(filter(is_palindrome, range(1, 200))) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191]:
    print('Test successful!')
else:
    print('Test failed!')

sorted() function

print(sorted([36, 5, -12, 9, -21]))
# The sorted() function is also a high-order function. It can also receive a key function to realize user-defined sorting, such as sorting by absolute value:
print(sorted([36, 5, -12, 9, -21], key=abs))

# String sorting
print(sorted(['bob', 'about', 'Zoo', 'Credit']))
print(sorted(['bob', 'about', 'Zoo', 'Credit'],key=str.lower))
# Reverse sort. You don't need to change the key function. You can pass in the third parameter reverse=True
print(sorted(['bob', 'about', 'Zoo', 'Credit'],key=str.lower,reverse=True))

practice

L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
# Sort by name
def by_name(t):
    #This is a tuple in the list
    #One tuple at a time
    return t[0].lower()   #t[0]: take out the names and sort them by the initial letter (do not take out the first letter of the name)
print(sorted(L,key=by_name))

# Sort by score (from high to low)
def by_score(t):
    return -t[1]
print(sorted(L,key=by_score))

Return function

In addition to accepting functions as parameters, python advanced functions can also return functions as result values

def calc_sum(*args):
    ax = 0
    for n in args:
        ax = ax + n
    return ax

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum

Call lazy_ When sum(), the result returned is not the summation result, but the summation function

print(lazy_sum(1,2,3,4,5)) # <function lazy_sum.<locals>.sum at 0x0000029714F105E0>
print(lazy_sum(1,2,3,4,5)()) # 15
f = lazy_sum(1,2,3,4,5) # f=sum() gets the function
print(f())                   # Call sum function

closure

In function lazy_ The function sum is defined in sum, and the internal function sum can refer to the external function lazy_sum parameters and local variables,
When lazy_ When sum returns the function sum, the relevant parameters and variables are saved in the returned function. This program structure is called "Closure",
The essence is that the information of the external method in the method stack is passed to the internal method return, and each time a new method is returned

f1 = lazy_sum(1, 3, 5, 7, 9)
f2 = lazy_sum(1, 3, 5, 7, 9)
print(f1 == f2)  # false

The returned function references the local variable args inside its definition. Therefore, when a function returns a function, its internal local variable is also referenced by the new function

def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs
f1, f2, f3 = count()
print(f1())   # 9

The count function passes the final value after i changes to the fs function, so F1, F2 and F3 get the f() value when i=3
One thing to keep in mind when returning closures: the return function should not reference any cyclic variables or variables that will change later.
What if you have to reference a loop variable? The method is to create another function and bind the current value of the loop variable with the parameter of the function. No matter how the loop variable changes later, the value bound to the function parameter remains unchanged:

def count():
    def f(j):
        def g():
            return j*j
        return g
    fs = []
    for i in range(1, 4):
        fs.append(f(i)) # f(i) is executed immediately, so the current value of i is passed into f()
    return fs
f1, f2, f3 = count()
print(f1(),f2(),f3())

Note: there is no problem with the access of closures to external variables; The assignment will report an error (similar to java assigning an external value to a lambda expression)

The reason is that x is not initialized as a local variable, so it is impossible to calculate x+1 directly. But we actually want to refer to x inside the inc() function, so we need to add a declaration of nonlocal x inside the fn() function.
When using closures, you need to use nonlocal to declare that the variable is not a local variable of the current function before assigning a value to an external variable.

def inc():
    x = 0
    def fn():
        # nonlocal x: the access is OK, and the assignment error is reported
        # x = x + 1
        return x
    return fn

f = inc()
print(f()) # 1
print(f()) # 2

Use closure to return a counter function, which returns an increasing integer every time it is called:

def createCounter():
    i = 0;
    def counter():
        nonlocal i;
        i = i + 1; 
        return i;
    return counter
# Test:
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
    print('Test passed!')
else:
    print('Test failed!')
# List can avoid this problem by modifying the value in the list without changing the direction of the list.
    
def createCounter():
    list = [0];
    def counter():
        list[0] = list[0] + 1
        return list[0];
    return counter    

Anonymous function

#When you pass in a function, sometimes you don't need to explicitly define the function. It's more convenient to directly pass in an anonymous function.
li = list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
print(li)

# The anonymous function lambda x: x * x is actually:
def f(x):
    return x * x

definition
The keyword lambda represents an anonymous function, and the x before the colon represents a function parameter
limit
There can only be one expression. There is no need to write return. The return value is the result of the expression.
Using anonymous functions has an advantage, because functions do not have names, so you don't have to worry about function name conflicts. In addition, an anonymous function is also a function object. You can also assign an anonymous function to a variable, and then use the variable to call the function:

anoy = lambda k : k + 10
print(anoy)  # <function <lambda> at 0x000002D3206A0700>
print(anoy(100))   # 110

# Anonymous functions can be returned as return values
def build(x, y):
    return lambda: x * x + y * y

L = list(filter(lambda n : n % 2 == 1, range(1, 20)))

Decorator

# A function is also an object, and the function object can be assigned to a variable, so the function can also be called through a variable.
from datetime import date
def now():
    print(date.today())
f = now
f()                     # 2022-02-12
print(f.__name__)       # now
print(now.__name__)     # now

Enhance the function of now() function, and do not want to change the definition of now function: print log before and after the date (similar to java annotation)
Decorator: a way to dynamically add functionality during code execution In essence, a decorator is a higher-order function that returns a function.
log is a decorator, so it takes a function as a parameter and returns a function. We need to use Python's @ syntax to place the decorator in the definition of the function

def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper


@log                        # now = log(now)
def now():
    print(date.today())
now()    # call now():     2022-02-12

now = log(now) execution process:
Log() is a decorator and returns a function. Therefore, the original now() function still exists, but now the now variable with the same name points to the new function, so calling now() will execute the new function, that is, the wrapper() function returned in the log() function.
Wrapper (* args, * * kW): the wrapper() function can accept calls with arbitrary parameters. In the wrapper () function, the log is printed first, and then the original function is called

The decorator itself needs to pass in parameters

def log(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

@log('execute')     # now = log('execute')(now)
def now():
     print(date.today())    
now()       # execute now(): 2022-02-12

now = log('execute ') (now) execution process: first execute log('execute'), return the decorator function, and then call the returned function. The parameter is now function, and the return value is finally wrapper function.
Question: the functions decorated with decorator, their__ name__ It has changed from 'now' to 'wrapper'; You need to put the original function__ name__ And other attributes are copied into the wrapper() function, otherwise, some code execution that depends on the function signature will make mistakes.
Use Python's built-in functools wraps

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper
# Decorated with parameters
def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

Job:

# Design a decorator that can act on any function and print the execution time of the function:
import time, functools
def metric(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kw):
        start = time.time()
        result = fn(*args, **kw) 
        end = time.time()
        exe_time = 1000 * (end - start)
        print('%s executed in %s ms' % (fn.__name__, exe_time))
        return result
    return wrapper

# test
@metric
def fast(x, y):
    time.sleep(0.0012)
    return x + y;

@metric
def slow(x, y, z):
    time.sleep(0.1234)
    return x * y * z;

f = fast(11, 22)
s = slow(11, 22, 33)
if f != 33:
    print('Test failed!')
elif s != 7986:
    print('Test failed!')

Write a decorator that can print out the logs of 'begin call' and 'end call' before and after the function call.
Write a @ log decorator to support both @ log and @ log("execute")

Partial function

Partial function: from functools
When the number of parameters of a function is too many and needs to be simplified, use functools Partial can create a new function, which can fix some parameters of the original function, making it easier to call.

import functools
int2 = functools.partial(int, base=2)
print(int2('1000000'))  # 64

Keywords: Python Back-end

Added by JCF22Lyoko on Sat, 12 Feb 2022 10:40:43 +0200