[Liao Xuefeng] Python functional programming: return function, anonymous function, decorator, partial function

return function

function as return value

In addition to accepting functions as parameters , higher-order functions can also return functions as result values .

Let's implement a summation of variable parameters. Typically, the summation function is defined like this:

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

However, if you do not need to sum immediately, you can return the summation function instead of the summation result :

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax += n
        return ax
    return sum  # 返回求和的函数

When we call lazy_sum(), what is returned is not the summation result, but the summation function .

When the function is called , the result of the sum f()is actually calculated :

f = lazy_sum(1, 3, 5, 7, 9)
print(f)  # <function lazy_sum.<locals>.sum at 0x000001F82E9CA950>
print(f())  # 25

We lazy_sumdefine the function in the function sum, and the internal function sumcan refer to lazy_sumthe parameters and local variables of the external function. When lazy_sumthe function is returned sum, the relevant parameters and variables are saved in the returned function. This is called **"closure ( Closure"** program structure has great power.

Please note one more thing, when we call lazy_sum(), each call will return a new function , even if the same parameters are passed in:

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

f1()The calling results of and f2()have no effect on each other.

Closure

Note that the returned function references local variables within its definition args. Therefore, when a function returns a function, its internal local variables are also referenced by the new function. Therefore, closures are easy to use but not easy to implement.

Another issue to note is that the returned function is not executed immediately, but is not executed until it is calledf() . Let's look at an example:

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)  # <function count.<locals>.f at 0x000001E525AADC80>
print(f1())  # 9
print(f2())  # 9
print(f3())  # 9

It can be seen that the results of calling f1(), f2()and f3()all are 9! The reason is that the returned function references the variable i, but it is not executed immediately. By the time all three functions return, the variables they refer to ihave become 3, so the final result is 9.

! ! Keep one thing in mind when returning a closure: the returning function should not reference any loop variables, or variables that will change subsequently.

What if you must reference the loop variable? The method is to create another function and use the parameters of the function to bind the current value of the loop variable . No matter how the loop variable changes subsequently, the value bound to the function parameter remains unchanged:

def count():
    def f(j):
        def g():  # f(j)里面再创建一个函数,用该函数的参数绑定循环变量当前的值
            return j * j
        return g
    fs = []
    for i in range(1, 4):
        fs.append(f(i))   # f(i)立刻被执行,因此i的当前值被传入f()
    return fs

f1, f2, f3 = count()
print(f1())  # 1
print(f2())  # 4
print(f3())  # 9

The disadvantage is that the code is long and the lambda function can be used to shorten the code.

nonlocal

Using closures means that the inner function refers to the local variables of the outer function. If we just read the value of the outer variable , we will find that everything is normal in the returned closure function call:

def inc():
    x = 0
    def fn():
        # 仅读取x的值:
        return x + 1
    return fn

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

However, if you assign a value to an outer variable , since the Python interpreter will xtreat it as a local variable of the function fn(), it will report an error. The reason is that xit is not initialized as a local variable, and direct calculation x+1is not possible.

But we actually want to reference inc()the inside of the function x, so we need to add a declaration fn()inside the function . nonlocal xAfter adding this declaration, the interpreter fn()treats xit as a local variable of the outer function. It has been initialized and can be calculated correctly x+1.

def inc():
    x = 0
    def fn():
        nonlocal x # 如果注释掉这一句,就会报错
        x = x + 1
        return x
    return fn

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

! ! When using closures, before assigning a value to an outer variable, you need to use nonlocal to declare that the variable is not a local variable of the current function.

practise

Use a closure to return a counter function that returns an incrementing integer each time it is called:

def createCounter():
    s = [0]
    def counter():
        s[0] = s[0] + 1
        return s[0]
    return counter

counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA())  # 1 2 3 4 5
counterB = createCounter()
print(counterB(), counterB(), counterB(), counterB())  # 1 2 3 4

Assign a required sequence to s. The purpose of this is to facilitate the sub-function to directly use the variable value in the parent function without causing errors such as "local variable 'xxx' referenced before assignment".

During the practice, I saw another wrong way of writing, but it was very interesting:

s = 0  # 设置全局变量初始值
def createCounter():
    def counter():
        global s  # 引入全局变量
        s = s + 1
        return s
    return counter

counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA())  # 1 2 3 4 5
counterB = createCounter()
print(counterB(), counterB(), counterB(), counterB())  # 6 7 8 9

After calling again using counterB createCounter(), the results are 6, 7, 8, 9.


anonymous function

>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]

Anonymous functions lambda x: x * xare actually:

def f(x):
    return x * x

The keyword lambdarepresents an anonymous function, and the one before the colon xrepresents function parameters.

Anonymous functions have a limitation, that is, they can only have one expression , no need to write it return, and the return value is the result of the expression.

There is an advantage to using anonymous functions because the function has no name and you don't have to worry about function name conflicts. In addition, the anonymous function is also a function object . You can also assign the anonymous function to a variable and then use the variable to call the function:

>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x0000015C26ABC5E0>
>>> f(5)
25

Similarly, anonymous functions can also be returned as return values , such as:

def build(x, y):
    return lambda: x * x + y * y

practise

Please modify the following code with anonymous functions:

def is_odd(n):
    return n % 2 == 1

L = list(filter(is_odd, range(1, 20)))

The modified code is as follows:

L = list(filter(lambda x: x % 2 == 1, range(1, 20)))
print(L)  # [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

Decorator

Since a function is also an object, and function objects can be assigned to variables, the function can also be called through variables.

The function object has an __name__attribute that allows you to get the name of the function:

>>> def now():
	print('2015-3-25')

	
>>> f = now
>>> f()
2015-3-25
>>> now.__name__
'now'
>>> f.__name__
'now'

Suppose we want to enhance now()the function of a function, for example, automatically print logs before and after a function call, but do not want to modify the now()definition of the function. This method of dynamically adding functions while the code is running is called a decorator .

Essentially, a decorator is a higher-order function that returns a function . Therefore, we need to define a decorator that can print logs, which can be defined as follows:

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

Observe the above log, because it is a decorator, it accepts a function as a parameter and returns a function . We need to use Python's @ syntax to place the decorator at the definition of the function:

@log
def now():
    print('2015-3-25')

Calling now()a function will not only run now()the function itself, but also now()print a line of log before running the function:

Note: If you use pycharm to call now()a function, an error will be reported. Without calling now()the function and running the program directly, you can already now()print a line of log before running the function. The result is as follows:

call now(): 
2015-3-25

Putting @logit now()at the definition of the function is equivalent to executing the statement:

now = log(now)

Since log()it is a decorator and returns a function , the original now()function still exists, but now nowthe variable with the same name points to the new function, so the call now()will execute the new function, that is, the function log()returned in the function wrapper().

wrapper()The parameter definition of a function is (*args, **kw), therefore, wrapper()the function can be called with any parameters. Within wrapper()the function, the log is printed first, and then the original function is called.

If the decorator itself needs to pass in parameters, then you need to write a higher-order function that returns the decorator , which will be more complicated to write. For example, to customize the text of the log:

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

@log('execute')
def now():
    print('2022-10-06')

now()  # 很奇怪,这里就需要调用now(),否则打印不出来

# 结果如下:
# execute now():
# 2022-10-06

Compared with two levels of nested decorators, the effect of three levels of nesting is as follows:

now = log('execute')(now)

Let's analyze the above statement. It is executed first and the function log('execute')is returned decorator. Then the returned function is called. The parameters are nowthe functions and the return value is finally wrapperthe function.

There is no problem with the above two definitions of decorator, but there is still one final step left. Because we have said that functions are also objects, they have __name__other attributes, but if you look at the functions decorated with decorators, they have changed __name__from the original ones :'now''wrapper'

print(now.__name__)  # wrapper

Because the name of the returned wrapper()function is 'wrapper', it is necessary to __name__copy the attributes of the original function to wrapper()the function, otherwise, some code that relies on the function signature will execute incorrectly.

There is no need to write wrapper.__name__ = func.__name__such code, Python is built in functools.wrapsto do this. Therefore, a complete decorator is written as follows :

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('call %s():' % func.__name__)
        return func(*args, **kwargs)
    return wrapper()

Or for a decorator with parameters:

import functools

def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log('hello')
def now():
    print('2015-3-8')

now()  
# 结果如下:
# hello now():
# 2015-3-8

import functoolsis an imported functoolsmodule. The concept of modules will be explained later. Now, just remember to wrapper()prepend the definition @functools.wraps(func).

summary

In the object-oriented (OOP) design pattern, decorator is called the decoration pattern. OOP's decoration mode needs to be implemented through inheritance and combination, and Python, in addition to supporting OOP's decorator, also supports decorators directly from the syntax level. Python's decorator can be implemented as a function or a class .

Decorators can enhance the functions of functions. Although they are a bit complicated to define, they are very flexible and convenient to use.

practise

1. Please design a decorator that can act on any function and print the execution time of the function:

import time, functools


def metric(fn):
    start_time = time.time()
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        end_time = time.time()
        print('%s executed in %s ms' % (fn.__name__, end_time - start_time))
        return fn(*args, **kwargs)
    return wrapper

@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)  # fast executed in 0.0 ms
print(f)  # 33
s = slow(11, 22, 33)  # slow executed in 0.007169008255004883 ms
print(s)  # 7986

'begin call'2. Please write a decorator that can print out and 'end call'log before and after the function call .

import functools

def log(text=''):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print('begin call %s %s' % (text, func.__name__))
            func(*args, **kwargs)
            print('end call %s %s' % (text, func.__name__))
        return wrapper
    return decorator


@log()
def f1():
    print('2022-03-19')

@log('execute')
def f2(name, age):
    print('%s 今年已经 %d 岁了' % (name, age))

f1()
f2("Lucy", 8)

The result is as follows:

begin call  f1
2022-03-19
end call  f1
begin call execute f2
Lucy 今年已经 8 岁了!
end call execute f2

partial function

Python functoolsmodules provide many useful functions, one of which is partial function . It should be noted that the partial function here is different from the partial function in the mathematical sense.

When introducing function parameters, we mentioned that by setting default values ​​for parameters , the difficulty of function calling can be reduced. Partial functions can also do this. Examples are as follows:

int()The function can convert a string into an integer. When only a string is passed in, int()the function defaults to decimal conversion .

But int()the function also provides additional baseparameters, with default values ​​of 10. If you pass in baseparameters, you can perform N-base conversion:

>>> int('12345')
12345
>>> int('12345', base=8)
5349
>>> int('12345', base=16)
74565

Suppose you want to convert a large number of binary strings, which is very troublesome to pass in every time int(x, base=2). We can define a int2()function and base=2pass it in by default:

def int2(str, base=2):
	return int(str, base)

print(int2('100000'))  # 32
print(int2('1010101'))  # 85

functools.partialIt helps us create a partial function. We don’t need to define it ourselves int2(). We can directly use the following code to create a new function int2:

import functools
int2 = functools.partial(int, base=2)

print(int2('100000'))  # 32
print(int2('1010101'))  # 85

Therefore, functools.partialthe function of a simple summary is to fix certain parameters of a function (that is, set default values) and return a new function . It will be easier to call this new function.

Note that the new int2function above only resets basethe parameters to their default values 2, but other values ​​can also be passed in when the function is called:

print(int2('100000', base=8))  # 32768

Finally, when creating a partial function, you can actually receive the function object *argsand **kwthese three parameters . When passed in:

int2 = functools.partial(int, base=2)

In fact, the keyword parameters of the int() function are fixed base, that is:

int2('10010')

Equivalent to:

kw = {
    
     'base': 2 }
int('10010', **kw)

When passed in:

max2 = functools.partial(max, 10)

10In fact, the part of the action will be *argsautomatically added to the left, that is:

max2(5, 6, 7)

Equivalent to:

args = (10, 5, 6, 7)
max(*args)

The result is 10.

summary

When a function has too many parameters and needs to be simplified, you functools.partialcan create a new function. This new function can fix some of the parameters of the original function, making it easier to call.
Reference link: Return function-Liao Xuefeng’s official website (liaoxuefeng.com)

Guess you like

Origin blog.csdn.net/qq_45670134/article/details/127186969