Saturday, 8 January 2011

Forward references

Forward references are sometimes necessary and in interpreted languages like Python almost never a problem. In class definitions however, it is. In this article we examine a solution for this problem with the use of the built in globals() function.

Consider the following use case: we have two classes A and B and we want the __init__() method of both classes to check that any arguments passed to it are instances of the other class, i.e. the A() constructor will only accept B instances as argument while the B() constructor only accepts A instances.

This isn't a problem per se because Python is an interpreted language. Is this context that means that a reference to any class name in for example the __init__() method will be resolved at run time. The __init__() method of the first class to be defined can therefor use an expression like issubclass(argument.__class__,B) without a problem, even if B is not yet defined.

But what if we want to parameterize this approach? Say we want to store the class that the arguments have to check against in a class variable. Such an assignment is executed while the class is being defined and therefor a forward reference (a reference to a class that is defined later in the file) will raise an exception. The following snippet of code illustrates the problem.

class A:
    argumentclass = B

    def __init__(self,*args):
        for a in args:
            if not issubclass(a.__class__,self.argumentclass):
                raise TypeError('argument not a subclass of' ,
                   self.argumentclass,a)
        self.refs = args

The code in line 2 will raise a NameError exception. There is a solution however: if we store the name of the class we can easily check if an argument is a sub class of a class with that stored name if we have a means of retrieving a class object by name. Fortunately that is quite simple by way of the globals() function. This built in function will return a dictionary of all objects in the current globals scope. By the time the __init__() method is executed this will hold both the A and the B class. The adapted code may look something like this:

class A:
    argumentclass = 'B' 
    def __init__(self,*args):
        for a in args:
            if not issubclass(a.__class__,globals()[self.argumentclass]):
                raise TypeError('argument not a subclass of' ,self.argumentclass,a)
        self.refs = args

This approach even allows for easy subclassing:

class X:
    argumentclass = None 
    def __init__(self,*args):
        for a in args:
            if not issubclass(a.__class__,globals()[self.argumentclass]):
                raise TypeError('argument not a subclass of' ,self.argumentclass,a)
        self.refs = args

class A(X):
    argumentclass = 'B'

class B(X):
    argumentclass = 'A' 

No comments:

Post a Comment