
# ######################################################
# Properties:
#
#    obj.variable  ===>  obj.getter( ) and obj.setter( )
#
# Properties are also useful for computed data attributes:
#
#    cost = shares * price

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares    # CALLS the @shares.setter method !!!
        self.price = price

    @property			# Define "shares" are a "property" function !
    def shares(self):           # Meaning: obj.share <=> obj.share()
        return(self._shares)

    # Function that layers the "set" operation 
    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError("Value is not an integer")
        self._shares = value

    # Functions that implements the "cost" property
    # NOTE: it does NOT have a setter method,
    #       So you CANNOT assign to "cost"
    # NOTE: try REMOVING "@property"
    #       This will define the METHOD cost()
    #       You can call this METHOD with:  s.cost() !!!
    @property
    def cost(self):
        return(self.shares * self.price)

s = Stock('IBM', 50, 91.1)	## Access __init__( )

# Trouble:
#
# We can update shares with an INTEGER:
#
s.shares = 100			# Correct
print(s.__dict__)

print(s.cost)			# This will call  s.cost() !!!

# Attribute access now triggers the getter and setter methods 
# under @property and @shares.setter.

# So: 
#   @property allows you to drop the extra parantheses, 
#   hiding the fact that it’s actually a method

# NOTE:
#
#   @property   is ACTUALLY a DECORATOR !!!!
#
#      A decorator is a modifier that’s applied to the FUNCTION DEFINITION
#      that immediately follows the declartor declaration
#
# More detail in:
#
#     https://dabeaz-course.github.io/practical-python/Notes/07_Advanced_Topics/00_Overview.html


