Python advanced functions - basic concepts, variable scopes, iterators and generators, decorators (4-2)

In the field of programming, function is actually a program with specific functions and tasks, which is used to reduce the workload of repeatedly writing program segments.

Process oriented: functions are called procedures and sub programs;
Object oriented: functions are called methods

1. Basic concept of function

The following principles should be followed when defining a function:
1) . the function code block starts with the def keyword, followed by the function name and parentheses "()", and the colon ":" after the parentheses indicates the beginning of the function body;
2) . any incoming parameters and arguments must be placed between the parentheses "()";
3) . the first line of the function can be described with annotation statements;
4) . the function body follows the indentation syntax;
5) The function ends with a return statement and is used to return the result to the caller.

The syntax for defining functions is as follows:

    def Function name(parameter list):
        Function body
    # Defines a function that prints Hello World text
    def Print_HelloWorld():
        print('Hello World')

    # Then define a function, add two parameters to the function, and calculate the rectangular area
    def Calc_Area(width, height):
        return width * height

    # After the function definition is completed, it can be called and run as follows:
    print(Calc_Area(5, 10))        # 50 = 5 * 10

Function parameters and return values in Python do not need to explicitly define data types.

In languages such as C + + or Java, it is often necessary to specify the data type of the result returned by the function and what data type each parameter is. Like these two languages, Python is also a strongly typed language, that is, the use of variables should strictly comply with the definition, and all variables must be defined before use. If a variable is specified with a data type, it will always be the data type as long as it is not cast.

You can use the type() function to view the type of the result returned by a variable or function, as follows:

    # Use the type() function to see what type the variable or function returns
    def Cal_Area(width, height):
        print(type(width))         # <class 'int'>
        print(type(height))        # <class 'int'>
        return width * height

    # Call the function and view the type of result returned by the function
    res = Cal_Area(2, 3)
    print(res)            # 6 = 2 * 3
    print(type(res))      # <class 'int'>

1.1 anonymous functions

It refers to a function without a function name. It is often used in the following situations:
1) It is only used once in the program without defining the function name, which can save the space for variable definition in memory;
2) . when writing Shell scripts, using anonymous functions can save the process of defining functions and make the code more concise;
3) In order to make the code easier to understand;

Python uses the lambda keyword to create anonymous functions. The characteristics of anonymous functions are as follows:
1) , is just an expression and can only encapsulate limited logic.
2) . it has its own namespace and cannot access parameters outside its own parameter list or global namespace;
3) It seems that you can only write one line, but it is not equivalent to C or C + + inline functions. The purpose of the latter is to call small functions without occupying stack memory, so as to improve operation efficiency.

Define anonymous functions:

    lambda Parameter 1, Parameter 2, Parameter 3, ...parameter n :expression

    # Some simple operations can be easily rewritten into anonymous functions, such as calculating rectangular area:
    area = lambda width, height: width * height
    print(area(4, 5))           # 20

1.2 parameters and parameter transfer

The parameters of functions in Python can be divided into four types: required parameters, keyword parameters, default parameters and indefinite length parameters.

1) . required parameter: refers to the parameter that must be explicitly assigned in order to ensure the correct execution of the function;

    # Defines a string function for printing input
    def print_string(str):
        print(str)
        return

    print_string()    # An error is reported, because the parameter str is not assigned when calling the function, because it is a required parameter, and the parameter must be assigned when calling the function
    Error message: TypeError: print_string() missing 1 required positional argument: 'str'

2) Keyword parameter: indicates the name of the formal parameter and assigns the value of the actual parameter when transferring parameters;

    # Define a function that evaluates a rectangle
    def Calc_Area(width, height):
        return width * height
    print(Calc_Area(height=3, width=4))     # 12. Specify the name of the formal parameter when passing the parameter

3) . default parameter: refers to taking a default value for the parameter of a function. You can call a function without passing in a parameter with a default value. The default value is used to participate in the operation when the function is executed;

    # When defining a function that evaluates a rectangle, specify a default value for the height parameter
    def Calc_Area(width, height=5):
        return width * height
    print(Calc_Area(2))     # 10. The height parameter has been assigned a default value when the function is defined. When calling the function, you only need to pass in the width parameter

4) Variable length parameters: some functions cannot specify all parameters during definition, or the number of parameters passed in during call is more than that during definition, so variable length parameters are required.

Variable length parameters can be passed in two ways:
A. One is to add an asterisk "*" before the parameter name and import it as tuple type to store all unnamed variable parameters.

    # For indefinite length parameters, add an asterisk * before the parameter name to import as tuple type
    def Multi_Add(arg1, *args):
        sum = 0
        for var in args:
            sum += var        # Equivalent to sum = sum + var
        return arg1 + sum

    print(Multi_Add(1, 2, 3, 4, 4))    # 14 = 1+2+3+4+4

B. One is to add two asterisks "* *" before the parameter name, which is imported as a dictionary type to store all named variable parameters.

    # For variable length parameters, add two asterisks * * before the parameter name to import them as dictionary type
    def fun(**kwargs):
        for key, value in kwargs.items():
            print("{0} like {1} ".format(key, value))

    print(fun(I='sour and hot', you='spicy '))   

result:
I like hot and sour 
Do you like spicy food 
None

All variables in Python are objects.
Numbers, strings and tuples are Immutable objects;
Lists, dictionaries, and so on are Mutable objects.

1) . an unchangeable object refers to changing the value of a variable. In fact, a variable of the same type is newly generated and assigned. For example, if the variable is assigned a=1, and then changed to take its value a=2, in fact, an object of int type 2 is newly generated, and then a points to it, and 1 is discarded, which is equivalent to a newly generated a.

When an unchangeable object is used as a function parameter, it is similar to the value transfer in C, C + + and other languages. What is passed is only the value of the parameter and will not affect the unchangeable object itself, as follows:

# Objects cannot be changed as parameters
    def changeVar(a):
        a = 1

    b = 2
    changeVar(b)
    print(b)        # 2 

2) . modifiable object refers to a part of the internal value of the variable. For example, the variable assignment list = [1, 2, 3], and then changing its value list [1] = 6 actually changes the value of the second element in the list. The list itself does not change, but some internal element values are changed.

When a changeable object is used as a function parameter, similar to reference passing in C, C + + and other languages, the object itself is passed. After the content of the object is modified in the function body, the value of its internal element will be truly modified, as follows:

    # You can change the object as a parameter
    def changeVar(list):
        list.append([3, 4])
        print("Value in function", list)      # Value in function [1, 2, [3, 4]]
        return

    list = [1, 2]
    changeVar(list)
    print("Value outside function", list)   # Value outside the function [1, 2, [3, 4]]

2 variable scope

Python's variable access permission depends on the location of its assignment, which is called variable scope. There are four scopes: local scope (L), function outside the closure function (Enclosing, E), Global scope (Global, G), and built-in scope (the scope of the module where the built-in function is located, Builtin, B).

The search order of variables in the scope is: l -- > e -- > G -- > B. That is, when it cannot be found in the local scope, go to the local outside the local scope (such as closures), and if it cannot be found again, find it in the global scope. Finally, find it in the scope of the module where the built-in function is located.

Examples of defining variables in the range of L, E and G are as follows:

    global_var = 0             # G, global scope, variables defined outside the function
    def outer():
        enclosing_var = 1      # E, in the function outside the closure function
        def inner():
            local_var = 2      # 50. L ocal scope, a variable defined inside a function

The built-in scope is implemented through the builtins module. You can use the following code to view the predefined variables in the current version of Python, as follows:

>>> import builtins
>>> dir(builtins)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError'
, 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 
'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 
'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 
'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 
'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 
'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 
'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 
'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 
'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 
'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 
'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 
'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', 
'__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 
'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 
'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 
'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 
'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 
'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 
'type', 'vars', 'zip']

Local variables can only be accessed inside the function where their declaration statement is located;
Global variables can be accessed throughout the program.
When a function is called, all variable names declared within the function are scoped.

When the internal scope wants to modify the variables of the external scope, you need to use the global keyword and nonlocal keyword to declare the variables of the external scope, as follows:

    global_num = 1
    def func1():
        enclosing_num = 2
        global global_num     # Use the global keyword to declare the variables of the external scope, so that the variables of the external scope can be modified in the internal scope
        print(global_num)     # 1. The variable of the original external scope is 1
        global_num = 123              # After the internal scope is declared, you can modify the variables of the external scope to 123
        print(global_num)     # 123
        def func2():
            nonlocal enclosing_num    # Use the nonlocal keyword to declare the variables of the external scope before modifying the variables of the external scope in the internal scope
            print(enclosing_num)      # 2. The variable value of the original external scope is 2
            enclosing_num = 345       # Now change it to 345
            
        func2()
        print(enclosing_num)          # 345
        
    func1()
    print(global_num)                 # 123

Only module s, class es and functions (def and lambda) will introduce new scopes. if/elif/else, try/except and for/while statements will not introduce new scopes, that is, external variables defined in these statements can be accessed.

3. Iterators and generators

The activity of repeating the feedback process during iteration is generally to approximate the desired goal or result. The repetition of each pair of processes is called an "iteration". The result of each iteration will be used as the initial value of the next iteration.

Iteration is a way to access collective data. For strings, lists, tuples, sets and dictionaries, iterators can be used to traverse each element, and these objects that can be traversed by the for loop are also called iteratable objects.

3.1 iterators

It is an object transformed from an iteratable object after adding the iterative traversal property.

The characteristics of iterators are as follows:
1) Access starts from the first element of the collection until all the elements are accessed.
2) , you can remember the traversal position.
3) , can only move forward, not backward.

An iteratable object is not necessarily an iterator, but an iterator must be an iteratable object.

You can use the isinstance() function to distinguish whether an object is an iterator or an iteratable object, as follows:

    print(isinstance('abc', Iterable))       # True, string - is an iteratable object
    print(isinstance([1, 2, 3], Iterable))   # True, list - is an iteratable object
    print(isinstance((1, 2), Iterable))       # True, tuple - is an iteratable object
    print(isinstance({1, 3}, Iterable))       # True, dictionary - is an iteratable object
    print(isinstance(123, Iterable))          # False, integer - not an iteratable object

    print(isinstance('abc', Iterator))       # False, not an iterator
    print(isinstance([1, 2, 3], Iterator))   # False, not an iterator
    print(isinstance((1, 2), Iterator))      # False, not an iterator
    print(isinstance({1, 3}, Iterator))      # False, not an iterator
    print(isinstance(123, Iterator))         # False, not an iterator

The principles for distinguishing iterators from iteratable objects are as follows:

1) , with__ iter __ The object of () method is called iteratable object, and the method can obtain its iterator object.
2) , with__ iter __ () methods and__ next __ The object of () method is called iterator object. This method can automatically return the next result and throw StopIteration exception when the end of the sequence is reached.

That is, the iterator does not necessarily iterate over the object itself, but through__ iter __ () method can get the corresponding iterator object.
Therefore, defining iteratable objects must be implemented__ iter __ () method; To define an iterator, you must implement__ iter __ () and__ next __ () method.

For an iteratable object, you can use the iter() function to get its corresponding iterator object, and use the next() function to get the elements currently returned by the iterator object, as follows:

    I = [1, 3, 4]
    iterName=iter(I)       # Use the iter() function to get its corresponding iterator object
    print(iterName)        # <list_iterator object at 0x000001EF366D2CD0>
    print(next(iterName))  # 1. Use the next() function to get the element currently returned by the iterator object
    print(next(iterName))  # 3
    print(next(iterName))  # 4
    print(next(iterName))  # StopIteration. A StopIteration exception is thrown when the end of the sequence is reached

The iter() function calls the object directly__ iter __ () method and take its return result as its own return value;
The next() function calls the object__ next __ () method to get the current element.

Therefore, the iterator can be simply understood as "an iteratable object with a built-in for loop". Every time the iterator object is accessed using the next() function, its internal pointer will point to the next element while returning the current element.

3.2 generator

A function that uses a yield statement is called a Generator. Different from ordinary functions, a Generator is a function that returns an iterator and can only be used for iterative operations, so a Generator is actually a special iterator. Calling a Generator function returns an iterator object.

The following code uses the generator with yield statement to obtain the Fibonacci sequence:

    def Fibonacci(n):
        a, b, c = 0, 1, 0
        while True:
            if(c > n):
                return
            yield a                      # The initial value of a is 0. The _iter_ () and _next_ () methods are encapsulated here. The value of a can be obtained through the next() method
            a, b = b, a+b
            c += 1

    f = Fibonacci(6)
    while True:
        try:
            print(next(f), end=" ")   # 0 1 1 2 3 5 8
        except StopIteration:
            sys.exit()
            
 When c = 6 When, counter = 0 ,stay if Only when counter=7 The operation will stop when counter = 0,1,2,3,4,5,6 That is, when it is less than 7, it needs to continue running, so it needs to operate for 7 times and return 7 operation results. This operation result is used as the initial value of the next calculation.
 First operation: a=0, b=1, c=0,  encounter yield sentence, next(a)=0,a=b=1, b=a+b=a+1=1, c=c+1=0+1=1
 Second operation: a=1, b=1, c=1,  encounter yield sentence, next(a)=1,  a=b=1, b=a+b=1+1=2, c=c+1=1+1=2 
 Third operation: a=1, b=2, c=2,  encounter yield sentence, next(a)=1,  a=b=2, b=a+b=1+2=3, c=c+1=2+1=3
 Fourth operation: a=2, b=3, c=3,  encounter yield sentence, next(a)=2,  a=b=3, b=a+b=2+3=5, c=c+1=3+1=4
 Fifth operation: a=3, b=5, c=4,  encounter yield sentence, next(a)=3,  a=b=5, b=a+b=3+5=8, c=c+1=4+1=5
 Sixth operation: a=5, b=8, c=5,  encounter yield sentence, next(a)=5,  a=b=8, b=a+b=5+8=13, c=c+1=5+1=6
 Seventh operation: a=8, b=13, c=6,  encounter yield sentence,next(a)=8, a=b=13, b=a+b=8+13=21, c=c+1=6+1=7
 The eighth operation, c=7 ,stay if Statement satisfaction c > n , That is, 7>6 , direct return,next(a)  initiation StopIteration abnormal

Using a yield statement is equivalent to encapsulating the _iter _ () and _next _ () methods for the function. In the process of calling the generator to run, each time a yield statement is encountered, the function will pause and save the execution state of the function, return the value of the expression in the yield statement, and run from the current position the next time the next() method is executed.
yield can be understood as "return", which returns the value of the subsequent expression to the outer code block,
The difference is that after return, the function will be released, but the generator will not.
When the next method is called directly or the for statement is used for the next iteration, the generator will execute from the next yield sentence until the next yield is encountered

Generators without yield statements can be used to define generator expressions and convert lists into tuples. Using generator expressions instead of list derivation can save CPU and memory resources at the same time, as follows:

    # A generator without a yield statement can be used to define a generator expression and turn a list into tuples
    L = [1, 3, 4, 5, 6]
    T = tuple(i for i in L)
    print(T)        # (1, 3, 4, 5, 6)

Some Python built-in functions can recognize the generator expression and directly bring it into the operation, as follows:

    print(sum(i for i in range(10)))    # 45 = sum of 0-9 numbers
    range(10) The resulting list is from 0-9 ,Excluding 10, follow the principle of "left opening and right closing"

4. Decorator

According to Python's programming principles, if you want to modify or expand the function after a function is defined, you should try to avoid directly modifying the code of the function definition, otherwise the function will not work normally when called elsewhere.

Therefore, you can use decorators when you need to modify or extend the functions of a defined function without directly modifying its code.

An example is as follows:

    def func1(function):
        print('This is execution function()Before function')
        def wrapper():
            function()
        wrapper()
        print('This is execution function()After function')

    @func1       #  Here @ func1 is equivalent to func(func2)    
    def func2():
        print('Executing function()function')
result:
This is execution function()Before function
 Executing function()function
 This is execution function()After function

In Python, everything is an object, so the decorator is essentially a higher-order function that returns a function. Combined with keyword parameters, a function can be used as the return value of its external function, as follows:

    def func(arg = True):
        def func1():
            print("This is func1() function")
        def func2():
            print("This is func2() function")
        if arg == True:
            return func1()
        else:
            return func2()

    # Calling func() actually runs the func1() function and runs the func1() function
    func()    # This is the func1() function

The decorator also supports nesting. The nesting decorator executes the order from inside to outside, first calls the inner layer decorator, then calls the outer decorator.

    @a
    @b
    @c
    def f():
        print(" ---- ")

The execution order is equivalent to: f = a(b(c(f)))

Keywords: Python Pycharm

Added by mjohnson025 on Thu, 30 Sep 2021 00:55:47 +0300