Python – Closures and Decorators

I. Closure

1. Definition of closure

Closures are a special mechanism in programming languages, belonging to higher-order functions. Specifically, they
occur when:

1. functions are nested (a function is defined inside another function);
2. the inner function uses variables (or parameters) of the outer function; and
3. the outer function returns a value from the inner function.

2. Sample Code

# The external function returns the internal function, which uses the variables/arguments of the external function
def outer_func(num1):
# Num1 is the input parameter
outer_num=num1

def inner_func(num2):
# Declare variables as external function variables using the nonlocal keyword
nonlocal outer_num
# Modify external function variables
outer_num+=1
print(f'inner func return {outer_num+num2}')
return outer_num+num2

print(outer_num)
inner_func(3)
print(outer_num)

return inner_func

if __name__ == '__main__':
f=outer_func(2) # Returns an internal function that prints 2 first, then outer_num becomes 3, prints inner funcs, and then prints 3
print(f) # Original outer_num=3, then outer_num+=1 becomes 4, print inner funct return 8, and then print 8

3. Advantages of closures

1. Enables the decorator pattern.

2. Enables the function factory pattern.

3. Enables function modularization and reuse.

4. Disadvantages of closures

1. Increases code complexity;

2. Makes debugging difficult;

3. Makes memory leaks more likely.

II. Decorative Objects

1. Definition of Decorator

A decorator is a function that adds extra functionality to an existing function; it is essentially a closure function.

Features of decorators:

  • Without modifying the source code of existing functions
  • Without modifying the calling convention of existing functions
  • Add extra functionality to an existing function

Decorator usage:
Python provides a simpler way to decorate functions: @decorator_name; which can decorate existing functions.

# Do not modify the code and calling methods of existing functions
# Add additional functionality to existing functions

def login(func): # The parameter must be a function
print('decorator starts executing')

def inner_func():
print('check and login')
func()

return inner_func

# Login is required before posting comments
def comment():
print('commit a comment')

@login # uses @ syntax to indicate that it uses login to decorate the comment function, similar to annotations in Java
def comment2():
print('commit a Comment 2')

if __name__ == '__main__':
# Method 1: Directly use, the disadvantage is writing an extra line of nested code
# Add a login decorator to the comment() method and return inner_func
comment = login(comment)
# Execute inner_func, first print: Check if not logged in and log in, then print: Commit a comment
comment()
# Method 2: Use the decorator with @, keeping all other codes unchanged
comment2()

Note: @check is equivalent to comment = check(comment)

2. Use Cases

Add a function to output the execution time of multiple functions.

# Add the function of printing execution time to multiple functions
import time

def timer(func):
    def inner_func():
        start = time.time()
        func()
        end = time.time()
        print(f'function {func. __name__} running {end-start} seconds')

    return inner_func

@timer
def test1():
    print('test1')
    time.sleep(1)

def test2():
    print('test2')
    time.sleep(2)

@timer
def test3():
    print('test3')
    time.sleep(2)

if __name__ == '__main__':
    test1()
    test2()
    test3()

3. Decorators with parameters

Decorators with parameters allow you to pass specified arguments when decorating a function. The syntax is:  @decorator(parameter, …

  • Decorators can only accept one parameter, and that parameter must be a function.
  • Wrap the decorator with another function, let the outermost function receive the parameters, and return the decorator.

Case requirement: Write a decorator to add authentication functionality (user list from a file) to multiple functions. The requirement is that after a successful login, subsequent functions can be executed directly without requiring a username and password.

# Decorator with parameters
# On the basis of the decorator, wrap another layer of function and return the decorator (3rd order function)
from functools import wraps

# The user data file contains the following content:
# {'name':'boy','passwd':'123456'},
# {'name':'girl','passwd':'123456'},
file_path='../resource/users.data'

# check logged in
already_login = False

def login(file_path):
# Initialize user information and read users from the file (the file will be loaded multiple times, the number of times=@login())
user_list = []
with open(file_path,'r',encoding='utf-8') as f:
for line in f.readlines():
user_list.append(eval(line))
print ('successfully loaded user list') # This line will be printed three times. If you want to load only once, you can consider turning user_ist and the loading process into global variables

def login_decorator(func):

@wraps(func) # Use the built-in wrap decorator to disguise funcs, so that the original func function does not lose its identity information after being decorated
def inner_func(*args, **kwargs):
global already_login
# Check if logged in and skip verification; Otherwise, log in
# Because it needs to take effect globally, this variable needs to be defined outside of login
if not already_login:
print(f'The user intends to execute {func. __name__}, but has not logged in, please log in first')
name=input('username:\t')
passwd=input('Password:\t')
for user in user_list:
if user['name'] == name and user['passwd'] == passwd:
print('User login successful, execute function')
func(*args, **kwargs)
# Modify global variables to identify logged in status
already_login = True
break
else:
print(f'account password incorrect, do not execute {func. __name__}')
else:
print('User logged in, execute directly')
func(*args, **kwargs)
return inner_func

return login_decorator

@login(file_path)
def test1():
print('test1')

@login(file_path)
def test2():
print('test2')

@login(file_path)
def test3():
print('test3')

if __name__ == '__main__':
print (the name of the f'test1 function is: {test1. __name__} ') # Verify if the wraps disguise is effective
test1()
test2()
test3()

4. How to write decorator classes

Another way to implement decorators is by defining a class to decorate the function. This requires using a `call` function within the class to turn the class instance into a callable object.

# Decorator class, rewrite the __call__ method to implement the content of the inner function inside
class Login:

def __init__(self,func):
self.func = func

# If it is a decorator class, the call function must be rewritten
def __call__(self,*args,**kwargs):
print('Check if not logged in, then log in first')
self.func(*args,**kwargs)

@Login
def test1():
print("test1")

if __name__ == '__main__':
print(test1.__class__.__name__)
test1()

5. Property decorator

The property decorator is a built-in Python decorator that allows you to treat a method as an attribute, simplifying the code when calling it.

class Person:

def __init__(self):
self.__age = 0

# Obtain age
@property
def age(self):
return self.__age

# This decorator uses age as an attribute to assign a value to age
@age.setter
def age(self,new_age):
if new_age>=200 or new_age<=0:
raise Exception('Illegal Age')
else:
self.__age = new_age


if __name__ == '__main__':
p = Person()
# Operate private attributes like regular attributes
p.age=10
print(p.age)

6. The implementation principle of property decorators

# property is actually a descriptor class
class MyProperty:
    """Simplified version of property implementation"""
    def __init__(self, fget=None, fset=None, fdel=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable")
        return self.fget(obj)
    
    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("not writable")
        self.fset(obj, value)
    
    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("cannot be deleted")
        self.fdel(obj)
    
    def setter(self, fset):
        """Create a new property object and set a new setter"""
        return type(self)(self.fget, fset, self.fdel)

# Use custom properties
class Demo:
    def __init__(self, x):
        self._x = x
    
    def get_x(self):
        return self._x
    
    def set_x(self, value):
        self._x = value
    
    x = MyProperty(get_x, set_x)

demo = Demo(10)
print(demo.x)  # 10
demo.x = 20    # Call set_x
print(demo.x)  # 20