Building Class Interfaces at Run Time

Một phần của tài liệu Python scripting for computational science (Trang 420 - 424)

Python is a very dynamic language and makes it possible for a class interface to be defined in terms of executable code. This allows for customization of the interface at run time or generation of comprehensive interfaces by compact code.

Generation of Properties in Class Methods. In Chapter 8.6.11 we discussed so-called properties versus traditional set and get functions for manipulat- ing variables in a class interface. Suppose we have a collection of “private”

variables with their names prefixed by an underscore. The set/get approach, which is particularly widespread among Java programmers, consists of mak- ing a pair of set and get functions for accessing and manipulating the private variables. Omitting the set function makes the variable read-only (although a Python programmer can access the private variable anyway). As an al- ternative to set and get functions, Python offers access to an attribute via hidden set and get functions. This feature enables complete control of what assignment to and from a class attribute implies.

It is attractive to drop the set/get approach in Python programming and access attributes either directly or through properties. Attributes that are not meant to be manipulated outside the class are made read-only by omitting the set function when defining the property.

However, properties seemingly still require the programmer to code all the get and set functions and define these in property statements. This is quite some work. Fortunately, the process can be automated, and the properties can be defined in parameterized code.

For some private variable self._x we would like to access self.x as a read-only attribute. This can be compactly accomplished by aproperty call utilizing alambdaconstruction (see page 116) for convenient and fast defini- tion of the get function:

A.x = property(fget=lambda self: self._x)

Here, A is the class name, and the get function will be called with self as first parameter so we need one argument in the lambda definition. (For quick property construction we could use a lambda function for the fset parameter too: lambda self, v: setattr(self, ’_x’, v), but then the set and get function do nothing but work with self._x so there is actually no gain in having a property compared to a straight attributeself.x).

The previous construction makes it easy to customize a class interface.

For example, when we use a NumPy array to represent points in space, it could be convenient to have read-only attributesx,y, andzfor the coordinate values of the point. For 2D points,zis omitted, and for points in one space dimensions, bothyandzare omitted. To create such an object, we introduce a classPointwith a special constructor that actually returns a NumPy array extended with extra properties. The__init__method must create objects of

the same type as the class type, but in new-style classes one can use__new__

as constructor, and this method can return objects of any type. A straight function returning the right manipulated object could equally well be used.

We create a NumPy array and add as many properties as there are space dimensions of the point. The point itself is a tuple or list given as argument to the constructor.

from numpy import * class Point(object):

"""

1D, 2D or 3D Point object implemented as a NumPy array with properties.

"""

def __new__(self, point):

a = array(point)

# define read-only attributes x, y, and z:

if len(point) >= 1:

ndarray.x = property(fget=lambda self: self[0])

# or a.__class__.x = property(fget=lambda self: self[0]) if len(point) >= 2:

ndarray.y = property(fget=lambda self: self[1]) if len(point) == 3:

ndarray.z = property(fget=lambda self: self[2]) return a

Note that the properties are class methods called with the instance object (“self”) as first argument. The read-only function simply applies the sub- scription operator on this argument. It is sufficient to add the properties once, but here we repeat the definition in every instantiation ofPoint instances13.

With classPointwe can run the following type of code:

>>> p1 = Point((0,1))

>>> p2 = Point((1,2))

>>> p3 = p1 + p2 # NumPy computations work

>>> p3 [ 1. 3.]

>>> type(p3)

<class ’numpy.ndarray’>

>>> p3.x, p3.y (1.0, 3.0)

>>> p3.z # should raise an exception Traceback (most recent call last):

...

AttributeError: ’numpy.ndarray’ object has no attribute ’z’

This interactive session demonstrates that we can tailor a class interface at run time and also do this with an existing class without altering its source code.

13 Note that if we make a 3D point and then compute with 2D points, thezproperty is defined so accessingp.zfor a 2D pointpis legal, but the get function performs look up beyond the range of the array.

8.6. Classes 401 Automatic Generation of Properties. Suppose we have a (long) list of pri- vate variable names and want these to have associated read-only attributes.

By parameterizing the code segment above we can define all the necessary properties in three lines:

for v in variables:

exec ’%s.%s = property(fget=lambda self: self._%s’ % \ (self.__class__.__name__, v, v)

An example of thevariablesmight be (’counter’, ’nx, ’x’, ’help’, ’coor’)

resulting in properties of the same name and attributes with an underscore prefix. The above code can conveniently be placed in a function being called from the constructor such that every instance gets the collection of properties.

Extending a Class with New Methods. The recipes 5.5, 5.8, 5.12, and 5.13 in the “Python Cookbook” [23] provides more information about dynamic extensions of classes and coding of properties. In particular, we mention the technique from recipe 5.12 about how to add new methods to an instance (see also page 394):

def func_to_method(func, class_, method_name=None):

setattr(class_, method_name or func.__name__, func)

Thefuncobject must be a stand-alone Python function with a class instance as first argument, by convention called self. Here is a very simple demon- stration of the functionality:

>>> class A:

pass

>>> def hw(self, r, file=sys.stdout):

file.write(’Hi! sin(%g)=%g’ % (r, math.sin(r)))

>>> func_to_method(hw, A) # add hw as method in class A

>>> a = A()

>>> dir(a)

[’__doc__’, ’__module__’, ’hw’]

>>> a.hw(1.2)

’Hi! sin(1.2)=0.932039’

Another way of extending classAwith a new methodhwis to implement hwin a subclass of A. Sometimes this is inconvenient, however, because users need to be a aware of a new class name. The following trick is then useful.

Suppose classAresides in the module fileA.py. We can then in a new module file importAunder another name and reserve the nameAfor a subclass where hwis implemented:

from A import A as A_old # import class A from module file A.py class A(A_old):

def hw(self, r, file=sys.stdout):

file.write(’Hi! sin(%g)=%g’ % (r, math.sin(r)))

For users it looks like classAhas been extended, but the code in fileA.pywas not changed. This technique can therefore be a nice way to extend libraries without changing the name of classes and without touching the original li- brary files.

Inspecting the Class Interface. Python has the function dir for listing the available variables and functions in an object. This is useful for looking up the contents of modules and class instances. In particular, thedirfunction is handy when class interfaces are built dynamically at run time. Instances have some standard attributes and special methods, recognized by a double leading and trailing underscore, which we might remove from the “table of contents”

produced by thedirfunction. The functiondumpin thescitools.miscmodule removes these items as well as non-public entries (starting with an under- score), writes all variables or attributes with values, and lists all functions or methods on a line:

>>> from scitools.misc import dump

>>> dump(p3) array([ 1., 3.]) flat=[ 1. 3.]

rank=1

real=[ 1. 3.]

shape=(2,) x=1.0 y=3.0

argmax, argmin, argsort, astype, byteswap, copy, diagonal, factory, fromlist, getflat, getrank, getreal, getshape, info, is_c_array, is_f_array, is_fortran_contiguous,

isaligned, isbyteswapped, iscontiguous, itemsize, nelements, new, nonzero, put, ravel, repeat, resize, setflat, setreal, setshape, sort, swapaxes, take, tofile, togglebyteorder, tolist, tostring, trace, transpose, type, typecode, view Thisdumpfunction is also useful for inspecting modules.

A special module inspect allows you to extract various properties of a variable. For example, you can test if the variable refers to a module, class, function, or method; you can extract arguments from function or method objects; and you can look at the source code where the object is defined:

>>> import inspect

>>> f = os.path.walk

>>> inspect.isfunction(f) True

>>> print inspect.getsource(f) # print the source code def walk(top, func, arg):

"""Directory tree walk with callback function.

...

>>> inspect.getargspec(f) # print arguments ([’top’, ’func’, ’arg’], None, None, None)

>>> print inspect.getdoc(f) # print doc string Directory tree walk with callback function.

...

8.6. Classes 403

Một phần của tài liệu Python scripting for computational science (Trang 420 - 424)

Tải bản đầy đủ (PDF)

(769 trang)