Question: An old-style class works, new-style class broken

Question

An old-style class works, new-style class broken

Answers 1
Added at 2016-12-08 22:12
Tags
Question
class A_old:
    def __getattr__(self, attr):
        print 'getattr', attr
        return super(A_old, self).__getattr__(attr)  # <-- note: don't do this!
    def __trunc__(self):
        return 3

class A_new(object):
    def __getattr__(self, attr):
        print 'getattr', attr
        return super(A_new, self).__getattr__(attr)
    def __trunc__(self):
        return 3

The old-style class works, but the new-style class doesn't.

>>> range(A_old())
getattr __int__
[0, 1, 2]
>>> range(A_new())
TypeError: range() integer end argument expected, got A_new.

Why?


Note: I'm using 2.7 above. None of this applies in Python 3, where range is documented as responding to __index__ and old-style classes have gone the way of the Norwegian Blue.

Answers to

An old-style class works, new-style class broken

nr: #1 dodano: 2016-12-08 22:12

Old-style classes implement a different method to test if you can convert to a number, one that supports using __trunc__ if __int__ doesn't exist.

range() (Python 2), uses Py_TYPE(arg)->tp_as_number->nb_int() to convert the value to an integer, which is roughly, but not quite, like using int(). So we have to look at the nb_int() slot for both old and new-style classes here.

Old-style classes implement the nb_int slot as instance_int(), which uses hasattr() (or rather, the C equivalent) to test for __int__:

if (PyObject_HasAttr((PyObject*)self, int_name))
    return generic_unary_op(self, int_name);

hasattr() swallows all exceptions, including the TypeError your old-style class throws:

>>> A_old().__int__
getattr __int__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __getattr__
TypeError: super() argument 1 must be type, not classobj

Because hasattr() swallows the exception, hasattr() returns False:

>>> hasattr(A_old(), '__int__')
getattr __int__
False

and the next line in instance_int() then uses __trunc__:

truncated = _instance_trunc(self);

New-style classes never also use __trunc__ when you ask for nb_int; they want __int__ or bust. That's because they support slots directly; tp_as_number->nb_int() directly calls __int__ if available (bypassing __getattribute__ altogether).

Note that when explicitly using int() to convert, then the underlying C code will look for a __trunc__ attribute explicitly (using it only if no tp_as_number->nb_int() slot is available), but at least it won't use hasattr() for this. This means using int() on your new-style class still works:

>>> int(A_new())
3

In Python 3, all use of __trunc__ treats it as a proper special method.

Source Show
◀ Wstecz