############################## Encapsulation with Descriptors ############################## :Date: 2013-03-15 :Speaker: Luciano Ramalho Assumptions =========== + You know the basics of classes/objects + New- vs old-style classes The Scenario ============ + Selling organic bulk foods + An order has several items + Each item has desc, weight, prices, subtotal (weight * price) A Simple object --------------- + Too simple? What about a negative weight? + Amazon found customers could order negative quantity and it would credit their cards!! Classic Solution ================ + Getters/setters - Breaks existing code - Protected attributes are no fun + Protected attributes in Pyhton exist for safety - to avoid accidental assignment/override - to prevent intentional use/misuse Validation with property ======================== + Implement ``weight`` as a property:: @property def weightself): return self.__weight @weight.setter: def weight(self, value): if value > 0: self.__weight = value else: raise ValueError('value must be > 0') + What if we want to do the same thing to ``price``? + Duplicate the getter/setter but for ``price``? Descriptors! ============ + Abstraction of getters/setters + Manage access to weight/price attrs:: class Quantity(object): __counter = 0 def __init__(self): prefix = '_' + self.__class__.__name__ key = self.__class__.__counter self.target_name = '%s_%s' % (prefix, key) self.__class__.__counter += 1 def __get__(self, instance, owner): return getattr(instance, self.target_name) def __set__(self, instance, value): if value > 0: setattr(instance, self.target_name, value) else: raise ValueError('value must be > 0') class LineItem(object): weight = Quantity() price = Quantity() + Get/set logic moved to ``Quantity`` descriptor class. + These instances are created at import time + Each access goes thru their descriptor What is a descriptor? ===================== + A descriptor is any class that defines ``__get__``, ``__set__``, or ``__delete__`` + ``target_name`` is the name of the attribute from the caller (aka instance) + Each ``Quantity`` instance must create and use a unique a target attribute name + e.g. ``LineItem._Quantity_0``, ``LineItem._Quantity_1`` - Instead of using a counter, perhaps you can use ``id()`` of the object Room for improvement ==================== + What about making them more descriptive: ``LineItem__weight``. + The challenge - When descriptor instantiated, LineItem class does not exist and attributes don't exist either. What now? ========= + If descirptor needs to know the name of the managed class attr... + ... Then you need to control construction using a METACLASS! + COMPLEXITY! References ========== + Raymond Hettinger's "Descriptor HowTo Guide" + Alex Martelli's "Python in a Nutshell 2e" + Dave Beazley "Python Essential Reference 4e" By the Way ========== + All Python functions are descriptors - They implement ``__get__`` + That's how a function becomes bound to an instance - ``fn.__get__`` returns a partial - this fixes the first arg (``self``) to the target instance