← index #3592Issue #6600
Related · medium · value 0.380
QUERY · ISSUE

Assigning to __class__ fails without error

openby nickovsopened 2018-02-02updated 2021-05-11
py-core

In the standard Python implementation it is possible to assign to the __class__ attribute of an object. This means that the following code works:

class A:
    def __init__(self, x):
        self.x = x

class B(A):
    @property
    def y(self):
        return self.x*2

a = A(10)
a.__class__ = B
print(a.y)   # Prints 20

One can debate the desirability of this sort of code but it can be quite useful for cases such as unpacking structured data where part of the structure contains type information and you'd like to have different accessor methods for the fields depending on the type tag.

Anyway, whether you like this style or not, in MicroPython assigning to __class__ silently fails. While I'd personally prefer it to succeed at the very least if it's going to fail it should fail with a AttributeError saying that the attribute can't be set.

CANDIDATE · ISSUE

py/objtype.c: Issue with bytecode cache invalidation and incorrect lookup order for descriptors.

openby jimmoopened 2020-11-05updated 2020-12-07

@pmp-p referenced https://bugs.python.org/issue42266 on the IRC channel. For context on that bug, what they're saying is that the Python 3.8 behavior is correct (i.e. prints 2) and Python 3.10's caching breaks it (and it prints 1, because f(o) is caching the lookup of o.x, without using the descriptor).

MicroPython also has the same bug, but also it looks like our implementation of descriptor lookup is also incorrect in this case and it still does the wrong thing with bytecode lookup caching disabled. A simpler repro for MicroPython is:

class Descriptor:
    pass

class C:
    x = Descriptor()

    def __init__(self):
        self.x = 1

o = C()

Descriptor.__get__ = lambda *args: 2
Descriptor.__set__ = lambda *args: None

print(o.x)

In CPython (3.8), the behavior is that the o.x on the last line (as per https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance) will first see that type(o).x is a descriptor, and use that instead of o.x. MicroPython doesn't do that -- mp_obj_instance_load_attr will return immediately with the result of mp_map_lookup(&self->members...) and only after that look up the type's members.

Additionally, setting the __get__ property on Descriptor will not set MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS on C, so the extra descriptor behavior will not apply on type(o).

And finally, just like CPython 3.10, if bytecode lookup caching is enabled, o.x will bypass all of this and use the cached value anyway.

Keyboard

j / / n
next pair
k / / p
previous pair
1 / / h
show query pane
2 / / l
show candidate pane
c
copy suggested comment
r
toggle reasoning
g i
go to index
?
show this help
esc
close overlays

press ? or esc to close

copied