pythonclassvariablesinheritance

Modifying class variable before __new__


I'd like to modify the class variable bar of a class B so that the creation of the class A object will be performed with bar2 instead of bar1 (where bar1 is the default). This makes it easier for the user to select if they want to create class B with parameter bar1 or bar2 when using the class. I'm not opposed to other ways (eg. method) to modify it. I just want the user to be able to do it before the class is instantiated.

Class A:

class A(object):
   def __new__(cls, foo):
        obj = super(A, cls).__new__(cls)
        return obj

I don't have access to Class A. Class A is coming from a third party package that doesn't have an __init__ but it takes foo as an argument of the __new__ function.

Class B:

class B(A):

    bar = "bar1"

    def __new__(cls, foo=bar):
        print("actual",foo)
        rm = super().__new__(cls,foo=foo)
        return rm

Class B is a derivative class that inherits from class A. Since I want to modify the foo parameter in class A, I need to overwrite the __new__ class.

One other piece of the puzzle is that on top of B there is a long stack of child classes that inherit from B and I don't want to modify all of their init to pass along the foo variable until class A.

I want to run the following code (because it's easy from the user perspective to choose the value of 'bar' and because the modifications to the code are very little.

if __name__ == '__main__':

    B.bar = 'bar2'
    b = B()
    print(b.bar)

I get:

actual bar1
bar2

whereas I'm expecting:

actual bar2
bar2

I modified my example to make it work but it's not pretty and I'm seeking for a more pythonic solution. This uses bar class variable as an array and uses class methods to modify it

class A(object):

    def __new__(cls, foo):
        obj = super(A, cls).__new__(cls)
        return obj

class B(A):

    _bar_array = ["bar1"]

    @classmethod
    def set_bar(cls, value):
        cls._bar_array[0] = value

    @classmethod
    def get_bar(cls):
        return cls._bar_array[0]

    def __new__(cls, foo=_bar_array):
        print("actual",foo[0])
        rm = super().__new__(cls,foo=foo[0])
        return rm

if __name__ == '__main__':

    B.set_bar("bar2")
    b = B()
    print(b.get_bar())
    print(B.get_bar())

Solution

  • As I explained in the comments, default arguments only get evaluated when they are defined not on each invocation. So just use the normal python idiom (often relied upon to make mutable default arguments work the way most people expect them to work) of setting the default value to None (or an appropriate sentinel if None has some other semantics) and then using the expression you were hoping to get re-evaluated within the body of the function if the default value is None:

    class A(object):
       def __new__(cls, foo):
           print('foo seen by bar', foo)
           return super(A, cls).__new__(cls)
    
    class B(A):
        bar = "bar1"
        def __new__(cls, foo=None):
            if foo is None:
                foo = cls.bar
            return super().__new__(cls, foo=foo)
    
    b = B() # foo seen by A: bar1
    B.bar = 'bar2'
    b = B() # foo seen by A: bar2
    

    If B.bar = None is supposed to have some other meaning, then you just use another sentinel (an arbitrary object works!)

    _NO_FOO_PROVIDED = object()
    
    class B(A):
        bar = "bar1"
        def __new__(cls, foo=_NO_FOO_PROVIDED):
            if foo is _NO_FOO_PROVIDED:
                foo = cls.bar
            return super().__new__(cls, foo=foo)
    
    B()
    B.bar = None
    B()