Conflicting needs for __init__ method

Steven D'Aprano steve at REMOVEME.cybersource.com.au
Sun Jan 14 22:43:55 EST 2007


On Sun, 14 Jan 2007 15:32:35 -0800, dickinsm wrote:

> Suppose you're writing a class "Rational" for rational numbers.  The
> __init__ function of such a class has two quite different roles to
> play.  First, it's supposed to allow users of the class to create
> Rational instances; in this role, __init__ is quite a complex beast.
> It needs to allow arguments of various types---a pair of integers, a
> single integer, another Rational instance, and perhaps floats, Decimal
> instances, and suitably formatted strings.  It has to validate the
> input and/or make sure that suitable exceptions are raised on invalid
> input.  And when initializing from a pair of integers---a numerator
> and denominator---it makes sense to normalize: divide both the
> numerator and denominator by their greatest common divisor and make
> sure that the denominator is positive.
> 
> But __init__ also plays another role: it's going to be used by the
> other Rational arithmetic methods, like __add__ and __mul__, to return
> new Rational instances.  For this use, there's essentially no need for
> any of the above complications: it's easy and natural to arrange that
> the input to __init__ is always a valid, normalized pair of integers.
> (You could include the normalization in __init__, but that's wasteful

Is it really? Have you measured it or are you guessing? Is it more or less
wasteful than any other solution?

> when gcd computations are relatively expensive and some operations,
> like negation or raising to a positive integer power, aren't going to
> require it.)  So for this use __init__ can be as simple as:
> 
> def __init__(self, numerator, denominator):
>     self.numerator = numerator
>     self.denominator = denominator
> 
> So the question is: (how) do people reconcile these two quite
> different needs in one function?  I have two possible solutions, but
> neither seems particularly satisfactory, and I wonder whether I'm
> missing an obvious third way.  The first solution is to add an
> optional keyword argument "internal = False" to the __init__ routine,
> and have all internal uses specify "internal = True"; then the
> __init__ function can do the all the complicated stuff when internal
> is False, and just the quick initialization otherwise.  But this seems
> rather messy.

Worse than messy. I guarantee you that your class' users will,
deliberately or accidentally, end up calling Rational(10,30,internal=True)
and you'll spent time debugging mysterious cases of instances not being
normalised when they should be.


> The other solution is to ask the users of the class not to use
> Rational() to instantiate, but to use some other function
> (createRational(), say) instead.

That's ugly! And they won't listen.

> Of course, none of this really has anything to do with rational
> numbers.  There must be many examples of classes for which internal
> calls to __init__, from other methods of the same class, require
> minimal argument processing, while external calls require heavier and
> possibly computationally expensive processing.  What's the usual way
> to solve this sort of problem?

class Rational(object):
    def __init__(self, numerator, denominator):
        print "lots of heavy processing here..."
        # processing ints, floats, strings, special case arguments, 
        # blah blah blah...
        self.numerator = numerator
        self.denominator = denominator
    def __copy__(self):
        cls = self.__class__
        obj = cls.__new__(cls)
        obj.numerator = self.numerator
        obj.denominator = self.denominator
        return obj
    def __neg__(self):
        obj = self.__copy__()
        obj.numerator *= -1
        return obj
        
I use __copy__ rather than copy for the method name, so that the copy
module will do the right thing.



-- 
Steven D'Aprano 




More information about the Python-list mailing list