python / python/cpython

__get__ called without being bound when a bound method should be called

Offen
#98,905 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

interpreter-core
Vorherrschende Sprache
Python
Sterne
77.2k
Forks
36k
PR-Merge-Kennzahlen
PR-Kennzahlen ausstehend

Beschreibung

(Edited a not important part of the code.)

Suppose a class D has a __get__ method D.__dict__["__get__"] for its instances, and O.__dict__["d"] is an object of type D, where O is a class. Then, for an object o of type O, my expectation was the value o.d would be got as (O.__dict__["d"]).__get__(o, O). In reality, its evaluation tries to call D.__dict__["__get__"] (on the arguments O.__dict__["d"], o, O). See the code below. I used Python 3.9.7 on MacOS.

This means it wouldn't help if d.__get__ will be callable for every object d of type D. For instance, D.__dict__["__get__"] can't be of type property, functools.partialmethod or staticmethod to work properly. Another concern is if D.__dict__["__get__"] happens to be callable in an unexpected way, then one can get an unexpected value for o.d, which would be worse if it happens. (If D.__dict__["__get__"] is callable and its class type(D.__dict__["__get__"]) doesn't have or inherit a __get__ method, then that __get__ method of D seems to be meant static.)

I don't know where else a dunder method may be required to be callable without being bound, but I think such a requirement should be replaced with one that bound methods be callable. In the case above, evaluation of o.d (or O.d) should call the bound method (O.__dict__["d"]).__get__.

Here's a code for your convenience.

from functools import partial

# Utility
def unpack(iterator):
    next_ = iterator.__next__
    while True:
        try:
            next_()
        except StopIteration:
            return

# Context
class Get:
    '''Creates a method to be __get__ .
    '''
    def __get__(self, descriptor, owner=None):
        return self if descriptor is None\
            else partial(self._do, descriptor)

    def _do(self, descriptor, instance, owner=None):
        return descriptor if instance is None else None

    # For the purpose of inspection, instances will be callable in an
    # unexpected way.
    def __call__(self, /, *args, **kwargs):
        print("Called:", (dict_ := locals()).pop("self"))
        return dict_

class D:
    '''A class of descriptors.
    '''
    __get__ = Get()

    def __set__(self, instance, value):
        return

    def __delete__(self, instance):
        return

class O:
    '''A class with a descriptor of type D .
    '''
    d = D()

# Test
unpack(map(print,
           map(eval,
               map("f'{{{}=}}'".format,
                   ('D.__dict__["__get__"]',
                    'O.__dict__["d"]',
                    'O.d',
                    '(o := O())',
                    'o.d', )))))

The expectation from the code is

O.d ---> O.__dict__["d"],
o.d ---> None.

An example of what actually get printed:

D.__dict__["__get__"]=<__main__.Get object at 0x100e21400>
O.__dict__["d"]=<__main__.D object at 0x100e213d0>
Called: <__main__.Get object at 0x100e21400>
O.d={'args': (<__main__.D object at 0x100e213d0>, None, <class '__main__.O'>), 'kwargs': {}}
(o := O())=<__main__.O object at 0x100e210a0>
Called: <__main__.Get object at 0x100e21400>
o.d={'args': (<__main__.D object at 0x100e213d0>, <__main__.O object at 0x100e210a0>, <class '__main__.O'>), 'kwargs': {}}

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Führe zunächst den Reproducer aus dem Issue aus und verfolge die Deskriptor-Suche für O.d und o.d, wobei du dich darauf konzentrierst, wie get aufgerufen wird. Die Arbeit ist abgeschlossen, wenn das Verhalten der angegebenen Erwartung entspricht: O.d gibt O.dict["d"] zurück und o.d gibt None zurück, mit einer Regressionstestabdeckung für das Beispiel.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
python
Bereich
compilers
Issue-Typ
Bug
Schwierigkeit
5/5
Geschätzter Aufwand
Über eine Woche
Aktivitätsstatus
Veraltet
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
25/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.