views:

71

answers:

1

This week on comp.lang.python, an "interesting" piece of code was posted by Steven D'Aprano as a joke answer to an homework question. Here it is:

class MultiplierFactory(object):
    def __init__(self, factor=1):
        self.__factor = factor
    @property
    def factor(self):
        return getattr(self, '_%s__factor' % self.__class__.__name__)
    def __call__(self, factor=None):
        if not factor is not None is True:
            factor = self.factor
        class Multiplier(object):
            def __init__(self, factor=None):
                self.__factor = factor
            @property
            def factor(self):
                return getattr(self,
                '_%s__factor' % self.__class__.__name__)
            def __call__(self, n):
                return self.factor*n
        Multiplier.__init__.im_func.func_defaults = (factor,)
        return Multiplier(factor)

twice = MultiplierFactory(2)()

We know that twice is an equivalent to the answer:

def twice(x):
    return 2*x

From the names Multiplier and MultiplierFactory we get an idea of what's the code doing, but we're not sure of the exact internals. Let's simplify it first.

Logic

if not factor is not None is True:
    factor = self.factor

not factor is not None is True is equivalent to not factor is not None, which is also factor is None. Result:

if factor is None:
    factor = self.factor

Until now, that was easy :)

Attribute access

Another interesting point is the curious factor accessor.

def factor(self):
    return getattr(self, '_%s__factor' % self.__class__.__name__)

During initialization of MultiplierFactory, self.__factor is set. But later on, the code accesses self.factor.

It then seems that:

getattr(self, '_%s__factor' % self.__class__.__name__)

Is doing exactly "self.__factor".

Can we always access attributes in this fashion?

def mygetattr(self, attr):
    return getattr(self, '_%s%s' % (self.__class__.__name__, attr))

Dynamically changing function signatures

Anyway, at this point, here is the simplified code:

class MultiplierFactory(object):
    def __init__(self, factor=1):
        self.factor = factor
    def __call__(self, factor=None):
        if factor is None:
            factor = self.factor
        class Multiplier(object):
            def __init__(self, factor=None):
                self.factor = factor
            def __call__(self, n):
                return self.factor*n
        Multiplier.__init__.im_func.func_defaults = (factor,)
        return Multiplier(factor)

twice = MultiplierFactory(2)()

Code is almost clean now. The only puzzling line, maybe, would be:

Multiplier.__init__.im_func.func_defaults = (factor,)

What's in there? I looked at the datamodel doc, and found that func_defaults was "A tuple containing default argument values for those arguments that have defaults, or None if no arguments have a default value". Are we just changing the default value for factor argument in __init__ here? Resulting code would then be:

class MultiplierFactory(object):
    def __init__(self, factor=1):
        self.factor = factor
    def __call__(self, factor=None):
        if factor is None:
            factor = self.factor
        class Multiplier(object):
            def __init__(self, innerfactor=factor):
                self.factor = innerfactor
            def __call__(self, n):
                return self.factor*n
        return Multiplier(factor)

twice = MultiplierFactory(2)()

Which means that dynamically setting the default value was just useless noise, since Multiplier is never called without a default parameter, right?

And we could probably simplify it to:

class MultiplierFactory(object):
    def __init__(self, factor=1):
        self.factor = factor
    def __call__(self, factor=None):
        if factor is None:
            factor = self.factor
        def my_multiplier(n):
            return factor*n
        return my_multiplier

twice = MultiplierFactory(2)() # similar to MultiplierFactory()(2)

Correct?

And for those hurrying to "this is not a real question"... read again, my questions are in bold+italic

+1  A: 

Q1. Can we always access attributes in this fashion?

A: No. It's only those attributes who start with double underscores. They get obfuscated in that way, to prevent accidental access/overriding from outside the class.

Q2: Are we just changing the default value for factor argument in __init__ here?

A: Yes.

Q2: right?

Right.

Lennart Regebro