[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.