Descriptor Example: Attribute Validation

LineItem Take #3: A Simple Descriptor


In [5]:
class Quantity:
    
    def __init__(self, storage_name):
        self.storage_name = storage_name
        
    def __set__(self, instance, value):
        if value > 0:
            instance.__dict__[self.storage_name] = value
        else:
            raise ValueError('value must be > 0')
            
class LineItem:
    weight = Quantity('weight')
    price = Quantity('price')
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price

In [9]:
truffle = LineItem('White truffle', 100, 0)


---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-9-9db5146ae683> in <module>()
----> 1 truffle = LineItem('White truffle', 100, 0)

<ipython-input-5-d176b7abc5af> in __init__(self, description, weight, price)
     17         self.description = description
     18         self.weight = weight
---> 19         self.price = price
     20 
     21     def subtotal(self):

<ipython-input-5-d176b7abc5af> in __set__(self, instance, value)
      8             instance.__dict__[self.storage_name] = value
      9         else:
---> 10             raise ValueError('value must be > 0')
     11 
     12 class LineItem:

ValueError: value must be > 0

In [10]:
truffle = LineItem('White truffle', 100, 1)

In [13]:
truffle.__dict__


Out[13]:
{'description': 'White truffle', 'price': 1, 'weight': 100}

LineItem Take #4: Automatic Storage Attribute Names


In [14]:
class Quantity:
    __counter = 0
    
    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter
        self.storage_name = '_{}#{}'.format(prefix, index)
        cls.__counter += 1
        
    def __get__(self, instance, owner):
        return getattr(instance, self.storage_name)
    
    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self.storage_name, value)
        else:
            raise ValueError('value must be > 0')
            
class LineItem:
    weight = Quantity()
    price = Quantity()
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price

In [15]:
coconuts = LineItem('Brazilian coconut', 20, 17.95)

In [16]:
coconuts.weight, coconuts.price


Out[16]:
(20, 17.95)

In [17]:
getattr(coconuts, '_Quantity#0'), getattr(coconuts, '_Quantity#1')


Out[17]:
(20, 17.95)

In [18]:
LineItem.weight


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-18-b9c12b383de2> in <module>()
----> 1 LineItem.weight

<ipython-input-14-f1a254c1447e> in __get__(self, instance, owner)
     10 
     11     def __get__(self, instance, owner):
---> 12         return getattr(instance, self.storage_name)
     13 
     14     def __set__(self, instance, value):

AttributeError: 'NoneType' object has no attribute '_Quantity#0'

In [19]:
class Quantity:
    __counter = 0
    
    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter
        self.storage_name = '_{}#{}'.format(prefix, index)
        cls.__counter += 1
        
    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return getattr(instance, self.storage_name)
    
    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self.storage_name, value)
        else:
            raise ValueError('value must be > 0')
            
class LineItem:
    weight = Quantity()
    price = Quantity()
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price

In [20]:
LineItem.price


Out[20]:
<__main__.Quantity at 0xa063d3ac88>

In [21]:
br_nuts = LineItem('Brazil nuts', 10, 34.95)
br_nuts.price


Out[21]:
34.95

LineItem Take #5: A New Descriptor Type


In [25]:
import abc

class AutoStorage:
    __counter = 0
    
    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter
        self.storage_name = '_{}#{}'.format(prefix, index)
        cls.__counter += 1
        
    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return getattr(instance, self.storage_name)
    
    def __set__(self, instance, value):
        setattr(instance, self.storage_name, value)
        
class Validated(abc.ABC, AutoStorage):
    
    def __set__(self, instance, value):
        value = self.validate(instance, value)
        super().__set__(instance, value)
        
    @abc.abstractmethod
    def validate(self, instance, value):
        """return validated value or raise ValueError"""
        
class Quantity(Validated):
    """a number greater than zero"""
    
    def validate(self, instance, value):
        if value <= 0:
            raise ValueError('value must be > 0')
        return value
    
class NonBlank(Validated):
    """a string with at least one non-space character"""
    
    def validate(self, instance, value):
        value = value.strip()
        if len(value) == 0:
            raise ValueError('value cannot be empty or blank')
        return value

In [26]:
class LineItem:
    description = NonBlank()
    weight = Quantity()
    price = Quantity()
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price

Overriding Versus Nonoverriding Descriptors


In [27]:
### auxiliary functions for display only ###

def cls_name(obj_or_cls):
    cls = type(obj_or_cls)
    if cls is type:
        cls = obj_or_cls
    return cls.__name__.split('.')[-1]

def display(obj):
    cls = type(obj)
    if cls is type:
        return '<class {}>'.format(obj.__name__)
    elif cls in [type(None), int]:
        return repr(obj)
    else:
        return '<{} object>'.format(cls_name(obj))
    
def print_args(name, *args):
    pseudo_args = ', '.join(display(x) for x in args)
    print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args))
    
### essential classes for this example ###

class Overriding:
    """a.k.a. data descriptor or enforded descriptor"""
    
    def __get__(self, instance, owner):
        print_args('get', self, instance, owner)
        
    def __set__(self, instance, value):
        print_args('set', self, instance, value)
        
class OverridingNoGet:
    """an overriding descriptor without ``__get__``"""
    
    def __set__(self, instance, value):
        print_args('set', self, instance, value)

class NonOverriding:
    """a.k.a. non-data or shadowable descriptor"""
    
    def __get__(self, instance, owner):
        print_args('get', self, instance, owner)

class Managed:
    over = Overriding()
    over_no_get = OverridingNoGet()
    non_over = NonOverriding()
    
    def spam(self):
        print('-> Managed.spam({})'.format(display(self)))

Overriding Descriptor


In [29]:
obj = Managed()
obj.over


-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

In [30]:
Managed.over


-> Overriding.__get__(<Overriding object>, None, <class Managed>)

In [31]:
obj.over = 7


-> Overriding.__set__(<Overriding object>, <Managed object>, 7)

In [32]:
obj.over


-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

In [33]:
obj.__dict__['over'] = 8

In [34]:
vars(obj)


Out[34]:
{'over': 8}

In [35]:
obj.over


-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

Overriding Descriptor Without get


In [36]:
obj.over_no_get


Out[36]:
<__main__.OverridingNoGet at 0xa063ca3048>

In [37]:
Managed.over_no_get


Out[37]:
<__main__.OverridingNoGet at 0xa063ca3048>

In [38]:
obj.over_no_get = 7


-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)

In [39]:
obj.over_no_get


Out[39]:
<__main__.OverridingNoGet at 0xa063ca3048>

In [40]:
obj.__dict__['over_no_get'] = 9
obj.over_no_get


Out[40]:
9

In [41]:
obj.over_no_get = 7
obj.over_no_get


-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)
Out[41]:
9

Nonoverriding Descriptor


In [42]:
obj = Managed()
obj.non_over


-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)

In [43]:
obj.non_over = 7
obj.non_over


Out[43]:
7

In [44]:
Managed.non_over


-> NonOverriding.__get__(<NonOverriding object>, None, <class Managed>)

In [45]:
del obj.non_over

In [46]:
obj.non_over


-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)

Overwriting a Descriptor in the Class


In [47]:
obj = Managed()
Managed.over = 1
Managed.over_no_get = 2
Managed.non_over = 3
obj.over, obj.over_no_get, obj.non_over


Out[47]:
(1, 2, 3)

Methods Are Descriptors


In [ ]: