Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Set the value of an argument in a class who inherits from int or float or str

An error is raised when I try to set the value of an argument of a class who inherits from str. The error occurs only when I try to access the argument with myClass(arg = 'test'). The error is :

TypeError: 'arg' is an invalid keyword argument for this function

The issue is shown in this exemple :

class A(str):    
    def __init__(self, arg):
        pass

A("test") # Works well
A(arg = "test") # Fails

Only the last line raises the error. The previous line works well. The problem is the same with classes that inherit from int or float.

UPDATE (solution) :

I found a solution with these links :

  • Adding optional parameters to the constructors of multiply-inheriting subclasses of built-in types?
  • inheritance from str or int

The solution is :

class A(str):

    def __new__(cls, *args, **kwargs):
        return str.__new__(cls)

    def __init__(self, arg01):
        print(arg01)

A(arg01= "test")

I don't know exactly why that works and I will investigate on it. If someone has got a clear explanation, I'm interested and I thank him in advance.

UPDATE (my explanation) :

I'm not sure at all of what I will say but that is what I understood.

Imagine a class 'myClass' without any heritance. When I do this myInstance = myClass(), here is what happens :

The method myClass.__new__ is executed. This method will create the object myInstance. __new__ is the real constructor (__init__ isn't a constructor !). In pseudocode, __new__ look something like this :

def __new__ (cls, *args, **kwargs):

    myInstance = # Do some stuff to create myInstance, an object of the type passed as argument (cls).
    # Add the method __init__ to myInstance.
    # Call __init__ and pass to it the list 'args' and the dictionary 'kwargs' (both come from arguments of __new__). Pass to it also the object itself (self) : 
    obj.__init__(self, args, kwargs) :
        # Do some stuff.

The situation is a little different when we are using immutable types (str, int, float, tuple). In the previous pseudocode, I wrote def __new__(cls, *args, **kwargs). With immutable types, the pseudocode for the method __new__ is more like this def __new__(cls, anUniqueValue). I don't realy understand why the behavior of immutableTypes.__new__ is deferent of others but it's the case. You can see it in this exemple :

class foo():

    def __init__(self):
        pass

foo.__new__(foo, arg = 1)
# That works because the method __new__ look like this : def __new__(*args, **kargs).

str.__new__(str, arg = 1)
# That fails because we are trying to pass the attribute 'arg' to a method which look like this : def __new__(anUniqueValue).

From there, we can understand why the solution presented earlier works. What we do is editing the method __new__ of an immutable type to works just like a mutable type.

def __new__(cls, *args, **kwargs):
    return str.__new__(cls)

These 2 lines convert def __new__ (cls, anUniqueValue) to def __new__ (cls, *args, **kwargs)

I hope my explanation is almost clear and without so much mistakes. If you speak french you can learn more on that link : http://sametmax.com/la-difference-entre-new-et-init-en-python/

like image 578
Morgan Avatar asked Dec 25 '15 23:12

Morgan


1 Answers

What you wrote is essentially right, there are a few mistakes here and there.


class A(str):

    def __new__(cls, *args, **kwargs):
        return str.__new__(cls)

    def __init__(self, arg01):
        print(arg01)

This is not completely correct: if you don't pass any argument to str.__new__, your new instance will be the equivalent of an empty string.

class A(str):

    def __new__(cls, arg):
        return str.__new__(cls, arg)

    def __init__(self, arg):
        print(arg)

In general, __new__ and __init__ should have the same signature. It is not a requirement, but in this case you need to pass arg to str.__new__ so you must intercept arg.


The method myClass.__new__ is executed. This method will create the object myInstance. __new__ is the real constructor (__init__ isn't a constructor !). In pseudocode, __new__ look something like this :

It is not the responsibility of __new__ to call __init__, as demonstrated by this simple example:

class C:

    def __new__(cls):
        print('entering __new__')
        self = super().__new__(cls)
        print('exiting __new__')
        return self

    def __init__(self):
        print('entering __init__')
        super().__init__()
        print('exiting __init__')


C()

# Output:
# entering __new__
# exiting __new__
# entering __init__
# exiting __init__

As you can see, in my __new__ I didn't call __init__ explicitly, and object.__new__ is not calling __init__ either.

__init__ is automatically called by the Python interpreter itself whenever __new__ returns an instance of the type.


The situation is a little different when we are using immutable types (str, int, float, tuple).

This is not entirely true. The situation is different when we are inheriting from a type that is not using the default __new__ implementation.

The default __new__ implementation (i.e. object.__new__) is very permissive and ignores every positional argument and keyword argument. If you replace it with a less permissive implementation, then problems like the one you are observing happen.

What is important to understand is that the problem is not brought by the non-default __new__ itself, but by the fact that our __init__ is not compatible with __new__.


 foo.__new__(foo, arg = 1)
 # That works because the method __new__ look like this : def __new__(*args, **kargs).
 str.__new__(str, arg = 1)
 # That fails because we are trying to pass the attribute 'arg' to a method which look like this : def __new__(anUniqueValue).

You got it. Just one bit is wrong: the correct signature for str.__new__ is:

def __new__(cls[, object[, encoding[, errors]]])

All arguments except cls are both positional and keyword argument. In fact you can do:

>>> class C(str):
...     def __init__(self, object):
...         pass
... 
>>> C('abc')
'abc'
>>> C(object='abc')
'abc'

You see? We used a signature for __init__ that is compatible with str.__new__ and now we can avoid overriding __new__!

like image 59
Andrea Corbellini Avatar answered Oct 18 '22 14:10

Andrea Corbellini