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.v
inicialmente imprimir __get__, obj=None, objtype=<class '__main__.B'>
faz sentido para mim.
O que eu não entendo é: por que a tarefa B.v = 3
substitui 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_GenericSetAttrWithDict
parece 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 = 3
substitui cegamente, em v
vez 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_setattro
que está sendo chamado durante B.v = 3
.
Isenção de responsabilidade 2: VocalDescriptor
nã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.
fonte
__get__
funcionou, e não por__set__
que não funcionou.__get__
método.B.v = 3
efetivamente substituiu o atributo por umint
.__get__
é chamado e as implementações padrão deobject.__getattribute__
etype.__getattribute__
invocam__get__
ao usar uma instância ou a classe. A atribuição via__set__
é apenas para instância.__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 queB.v = 3
é capaz de substituir o descritor de classe. Com base na implementação do CPython, eu esperavaB.v = 3
também disparar__set__
.Respostas:
Você está correto que
B.v = 3
simplesmente substituir o descritor por um número inteiro (como deveria).Para
B.v = 3
chamar um descritor, o descritor deveria ter sido definido na metaclasse, ou seja, ativadotype(B)
.Para chamar o descritor em
B
, você usaria uma instância: oB().v = 3
fará.O motivo para
B.v
chamar 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:Agora
B.v
retornaria 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 deB
.É claro que depende do código do usuário definir exatamente o que eles querem
B.v
fazer, retornarself
é apenas o padrão comum.fonte
__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__ #_PyObject_GenericSetAttrWithDict
, ele puxa o Py_TYPE de B astp
, que é a metaclasse de B (type
no meu caso), então é a metaclassetp
que é tratada pela lógica de curto-circuito do descritor . Portanto, o descritor definido diretamenteB
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.__set__
método desse descritor é chamado.Exceto qualquer substituição,
B.v
é equivalente atype.__getattribute__(B, "v")
, enquantob = B(); b.v
é equivalente aobject.__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.v
passaNone
como o primeiro argumento, enquantoB().v
passa a própria instância. Em ambos os casosB
é passado como o segundo argumento.B.v = 3
, por outro lado, é equivalente atype.__setattr__(B, "v", 3)
, que não chama__set__
.fonte