3

I have a situation where I'm using @classmethod to create a constructor for a class. Within this constructor, a function gets called, which then in turn calls another function. But either this doesn't work or (more probably) I'm doing something to make it not work. Here's an example in miniature:

class testclass:
    def __init__(self, x):
        self.x = x

    @classmethod
    def constructor(cls, x):
        adj_x = cls.outer_adjust(cls, x)
        return testclass(adj_x)

    def outer_adjust(self, x):
        return self.inner_adjust(x)

    def inner_adjust(self, x):
        return x + 1

test_instance = testclass.constructor(4)

This produces an error message:

inner_adjust() missing 1 required positional argument: 'x'

I can make it work by explicitly passing self to inner_adjust, eg

def outer_adjust(self, x):
    return self.inner_adjust(self, x)

But this then means that the outer_adjust method can't be used outside of the constructor, which is not what I want.

Any assistance gratefully received.

Here's a more detailed example, with two constructors shown. I'm trying to follow the approach to constructors described in What is a clean, pythonic way to have multiple constructors in Python? Which is essentially that the constructors do some processing to figure out what variables they should pass to init when instantiating the class. Both constructors give the same error:

if_char_is_z_make_it_a() missing 1 required positional argument: 'char_input'

As before, I need to be able to use the if_char_is_make_it_a function outside of the constructor (ie, when using the class normally).

class testclass:
    def __init__(self, char):
        self.char = char

    @classmethod
    def constructor_from_int(cls, int_input):
        as_char = chr(int_input)
        char = cls.process_char(cls, as_char)
        return testclass(char)

    @classmethod
    def constructor_from_char(cls, char_input):
        char = cls.process_char(cls, char_input)
        return testclass(char)

    def process_char(self, char_input):
        processed_char = '(' + char_input + ')'
        output_char = self.if_char_is_z_make_it_a(processed_char)
        return output_char

    def if_char_is_z_make_it_a(self, char_input):
        if char_input == '(z)':
            return '(a)'
        return char_input

test_instance = testclass.constructor_from_char('a')
Chris Harris
  • 177
  • 3
  • 9

3 Answers3

2

When you call cls.outer_adjust from constructor you are calling the unbound outer_adjust method.

Thus, you pass the class itself as self and not an instance to a method that expects to receive an instance as argument.

Although, there is no real reason to have a constructor method. This is exactly what __init__ is for.

class testclass:
    def __init__(self, x):
        self.x = self.outer_adjust(x)

    def outer_adjust(self, x):
        return self.inner_adjust(x)

    def inner_adjust(self, x):
        return x + 1

test_instance = testclass(4)

If you absolutely need the transformation on x to be done before the instantiation, then use __new__ instead. Although, this is generally not necessary.

Multiple constructors

If for some reason you still need to have a constructor method, by example if you want multiple constructors. Then keep in mind that outer_adjust and inner_adjust are instance methods, this means they must be called after you have created an instance.

class testclass:
    def __init__(self, x):
        self.x = x

    @classmethod
    def constructor1(cls, x):
        instance = cls(x)
        instance.outer_adjust()
        return instance

    @classmethod
    def constructor2(cls, x):
        instance = cls(x)
        instance.inner_adjust()
        return instance

    def outer_adjust(self):
        print('Do something else')
        return self.inner_adjust()

    def inner_adjust(self):
        self.x += 1

As a sidenote, notice how I did not need to call testclass, but simply called cls in the constructor methods. Since this is a class method, we do not need to explicitly name the class. This is better, especially if you are to use inheritance.

Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
  • Thanks - I do understand what you're saying but this is a class (maybe not obvious from my shortened example) that has multiple constructors, so I can't just use __init__. – Chris Harris Aug 23 '18 at 14:56
  • @ChrisHarris You might need to share more details. What you describe looks a bit like code smell to me and you might want to consider a better way to do it, by example using inheritance. – Olivier Melançon Aug 23 '18 at 14:58
  • thanks for the detailed response. I'm trying to use the approach to using constructors described here: https://stackoverflow.com/questions/682504/what-is-a-clean-pythonic-way-to-have-multiple-constructors-in-python Where the constructor function does most of the work to figure out what variables should ultimately be passed when instantiating the class via __init__. I'll edit my original post to include a more detailed example which hopefully makes it a bit clearer. – Chris Harris Aug 24 '18 at 08:22
1

Basically what you are doing here shall be done via the __new__ which serve as constructor.

class testclass:
    def __init__(self, x):
        self.x = x

    def __new__(cls, *args, **kwargs):
        instance = super(testclass, cls).__new__(cls, *args, **kwargs)
        instance.outer_adjust(args[0])
        return instance

    def outer_adjust(self, x):
        return self.inner_adjust(x)

    def inner_adjust(self, x):
        self.x = x + 1

test_instance = testclass(4)
Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
Andriy Ivaneyko
  • 20,639
  • 6
  • 60
  • 82
  • I'm finding that code doesn't work on my setup. I'd also like to stick to the usual approach when using constructors of figuring out the variables within the @classmethod function, then creating an instance based on __init__ using them. – Chris Harris Aug 23 '18 at 14:59
  • Also, I should point out that in your example `inner_adjust` simply returns the value and `adj_x` is thrown away, so `__new__` does nothing here. – Olivier Melançon Aug 23 '18 at 15:05
  • @OlivierMelançon yes, you are right, but maybe it's some placeholders... – Andriy Ivaneyko Aug 23 '18 at 15:08
  • @OlivierMelançon yeah, thanks, honestly didn't catch from the begining your thought, no need to set `adj_x` variable – Andriy Ivaneyko Aug 23 '18 at 15:11
0

You are abusing self. The point of the class method is to use the cls argument as constructor, instead of explicitly naming the class by testclass(adj_x). Also, during the cls.outer_adjust(cls, x) call, you are passing the class instead of the instance, which happens to work because you are not using any instance attributes.

As to your questions, there's no way to avoid the x argument. inner_adjust increases some value by 1, so you must give it something to increase. The idea would be to have

def constructor(cls, x):
    return cls(x)

def inner_adjust(self):
    return self.x += 1

and then do something like

object= testclass.constructor(12)
object.inner_adjust()
blue_note
  • 27,712
  • 9
  • 72
  • 90
  • Thanks for the response. I see what you're saying about using cls rather than naming the class to create the instance. However I don't think the method you're describing is going to work in this case, because I need to use the inner_adjust function elsewhere in the code, where I may want to pass it parameters other than self.x. – Chris Harris Aug 24 '18 at 09:45
  • @ChrisHarris: 1) that's the point of classmethod. If you use the name the class, it will fail with inheritance, or if the name changes. 2) if you are not using `self` attributes, but only parameters, maybe it should be a `@staticmethod` – blue_note Aug 24 '18 at 16:22