
# ######################################################
# Restricting the set of attributes in an object
#
#    the  __slots__  TUPLE variable
#

class Stock:

    __slots__ = ('name', '_shares', 'price') # Restricts the PHYSICAL attributes

    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
    @property
    def cost(self):
        return(self.shares * self.price)

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

#
s.shares = 100			# Correct
print(s.cost)			# This will call  s.cost() - OK !!!

s.xxx = 100			# INCorrect

# NOTE:
#
# When you use __slots__, Python uses a *** more efficient***  internal 
# representation of objects. 
# IT NO LONGER uses an underlying dictionary !!!
# So:
#
#    s.__dict__  WILL NOT WORK any more !!!

# It should be noted that __slots__ is most commonly used as 
# an optimization on classes that serve as data structures. 

# Using slots will make such programs use far-less memory and 
# run a bit faster. 

# You should probably avoid __slots__ on most other classes however.

