Por que definir um descritor em uma classe substitui o descritor?

10

Reprodução simples:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

class B(object):
    v = VocalDescriptor()

B.v # prints "__get__, obj=None, objtype=<class '__main__.B'>"
B.v = 3 # does not print "__set__", evidently does not trigger descriptor
B.v # does not print anything, we overwrote the descriptor

Essa pergunta possui uma duplicata efetiva , mas a duplicata não foi respondida e eu procurei um pouco mais na fonte do CPython como um exercício de aprendizado. Atenção: entrei no mato. Estou realmente esperando conseguir ajuda de um capitão que conhece essas águas . Tentei ser o mais explícito possível ao rastrear as ligações que estava olhando, para meu próprio benefício futuro e o benefício de futuros leitores.

Eu já vi muita tinta derramada sobre o comportamento de __getattribute__aplicado aos descritores, por exemplo, precedência de pesquisa. O trecho de código Python em "Invoking Descriptors", logo abaixo, For classes, the machinery is in type.__getattribute__()...concorda em minha mente com o que acredito ser a fonte CPython correspondente type_getattro, na qual localizei olhando "tp_slots" e depois onde tp_getattro é preenchido . E o fato de B.vinicialmente imprimir __get__, obj=None, objtype=<class '__main__.B'>faz sentido para mim.

O que eu não entendo é: por que a tarefa B.v = 3substitui cegamente o descritor, em vez de disparar v.__set__? Tentei rastrear a chamada CPython, iniciando mais uma vez a partir de "tp_slots" , depois olhando para onde tp_setattro está preenchido , depois olhando para type_setattro . type_setattro parece ser um invólucro fino em torno de _PyObject_GenericSetAttrWithDict . E há o cerne da minha confusão: _PyObject_GenericSetAttrWithDictparece ter lógica que dá precedência ao __set__método de um descritor !! Com isso em mente, não consigo descobrir por que B.v = 3substitui cegamente, em vvez de acionar v.__set__.

Isenção de responsabilidade 1: Eu não reconstruí o Python da fonte com o printfs, por isso não tenho muita certeza do type_setattroque está sendo chamado durante B.v = 3.

Isenção de responsabilidade 2: VocalDescriptornão pretende exemplificar a definição do descritor "típico" ou "recomendado". É um no-op detalhado para me dizer quando os métodos estão sendo chamados.

Michael Carilli
fonte
11
Para mim, este estampas 3 na última linha ... O código funciona bem
Jab
3
Os descritores se aplicam ao acessar atributos de uma instância , não a própria classe. Para mim, o mistério é por que __get__funcionou, e não por __set__que não funcionou.
Jasonharper 16/10/19
11
O @Jab OP ainda espera invocar o __get__método. B.v = 3efetivamente substituiu o atributo por um int.
r.ook
2
O acesso ao atributo @jasonharper determina se __get__é chamado e as implementações padrão de object.__getattribute__e type.__getattribute__invocam __get__ao usar uma instância ou a classe. A atribuição via __set__é apenas para instância.
chepner
@jasonharper Acredito que os __get__métodos dos descritores devem disparar quando invocados da própria classe. É assim que @classmethods e @staticmethods são implementados, de acordo com o guia de instruções . @ Jab Eu estou querendo saber por que B.v = 3é capaz de substituir o descritor de classe. Com base na implementação do CPython, eu esperava B.v = 3também disparar __set__.
Michael Carilli 16/10/19

Respostas:

6

Você está correto que B.v = 3 simplesmente substituir o descritor por um número inteiro (como deveria).

Para B.v = 3chamar um descritor, o descritor deveria ter sido definido na metaclasse, ou seja, ativado type(B).

>>> class BMeta(type): 
...     v = VocalDescriptor() 
... 
>>> class B(metaclass=BMeta): 
...     pass 
... 
>>> B.v = 3 
__set__

Para chamar o descritor em B , você usaria uma instância: o B().v = 3fará.

O motivo para B.vchamar o getter é permitir o retorno da própria instância do descritor. Geralmente você faria isso, para permitir acesso ao descritor por meio do objeto de classe:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

Agora B.vretornaria uma instância como a <mymodule.VocalDescriptor object at 0xdeadbeef>qual você pode interagir. É literalmente o objeto descritor, definido como um atributo de classe, e seu estadoB.v.__dict__ é compartilhado entre todas as instâncias de B.

É claro que depende do código do usuário definir exatamente o que eles querem B.vfazer, retornar selfé apenas o padrão comum.

wim
fonte
11
Para completar essa resposta, eu acrescentaria que __get__foi projetado para ser chamado como atributo de instância ou atributo de classe, mas __set__é projetado para ser chamado apenas como atributo de instância. E documentos relevantes: docs.python.org/3/reference/datamodel.html#object.__get__ #
dez /
@wim Magnificent !! Paralelamente, eu estava olhando mais uma vez a cadeia de chamadas type_setattro. Vejo que a chamada para _PyObject_GenericSetAttrWithDict fornece o tipo (nesse ponto B, no meu caso).
Michael Carilli
Dentro _PyObject_GenericSetAttrWithDict, ele puxa o Py_TYPE de B as tp, que é a metaclasse de B ( typeno meu caso), então é a metaclasse tp que é tratada pela lógica de curto-circuito do descritor . Portanto, o descritor definido diretamente B não é visto por essa lógica de curto-circuito (portanto, no meu código original __set__não é chamado), mas um descritor definido na metaclasse é visto pela lógica de curto-circuito.
Michael Carilli
Portanto, no seu caso em que a metaclasse possui um descritor, o __set__método desse descritor é chamado.
Michael Carilli
@sanyash fique à vontade para editar diretamente.
Wim
3

Exceto qualquer substituição, B.vé equivalente a type.__getattribute__(B, "v"), enquanto b = B(); b.vé equivalente a object.__getattribute__(b, "v"). Ambas as definições invocam o__get__ método do resultado, se definido.

Observe, pensou, que a chamada para __get__difere em cada caso. B.vpassa Nonecomo o primeiro argumento, enquanto B().vpassa a própria instância. Em ambos os casosB é passado como o segundo argumento.

B.v = 3, por outro lado, é equivalente a type.__setattr__(B, "v", 3), que não chama __set__.

chepner
fonte