1

(Python 3.7)

pint / Q_ behavior

I want to subclass pint's quantity class and override __init__. The standard syntax fails because apparently the arguments bubble up to the object init method (which takes no argument):

# file: test.py
from pint import UnitRegistry

ureg = UnitRegistry()
Q_ = ureg.Quantity

class Q_Child_(Q_):
    def __init__(self, *args, **kwargs):
        super(Q_, self).__init__(*args, **kwargs)
        self.foo = 'foo'

a=Q_Child_(1.0, 's') # Raises an error:
# Traceback(most recent call last):
#   File ".../test.py", line 12, in < module >
#   a = Q_Child_(1.0, 's')
#   File ".../test.py", line 9, in __init__
#   super(Q_, self).__init__(*args, **kwargs)
# TypeError: object.__init__() takes exactly one argument(the instance to initialize)

What seems to work is to not call super/init at all in the subclassing, but how does this even make sense?

from pint import UnitRegistry

ureg = UnitRegistry()
Q_ = ureg.Quantity

class Q_Child_(Q_):
    def __init__(self, *args, **kwargs):
        # not calling super or Q_.__init__ (or so it seems)
        self.foo = 'foo'

a=Q_Child_(1.0, 's')
print(a)  # '1.0 second'  -> how does it even know the passed arguments?
print(a.foo)  # 'foo'

I believe the latter is what I want to use, but using code that works when you think it should not work seems a recipe for disaster. What is happening?

I guess it is due to Q_ being generated dynamically (when ureg is instantiated, see https://github.com/hgrecco/pint/blob/master/pint/registry.py#L115 and https://github.com/hgrecco/pint/blob/master/pint/quantity.py#L1741) but fail to see why it should matter.

That question is not a duplicate of correct way to use super (argument passing) (or similar) because the standard way does not work here:

class NewDict(dict):
    def __init__(self, *args, foo='foo', **kwargs):
        super(NewDict, self).__init__(*args, **kwargs)
        self.foo = foo

nd = NewDict({'a': 1}, foo='bar')
print(nd)  # {'a': 1}
print(nd.foo)  # 'bar'

Without the super call, the parent class has no way to know the arguments that it is supposed to initialize with:

class NewDictWithoutSuper(dict):
    def __init__(self, *args, foo='foo', **kwargs):
        self.foo = foo

nd = NewDictWithoutSuper({'a': 1}, foo='bar')
print(nd)  # {} i.e. the default dict()
print(nd.foo)  # 'bar'
Leporello
  • 638
  • 4
  • 12
  • Unfortunately, you just have to *know* that `super(Q_, self).__init__` is going to resolve to `object.__init__`, and therefore take no arguments. `Q_` should document that "its" `__init__` method takes no argument, so that any subclass that *does* define arguments for `__init__` knows that it should not pass them on. – chepner Aug 09 '19 at 12:23
  • 1
    This is true of *any* method in a properly designed class hierarchy that intends to use `super`: the class that introduces a method is responsible for *not* calling `super`, because none of its ancestors will have defined that method. A corollary to this is that changing the signature of a method you are overriding is basically the same as introducing a new method: you are responsible for ensuring that you don't pass your extra arguments on to `super`. – chepner Aug 09 '19 at 12:26
  • @chepner OK, looking at the code source again, I see that it does not define any `__init__` anywhere. ([Quantity inherits from _ Quantity](https://github.com/hgrecco/pint/blob/master/pint/quantity.py), which inherits from two object-inherited classes in [pint.util](https://github.com/hgrecco/pint/blob/master/pint/util.py), and none of the four define `__init__`). But then, what _does_ happen to the arguments of, say, `Q_(1.0, 's')`, if not parsed by an `__init__` method somewhere? (Knowing that would be necessary to proper subclassing.) – Leporello Aug 09 '19 at 12:32
  • Ah, hold on. I'll write a proper answer now, since I think I've gotten to the root of the problem. – chepner Aug 09 '19 at 12:48
  • Or maybe not. This has uncovered a slight misunderstanding on my part of exactly how and when `__init__` gets called. Suffice to say, `_Quantity.__new__` is handling the arguments itself, and it appears that your definition of `Q_Child_` is triggering an implicit call to `__init__` that doesn't happen otherwise. – chepner Aug 09 '19 at 13:14

1 Answers1

0

This is an old question, but I had the same issue right now. As pointed out in the comments to the question, the Quantity class utilizes the __new__ operator (here is a nice description how it works).

Leporello's example without calling __init__ works for instantiation with value and unit, but not when asking to parse the value as a string:

from pint import UnitRegistry

reg = UnitRegistry()
_Q = reg.Quantity
  
class Quantity(_Q):
    pass

print(Quantity(1, "K"))  # -> 1 kelvin :-)
print(Quantity("1 K"))  # -> 1 kelvin dimensionless :-(

They key is to call the __new__ operator on the correct base-class. But as user2357112 pointed out in the comments, this actually creates only an object of type _Q. Desperately, I then changed the class manually.

class Quantity(_Q):
    def __new__(cls, *args, **kwargs):
        obj = _Q.__new__(_Q, *args, **kwargs)
        obj.__class__ = cls
        return obj

print(Quantity(1, "K"))  # -> 1 kelvin :-)
print(Quantity("1 K"))  # -> 1 kelvin :-)
print(isinstance(a, Quantity))  # -> True :-)

The constructor isn't used at all.

While I hereby answered the question also for myself, the negative side of it is that this is a hack and not part of the documented API of the pint library, thus the code might break in the future.

Dr. V
  • 1,747
  • 1
  • 11
  • 14
  • Your code doesn't actually work, though - it's creating instances of `_Q`, not instances of your class. Try checking `isinstance(Quantity("1 K"), Quantity)`. – user2357112 Nov 12 '22 at 23:35
  • Ok, it became even more hacky, but I could assign the `__class__` attribute manually. Not sure if I'll use this solution in my own code though. It looks fragile. – Dr. V Nov 12 '22 at 23:57