A Descriptor is a class which provides detailed get, set and
delete control over an attribute of another object. This allows you to
define attributes which are fairly complex objects in their own right.
The idea is that we can use simple attribute references in a program,
but those simple references are actually method functions of a
descriptor object.
This allows us to create programs that look like the following
example.
>>>
oven= Temperature()
>>>
oven.farenheit= 450
>>>
oven.celsius
232.22222222222223
>>>
oven.celsius= 175
>>>
oven.farenheit
347.0
In this example, we set one attribute and the value of another
attribute changes to mirror it precisely.
A common use for descriptors is in an object-oriented database (or
an object-relational mapping). In a database context, getting an
attribute value may require fetching data objects from the file system;
which may involve creating and executing a query in a database.
Descriptor Design Pattern. The Descriptor design pattern has two parts: the
Owner
and the attribute
Descriptor
. The Owner is usually a relatively
complex object that uses one or more Descriptors for it's attributes.
Each Descriptor class defines get, set and delete methods for a
specific attribute of the Owner.
Note that Desciptors can easily be written as reusable, generic
types of attributes. The Owner can have multiple instances of each
Descriptor. Each use of a Desciptor class is a unique instance of a
Descriptor object, bound to an attribute name when the Owner class is
defined.
To be recognized as a Descriptor, a class must implement some
combination of the following three methods.
-
__get__
(
self
,
instance
,
owner
)
-
The
instance
argument is the
self
variable of the owning class; the
owner
argument is the owning class
object. This method of the descriptor must return this attribute's
value. If this descriptor implements a class level variable, the
instance parameter can be ignored.
-
__set__
(
self
,
instance
,
value
)
-
The
instance
argument is the self
variable of the owning class. This method of the descriptor must
set this attribute's value.
-
__delete__
(
self
,
instance
)
-
The
instance
argument is the self
variable of the owning class. This method of the descriptor must
delete this attribute's value.
Sometimes, a descriptor class will also need an
__init__
method function to initialize the
descriptor's internal state. Less commonly, the descriptor may also need
__str__
or __repr__
method functions to display the instance variable correctly.
You must also make a design decision when defining a descriptor.
You must determine where the underlying instance variable is contained.
You have two choices.
-
The Descriptor object has the instance variable.
-
The Owner object contains the instance variable. In this case,
the descriptor class must use the instance
parameter to reference values in the owning object.
Descriptor Example. Here's a simple example of an object with two attributes defined
by descriptors. One descriptor (Celsius
)
contains it's own value. The other desriptor
(Farenheit
), depends on the
Celsius
value, showing how attributes can be
"linked" so that a change to one directly changes the other.
Example 25.1. descriptor.py
class Celsius( object ):
def __init__( self, value=0.0 ):
self.value= float(value)
def __get__( self, instance, owner ):
return self.value
def __set__( self, instance, value ):
self.value= float(value)
class Farenheit( object ):
def __get__( self, instance, owner ):
return instance.celsius * 9 / 5 + 32
def __set__( self, instance, value ):
instance.celsius= (float(value)-32) * 5 / 9
class Temperature( object ):
celsius= Celsius()
farenheit= Farenheit()
|
We've defined a Celsius descriptor.
The Celsius descriptor has an __init__
method which defines the attribute's value. The Celsius
descriptor implements the __get__
method to return the current value of the attribute, and a
__set__ method to change the value of
this attribute.
|
|
The Farenheit descriptor implements
a number of conversions based on the value of the celsius
attribute. The __get__ method converts
the internal value from Celsius to Farenheit. The
__set__ method converts the supplied
value (in Farenheit) to Celsius.
|
|
The owner class, Temperature has
two attributes, both of which are managed by descriptors. One
attribute, celsius , uses an instance of the
Celsius descriptor. The other attribute,
farenheit , uses an instance of the
Fareheit descriptor. When we use one of
these attributes in an assignment statement, the descriptor's
__set__ method is used. When we use one
of these attributes in an expression, the descriptor's
__get__ method is used. We didn't show
a __delete__ method; this would be used
when the attribute is used in a
del
statement.
|
Let's look at what happens when we set an attribute value, for
example, using oven.farenheit= 450
. In this case, the
farenheit
attribute is a Descriptor with a
__set__
method. This
__set__
method is evaluated with
instance
set to the object which is being modified
(the oven
variable) and owner set to the
Temperature
class. The
__set__
method computes the celsius value, and
provides that to the celsius attribute of the instance. The Celsius
descriptor simply saves the value.
When we get an attribute value, for example, using
oven.celsius
, the following happens. Since
celsius
is a Descriptor with a
__get__
method, this method is evaluated, and
returns the celsius temperature.