smarie / python-pyfields

Define fields in python classes. Easily.
https://smarie.github.io/python-pyfields
BSD 3-Clause "New" or "Revised" License
45 stars 10 forks source link

Weird behavior when calling super().__init__ #53

Closed devashishshankar closed 4 years ago

devashishshankar commented 4 years ago

Let's say I want to call superclass init in my child class:

from pyfields import field, init_fields, get_field

class A:
    a = field(str, check_type=True)

    @init_fields()
    def __init__(self):
        pass

class B(A):
    b = field(str, check_type=True)

    @init_fields()
    def __init__(self):
        super(B, self).__init__(a=self.a)
        pass

print(B('a', 'b'))

This fails with the following Error:

    super(B, self).__init__(a=self.a)
TypeError: __init__() missing 1 required positional argument: 'b'

If I try the following:

from pyfields import field, init_fields, get_field

class A:
    a = field(str, check_type=True)

    @init_fields(a)
    def __init__(self):
        pass

class B(A):
    b = field(str, check_type=True)

    @init_fields()
    def __init__(self):
        super(B, self).__init__(a=self.a)
        pass

print("Init before calling B", B.__init__)
B(a='22', b='33')
print("Init after calling B", B.__init__)

This leads to the following:

Init before calling B <function B.__init__ at 0x108951cb0>
Init after calling B <function A.__init__ at 0x108951f80>

This is very dangerous, and was leading to a weird error in my code. Took me a while to figure out this was the cause.

I think the superclass constructor is being overriden by the child class. setattr(objtype, '__init__', new_init) in init_makers.py

Is calling superclass constructor not supported?

I am using version 1.0.2

smarie commented 4 years ago

wow, very strange indeed ! This should definitely not happen. It is probably related to the fact that the __init__ descriptor is not called in an intuitive way by super . I'll have a look, thanks a lot for reporting !

smarie commented 4 years ago

For some reason that seems to be a python bug, super(B, self).__init__(a) is not equivalent to A.__init__(self, a) when the __init__ method is a method descriptor: InitDescriptor.__get__(obj, objtype) is called but when it is called through super, objtype is not replaced with A.

I'll have to find a workaround, that should not be too difficult. In the meantime, you can use A.__init__(self, a=self.a) as a constructor, it works.

smarie commented 4 years ago

I found a workaround but for python 3.6+ (where __setname__ callback is called when the init descriptor is attached to the class). Unfortunately for earlier versions I still have no way to determine which class owns the init method to be created. I really thought that objtype in InitDescriptor.__get__(obj, objtype) was reliable...

devashishshankar commented 4 years ago

Thanks, using A.__init__(self, a=self.a) works for me.

smarie commented 4 years ago

I finally found a "clever" way to solve this :)

It will ship in 1.0.3 in a few minutes when travis has completed