
# When to Use the @ Symbol in Python:
#
#   The main use case of the symbol @ in Python is decorators. 
#
# In Python, a decorator is a function that extends the functionality 
# of an existing function or class.

# Decorators:
#
# this piece of code . . .
#
#      def extend_behavior(func):
#          return func
#
#      @extend_behavior
#      def some_func():
#           pass
#
# . . . does the exact same as this piece of code:
#
#      def extend_behavior(func):
#          return func
#
#      def some_func():					(1)
#          Pass
#
#      some_func = extend_behavior(some_func)		(2)
#
# So some_func(x) @(2) will call: extend_behavior(some_func(x) @(1))

# Example:

# Let’s say you have a function that divides two numbers:

# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
# The definition of divide () is MOVED BELOW the definition
# of @guard_zero() !! (for syntax reasons)
# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@


# To PREVENT divide from using y=0 (WITHOUT changing divide()), 
# we can write a NEW function as follows:

# Note:
#
#   guard_zero()  accepts operate() function as an argument.
#   guard_zero()  returns the function (= address) inner()

def guard_zero(operate):
    """
    Define an inner function that test y==0
    """
    def inner(x, y):		# Python can define funct inside another func
        if y == 0:
            print("Cannot divide by 0.")
            return 		# This returns None
        else:
            return operate(x, y)

    return inner		# guard_zero(operate) returns inner
				# which is the address of the inner function
				# that take 2 parameters


# Now we can CREATE a NEW safe "divide" function as follows:
# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
# (2) REMOVE this line (assumed by the @guard_zero syntax
#
# divide = guard_zero(divide)
# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@



# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
# This is SYNTACTICAL SUGAR...
#
# (1) Add this
# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@guard_zero
def divide(x, y):
    return x / y



# Good:
print( divide(4, 5) )

# Problem:
try:
    print( divide(4, 0) )
except ZeroDivisionError:
    print("Problem: divide by 0")







# Good:
print( divide(4, 5) )

# Problem SOLVED:
print( divide(4, 0) )	# divide(4,0) ACTUALLY calls  inner(4,0) !!!




