Decorators in Python are functions that modify the behavior of other functions or classes. They take in a target function or class as input and return a modified version, allowing us to add additional functionality or alter the behavior of the original code.


#Decorators
def functions(arg):
    return "value"

#Works the same as
def function(arg):
    return "value"
function = decorator(function)
#This passes the function to the decorator, and reassigns it to the functions

#We have to return the new_function, or it wouldn't reassign it to the value
def repeater(old_function):
    def new_function(*args, **kwds): #
        old_function(*args, **kwds) # we run the old function
        old_function(*args, **kwds) # we do it twice
    return new_function

To apply a decorator to a function or class, we use the “@” symbol followed by the decorator function’s name. This syntactic sugar simplifies the process of extending and enhancing the behavior of our code.


#This makes a function run twice
>>> @repeater
def multiply(num1, num2):
    print(num1 * num2)

>>> multiply(2, 3)
6
6

#You can also make it change the output
def double_out(old_function):
    def new_function(*args, **kwds):
        return 2 * old_function(*args, **kwds) # modify the return value
    return new_function

#And even the input
def double_Ii(old_function):
    def new_function(arg): # only works if the old function has one argument
        return old_function(arg * 2) # modify the argument passed
    return new_function

Decorators offer several benefits, including code reuse, separation of concerns, and the ability to add cross-cutting functionality. They promote cleaner code organization and make it easier to implement common patterns or modifications.


#Check the function
def check(old_function):
    def new_function(arg):
        if arg < 0: raise (ValueError, "Negative Argument") # This causes an error, which is better than it doing the wrong thing
        old_function(arg)
    return new_function

def multiply(multiplier):
    def multiply_generator(old_function):
        def new_function(*args, **kwds):
            return multiplier * old_function(*args, **kwds)
        return new_function
    return multiply_generator # it returns the new generator

# Usage
@multiply(3) # multiply is not a generator, but multiply(3) is
def return_num(num):
    return num

# Now return_num is decorated and reassigned into itself
return_num(5) # should return 15

Decorators offer several benefits, including code reuse, separation of concerns, and the ability to add cross-cutting functionality. They promote cleaner code organization and make it easier to implement common patterns or modifications.

Decorators Exercise Solution


#Code Completed
def type_check(correct_type):
    def check(old_function):
        def new_function(arg):
            if (isinstance(arg, correct_type)):
                return old_function(arg)
            else:
                print("Bad Type")
        return new_function
    return check

@type_check(int)
def times2(num):
    return num*2

print(times2(2))
times2('Not A Number')

@type_check(str)
def first_letter(word):
    return word[0]

print(first_letter('Hello World'))
first_letter(['Not', 'A', 'String'])

By applying decorators, we can modify the behavior of functions or classes without directly modifying their source code, promoting code reusability and enhancing code modularity. Understanding and utilizing decorators empowers us to write cleaner, more expressive, and highly extensible code.