<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Decorator without arguments
Step1:  Decorator with arguments
Step2:  Decorator with or without arguments
Step4:  Example from NetworkUnit
If there exists a pre-calculated prediction in cache return it without recalculating,
else calculate it and save it to the cache.
Optionally, a hash for the cache location can be provided.
<ASSISTANT_TASK:>
Python Code:
def decorator_wo_args(original_function):
    def wrapped_function(*args, **kwargs):
        print('args:', args, 'kwargs:', kwargs)
        out = original_function(*args, **kwargs)
        return out
        
    return wrapped_function
@decorator_wo_args
def func_dec_wo_args(func_arg='!'):
    print('func_dec_wo_args', func_arg)
func_dec_wo_args()
Explanation: Decorator without arguments
End of explanation
def decorator_w_args(argument):
    def _decorate(function):
        
        # functools fixes function metadata (e.g. name)
        @functools.wraps(function)
        def wrapped_function(*args, **kwargs):
            print('args:', args, 'kwargs:', kwargs)
            print('decorator argument:', argument)
            out = function(*args, **kwargs)
            return out
        
        return wrapped_function
    return _decorate
@decorator_w_args(42)
def func_dec_w_args(func_arg='!'):
    print('func_dec_w_args', func_arg)
func_dec_w_args(func_arg='?')
Explanation: Decorator with arguments
End of explanation
def decorator_w_or_wo_args(original_function=None, optional_argument=None):
    def _decorate(function):
        # functools fixes function metadata (e.g. name)
        @functools.wraps(function)
        def wrapped_function(*args, **kwargs):
            print('args:', args, 'kwargs:', kwargs)
            print('decorator argument:', optional_argument)
            out = function(*args, **kwargs)
            return out
        
        return wrapped_function
    if original_function:
        return _decorate(original_function)
    else:
        return _decorate
@decorator_w_or_wo_args
def func_dec_w_or_wo_args1(func_arg='!'):
    print('func_dec_w_or_wo_args1', func_arg)
@decorator_w_or_wo_args(optional_argument=99)
# in this case it the decorator argument must have a keyword
def func_dec_w_or_wo_args2(func_arg='!'):
    print('func_dec_w_or_wo_args2', func_arg)
func_dec_w_or_wo_args1('!?')
func_dec_w_or_wo_args2()
Explanation: Decorator with or without arguments
End of explanation
def use_prediction_cache(generate_prediction_func=None, hash_key=None):
    
    Decorator for the `generate_prediction()` function of the tests, handles
    cached prediction loading, parameter update and prediction saving.
    
    def _decorate(function):
        @functools.wraps(function)
        def wrapper(self, model):
            # Check if predictions were already calculated
            prediction = self.get_prediction(model, key=hash_key)
            # If any parameter was specified by the user in the generate_prediction
            # function the predictions are recalculated
            if prediction is None:
                # Generate and save prediction
                prediction = function(self, model)
                self.set_prediction(model, prediction, key=hash_key)
            return prediction
        return wrapper
    if generate_prediction_func:
        return _decorate(generate_prediction_func)
    else:
        return _decorate
Explanation: Example from NetworkUnit
If there exists a pre-calculated prediction in cache return it without recalculating,
else calculate it and save it to the cache.
Optionally, a hash for the cache location can be provided.
End of explanation
<END_TASK>