<Python> Function Parameters & Nested Functions

JunePyo Suh·2020년 4월 23일
0

Nested functions and closure

Nested function example:

def parent_function():
    def child_function():
        print("this is a child function")

    child_function()

parent_function()
> "this is a child function"

There are two reasons for using nested functions: (1) Readability and (2) Closure. Nested functions can effectively rearrange repetitive codes in a parent function. Moreover, because parent functions return child (nested) functions, variables in parent function will restrict direct access from outside but still be used for any operation. This closure feature is extremely useful for "factory functions" that need to produce different functions or objects from many different initial conditions (argument inputs). By the same token, arguments for a parent function are inherited to child functions, so the child functions gain more latitude in doing operations.
Below is an example of a factory function using nested function feature.

def generate_power(base_number):
    def nth_power(power):
        return base_number ** power

    return nth_power
 
calculate_power_of_two = generate_power(2)
calculate_power_of_two(7)
> 128
calculate_power_of_two(10)
> 1024

calculate_power_of_seven = generate_power(7)
calculate_power_of_seven(3)
> 343
calculate_power_of_seven(5)
> 16907

Decorator

To begin with, one should note that in Python, (1) function names are references to functions, and (2) we can assign multiple names to the same function. This characteristic has something to do with a concept called First Class Object.

First Class Object

First Class Object refers to entities that can be freely operated by other entities as function parameters, return values, or any modifications. Famous data types such as List, String, and Dictionary are all First Class Objects. In Python, functions are also First Class Objects: Any operation that can be performed on data types mentioned above can equally be performed on functions.

def add(a, b):
    return a + b

def execute(func, *args):
    return func(*args) # 2.

f = add # 3.

>>> execute(f, 3, 5) # 1.

8

Decorators are thoroughly explained in this page. Example codes below continue to explain how functions are used as First Class Objects.

>>> def succ(x):
...     return x + 1
... 
>>> successor = succ
>>> successor(10)
11
>>> succ(10)
11

These reference names can be deleted without deleting the function itself.

>>> del succ
>>> successor(10)
11

Due to the fact that every parameter of a function is a reference to an object and functions are objects as well, functions - or better, "references to functions" - can be passed as parameters to a function.

def g():
    print("Hi, it's me 'g'")
    print("Thanks for calling me")
    
def f(func):
    print("Hi, it's me 'f'")
    print("I will call 'func' now")
    func()
    print("func's real name is " + func.__name__) 

          
f(g)

This leads to defining a first simple decorator:

def our_decorator(func):
    def function_wrapper(x):
        print("Before calling " + func.__name__)
        func(x)
        print("After calling " + func.__name__)
    return function_wrapper

def foo(x):
    print("Hi, foo has been called with " + str(x))

print("We call foo before decoration:")
foo("Hi")
    
print("We now decorate foo with f:")
foo = our_decorator(foo)

print("We call foo after decoration:")
foo(42)

As illustrated above, here is how a decorator works: our_decorator(foo) is stored in "foo," an object with reference to the function that will be decorated. The call foo(42) then triggers "our_decorator(foo(42)).

The usual syntax for decorators is:

def our_decorator(func):
    def function_wrapper(x):
        print("Before calling " + func.__name__)
        func(x)
        print("After calling " + func.__name__)
    return function_wrapper

@our_decorator
def foo(x):
    print("Hi, foo has been called with " + str(x))

foo("Hi")

Decorators with Parameters

Now, decorators may need parameters. Code below creates a decorator that appends an argument (user's name) to a greeting message.

def name_decorator(expr):
  def name_decorator_decorator(func):
    def function_wrapper():
      result = func() + expr
      return result
    return function_wrapper
  return name_decorator_decorator

@name_decorator("정우성")
def greeting():
  return "Hello, "

To take in a parameter, a parent function (name_decorator) is wrapped around the usual decorator function. It is important to note that name_decorator is not a decorator, but a parent function of a decorator function.
The real decorator function is name_decorator_decorator(func)... Although the decorator indicator "@" signals to the Python interpreter to begin the decoration process, the user should be able to conceptually differentiate a parent function that wraps the decorator, and the real decorator function. A decorator wrapped underneath a parent function should only take in the reference to a function as an argument. The closure feature enables the decorator function to inherit the "expr" argument.

An image to help

0개의 댓글