In [2]:
from functools import wraps

def my_name(his_name="Bob"):
    """
    In order to pass arguments to a function decorator, you need an outer layer that
    accepts the arguments and returns the function decorator function.
    """
    def my_name_decorator(function):
        """
        The function decorator function accepts the decorated function as an argument 
        and contains a wrapper function that it must return.
        """

        """
        The @wraps decorator is a helper decorator. See
        https://docs.python.org/3.4/library/functools.html#functools.wraps for more
        information.
        """
        @wraps(function)
        def my_name_wrapper(name):
            """
            The wrapper function checks state and makes whatever modifications are 
            required. It should return whatever is expected from the decorated 
            function and it may even call the original function.
            """
            print("My name is Chris, his name is {}".format(his_name))
            return function(name)
        return my_name_wrapper
    return my_name_decorator

# Calling the function decorator with an argument uses the outer function to get
# the arguments into the scope of the decorator function.
@my_name(his_name="George")
def print_name_with_arg(name):
    print("Your name is {}".format(name))

# when we don't have an argument, we still need the () so that the outer function
# will return the decorator function
@my_name()
def print_name_without_arg(name):
    print("Your name is {}".format(name))

In [5]:
print_name_with_arg("Chris")


My name is Chris, his name is George
Your name is Chris

In [6]:
print_name_without_arg("Chris")


My name is Chris, his name is Bob
Your name is Chris