[Python-Dev] Type checks of instance variables

Michael McLay mclay@nist.gov
Wed, 10 Oct 2001 13:35:58 -0400


Many applications require constraining the contents of instance variables to 
specific types.  For instance, the attributes of an element in a validating 
XML Schema implementation would require constraining the types allowed in 
each of the instance variables of the class implementing the element 
definition. This testing for  type constraints can be accomplished by adding 
an isinstance test to the __setattr__ of the class that contains the 
constrained attributes. 

Prior to Python 2.2 the resulting class definitions would be bloated with 
many lines of type checking code. The new builtin property class has 
simplified the task of constraint checking somewhat.  There are some 
improvements that would make it easier and less error prone to selectively 
restrict the types of instance variables.

The following change to the property class adds a  test for the instance 
variable type to the __set__ method.

class property2(object):

    def __init__(self, fget=None, fset=None, doc=None, type=None):
        self.fget = fget
        self.fset = fset
        self._type = type
        self.__doc__ = doc
         
    def __get__(self, inst, type=None):
        if self.fget is None:
            raise AttributeError, "this attribute is write-only"
        return self.fget(inst)

    def __set__(self, inst, value):
        if self.fset is None:
            raise AttributeError, "this attribute is read-only"
        if not isinstance(value,self._type):
            raise AttributeError,"attribute not of type '%s'"%\
                                                        self._type.__name__
        return self.fset(inst, value)

The following example illustrates the use of the property2 type checking 
capability.   When the assignment of a value is made to c.b the type checks 
are performed as desired.  Unfortunately this approach is not sufficient to 
guard against incorrect types being assigned to the instance variables. There 
are several examples demonstrating the "hidden" _b instance variable being 
populated with data of the wrong type.  

################# demonstration of property2 ##############################
from property2 import property2

class unsignedByte(int):
    """ An unsignedByte is a constrained integer with a range of 0-255
    """
    def __init__(self,value):
	if value < 0 or value > 255:
	    raise ValueError, "unsignedByte must be between 0 and 255"
	self = value

class Color(object):
    def __init__(self,b= 0 ):
        # the following assignment does not check the type of "_b".
        # The default value of "0" is not of the proper type for 
        # the instance variable b as defined by the property2 statement.
        self._b = b
        
    __slots__ =  ["_b"]
    def setb(self, value):
        self._b = value
    def getb(self):
        return self._b
    def messItUp(self):
        self._b = 2000

    b = property2(getb,setb,type=unsignedByte)

c = Color()

x = unsignedByte(34)

c.b = x

print "The value of c.b is ",c.b

c.messItUp()
print "A method can accidentally mess up the variable", c.b

c.b = x
c._b = 4000
print "you can override the hidden value as well", c.b

# Proper assignment to the instance variable does run the type check.
# An exception is raised by the following incorrectly typed variable 
# assignment

c.b = 25

In order for the property2 type checking to work well the mechanism needs to 
forbid access to the "hidden" container for the value of the instance 
variable.  

The type checking could also be combined with the new __slots__ mechanism for 
declaring variable names.  In this approach a TypedSlots class defines the 
type constraints for the slot names. The following example  demonstrates the 
use of the TypedSlots class (a subclassed dictionary) to define the types 
allowed for each slot in a class definition. 

class unsignedByte(int):
    """ An unsignedByte is a constrained integer with a range of 0-255
    """
    def __init__(self,value):
	if value < 0 or value > 255:
	    raise ValueError, "unsignedByte must be between 0 and 255"
	self = value

class TypedSlots(dictionary):
    """A TypedSlot object defines an object that is assigned to a
    class.__slot__ definition. The TypedSlot object constrains each slot to
    a specific set of types.  The allowed types are associated with the name
    at the time the slot names are defined.
    """
    def defineSlot(self,name,types):
	"""Add a new slot to the TypedSlots object.  All slot definitions
	can be added as part of the TypedSlots constructor, or they can
	be added one at a time using the definedSlot method
	"""
	self["_" + name] = (name,types)

# build an instance of the TypedSlots object by adding the slots
# one at a time.
	
t = TypedSlots()
t.defineSlot("r",unsignedByte)
t.defineSlot("g",unsignedByte)
t.defineSlot("b",unsignedByte)
t.defineSlot("name",str)

# The following class definition uses the TypedSlot instance example
# The dictionary type can be used in place of a list of
# strings for defining slots because iterating over a dictionary returns 
# a list of strings (the keys to the dictionary).

class Color(object):
    __slots__ = t

    # The bytecodes generated by the following setX and getX methods and
    # assignment of property to X would be automatically generated when
    # an object of type TypedSlots is detected as the type for __slots__.

    def setb(self,value):
	if isinstance(value,self.__slots__["_b"][1]):
	    self._b = value
	else:
	    raise TypeError, "b must be of type unsignedByte"
	
    def getb(self):
	if isinstance(self._b,self.__slots__["_b"][1]):
	    return self._b
	else:
	    raise TypeError, "b must be set to a value of type unsignedByte"
	    
    b = property(getb,setb)

    def setr(self,value):
	if isinstance(value,self.__slots__["_r"][1]):
	    self._r = value
	else:
	    raise TypeError, "r must be of type unsignedByte"
	
    def getr(self):
	if isinstance(self._r,self.__slots__["_r"][1]):
	    return self._r
	else:
	    raise TypeError, "r must be set to a value of type unsignedByte"
	    
    r = property(getr,setr)

    def setg(self,value):
	if isinstance(value,self.__slots__["_g"][1]):
	    self._g = value
	else:
	    raise TypeError, "g must be of type unsignedByte"
	
    def getg(self):
	if isinstance(self._g,self.__slots__["_g"][1]):
	    return self._g
	else:
	    raise TypeError, "g must be set to a value of type unsignedByte"
	    
    g = property(getg,setg)

    def setname(self,value):
	if isinstance(value,self.__slots__["_name"][1]):
	    self._name = value
	else:
	    raise TypeError, "name must be of type str"
	
    def getname(self):
	if isinstance(self._name,self.__slots__["_name"][1]):
	    return self._name
	else:
	    raise TypeError, "name must be set to a value of type str"
	    
    name = property(getname,setname)


c = Color()

c.name = "aqua"
print "The color name is", c.name

x = unsignedByte(254)
c.b= x
print "The value of the blue component is", c.b

# the following statement will fail because the value is not 
# of type unsignedByte
c.g= 255


This implementation also cannot guard the "hidden" names for the slots from 
access.  This creates a security hole in the type constraint system.  This 
hole complicates any attempts to write optimizations that take advantage of 
the typed slots. The proposed change to the __slots__ implementation would 
force type checks to be made on all accesses to the variables defined by 
TypedSlots.  The implementation would automatically generate the bytecode for 
testing the instance type as defined by the TypeSlots definition.

The implementation of the example required the slot to have "hidden" names 
(i.e. the names _r, _b _g, and _name) in order to implement the type checks 
on the slots for the instance variables.  The proposed TypedSlots mechanism 
would eliminate the "hidden" name and calculate the address of the slot 
directly.

The addition of this mechanism to the __slots__ definition would add 
protections against violations of a type constraint system for slots.  The 
mechanism would also reduce the code required to define the constraints.  
Using the TypedSlots definition, the constrained type of the example would be 
written as follows:

class Color(object):
    __slots__ =TypedSlots({ "r":unsignedByte,
                            "g":unsignedByte,
                            "b":unsignedByte,
                            "name":str})

The contents of the slot would only be accessible though the exposed instance 
variable name.  For example:

c = Color()
c.r = unsignedByte(126)

An optional default value could be defined during the creation of the slots 
by making the value a tuple:

class Color(object):
    __slots__ =TypedSlots({ "r":(unsignedByte,unsignedByte(0),
                            "g":(unsignedByte,unsignedByte(0),
                            "b":(unsignedByte,unsignedByte(0),
                            "name":str})

If default values are defined the type test on __get__ could be skipped.