Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Class constructors and keyword arguments - How does Python determine which one is unexpected?

Tags:

Say I define the following class:

class MyClass(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

Normally, one would instantiate this class in one of the following ways:

>>> MyClass(1,2)
<__main__.MyClass object at 0x8acbf8c>
>>> MyClass(1, y=2)
<__main__.MyClass object at 0x8acbeac>
>>> MyClass(x=1, y=2)
<__main__.MyClass object at 0x8acbf8c>
>>> MyClass(y=2, x=1)
<__main__.MyClass object at 0x8acbeac>

Which is just fine and dandy.

Now, we try with an invalid keyword argument and see what happens:

>>> MyClass(x=1, j=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'j'

Python correctly raises a type error and complains about the unexpected keyword argument 'j'.

Now, we can try with two invalid keyword arguments:

>>> MyClass(i=1,j=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'i'

Notice, that two of the keyword arguments were invalid, but Python is only complaining about one of them, 'i' in this case.

Lets reverse the order of the invalid keyword arguments:

>>> MyClass(j=2, i=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'i'

That is interesting. I changed the order of the invalid keyword arguments, but Python still decides to complain about 'i' and not 'j'. So Python obviously doesn't simply pick the first invalid key to complain about.

Lets try some more:

>>> MyClass(c=2, i=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'i'
>>> MyClass(q=2, i=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'i'

Alphabetically, I tried a letter before i and one after i, so Python is not complaining alphabetically.

Here are some more, this time with i in the first position:

>>> MyClass(i=1, j=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'i'
>>> MyClass(i=1, b=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'i'
>>> MyClass(i=1, a=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'a'

Aha! I got it to complain about 'a' instead of 'i'.

My question is, when invalid keyword arguments are given to a class constructor, how does Python determine which one to complain about?

like image 318
DJG Avatar asked Aug 30 '13 18:08

DJG


1 Answers

Keyword arguments are stored in a dictionary, and dictionary order (e.g. arbitrary, based on the hashing algorithm, hash collisions and insertion history) applies.

For your first sample, a dictionary with both i and j keys results in i being listed first:

>>> dict(j=2, i=1)
{'i': 1, 'j': 2}

Note that the {...} literal dict notation inserts keys from right-to-left, while keyword parsing inserts keywords from left-to-right (this is a CPython implementation detail); hence the use of the dict() constructor in the above example.

This matters when two keys hash to the same slot, like i and a:

>>> dict(i=1, a=2)
{'a': 2, 'i': 1}
>>> {'i': 1, 'a': 2}
{'i': 1, 'a': 2}

Dictionary output order is highly dependent on the insertion and deletion history and the specific Python implementation; Python 3.3 introduced a random hash seed to prevent a serious denial of service vector, for example, which means that the dictionary order will be radically different even between Python processes.

like image 105
Martijn Pieters Avatar answered Sep 18 '22 15:09

Martijn Pieters