1 pontos por GN⁺ 1 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • PEP 661 propõe o objeto chamável embutido do Python sentinel() e a API C PySentinel_New() para criar valores sentinela distintos em situações nas quais None é um valor válido
  • O idiomatismo existente _sentinel = object() faz com que a repr em assinaturas de função seja longa e pouco clara, além de poder causar problemas com assinaturas de tipo explícitas, cópia e serialização com pickle
  • A chamada sentinel('MISSING') cria um novo objeto único com uma repr curta, e para compartilhar o mesmo sentinela é preciso atribuí-lo a uma variável e reutilizá-lo explicitamente, como em MISSING = sentinel('MISSING')
  • Recomenda-se comparar sentinelas com is; eles são avaliados como verdadeiros, copy.copy() e copy.deepcopy() retornam o mesmo objeto e, quando podem ser importados pelo nome a partir de um módulo, preservam a identidade mesmo após serialização com pickle
  • O sistema de tipos permite usar o próprio sentinela em expressões de tipo, como int | MISSING, e a documentação oficial mais recente está na documentação de [sentinel](<https://docs.python.org/3.15/library/functions.html#sentinel "(in Python v3.15>)") do Python 3.15

Contexto da introdução

  • Um valor sentinela (sentinel value), um marcador exclusivo, é usado como valor padrão quando um argumento de função não é fornecido, como valor de retorno para indicar falha em uma busca ou como valor para representar dados ausentes
  • O Python normalmente tem o valor especial None para esse tipo de uso, mas em contextos nos quais o próprio None é um valor válido, é necessário um valor sentinela separado que possa ser distinguido de None
  • Em maio de 2021, na lista de discussão python-dev, foi debatida uma forma melhor de implementar o valor sentinela usado em traceback.print_exception
  • A implementação existente usava o idiomatismo comum _sentinel = object(), mas sua repr era longa demais e pouco informativa, tornando a assinatura da função difícil de ler
    &gt;&gt;&gt; help(traceback.print_exception)  
    Help on function print_exception in module traceback:  
    
    print_exception(exc, /, value=&lt;object object at  
    0x000002825DF09650&gt;, tb=&lt;object object at 0x000002825DF09650&gt;,  
    limit=None, file=None, chain=True)  
    
  • Durante a discussão, também foram identificados outros problemas em implementações existentes de sentinelas
    • alguns sentinelas não têm um tipo próprio, o que dificulta definir assinaturas de tipo claras para funções que usam sentinelas como valor padrão
    • após a cópia, uma instância separada pode ser criada, fazendo com que comparações com is falhem e o comportamento se torne diferente do esperado
    • alguns idiomatismos comuns têm problemas semelhantes mesmo após serializar e desserializar com pickle
  • Victor Stinner forneceu uma lista de valores sentinela usados na biblioteca padrão do Python, e confirmou-se que, mesmo dentro da biblioteca padrão, são usados vários métodos de implementação, muitos deles com um ou mais dos problemas acima
  • A votação no discuss.python.org não chegou a uma conclusão clara, com base em 39 votos
    • 40% escolheram “o estado atual é aceitável e não há necessidade de consistência”
    • a maioria escolheu uma ou mais soluções padronizadas
    • 37% escolheram a opção “usar de forma consistente uma nova fábrica/classe/metaclasse dedicada de sentinela e disponibilizá-la publicamente na biblioteca padrão”
  • Por causa desse resultado dividido, o PEP foi redigido, chegando à conclusão de que uma implementação simples e boa na biblioteca padrão seria útil tanto dentro quanto fora dela
  • Não é obrigatório converter todos os sentinelas existentes da biblioteca padrão para esse método; isso fica a critério de cada mantenedor
  • O documento do PEP é um documento histórico, e a documentação oficial mais recente está na documentação de [sentinel](<https://docs.python.org/3.15/library/functions.html#sentinel "(in Python v3.15>)") do Python 3.15

Critérios de projeto

  • Um objeto sentinela deve ser sempre idêntico a si mesmo quando comparado com o operador is e não deve ser idêntico a nenhum outro objeto
  • A criação de um objeto sentinela deve ser um código de uma linha simples e intuitivo
  • Deve ser possível definir facilmente quantos valores sentinela diferentes forem necessários
  • Um objeto sentinela deve ter uma repr curta e clara
  • Deve ser possível usar uma assinatura de tipo clara para o sentinela
  • Ele deve funcionar corretamente mesmo após cópia e ter comportamento previsível durante serialização e desserialização com pickle
  • Deve funcionar em CPython 3.x e PyPy3 e, se possível, também em outras implementações de Python
  • Tanto a implementação quanto o uso devem ser o mais simples e intuitivos possível, sem se tornarem mais um conceito especial que aumente a carga ao aprender Python
  • Como a biblioteca padrão não pode depender de implementações de pacotes PyPI como sentinels ou sentinel, é necessária uma implementação que possa ser usada dentro da própria biblioteca padrão

Especificação de sentinel()

  • Foi adicionado um novo objeto chamável embutido sentinel
    >>> MISSING = sentinel('MISSING')  
    >>> MISSING  
    MISSING  
    
  • sentinel() recebe um único argumento somente posicional, name, e name deve obrigatoriamente ser uma str
  • Se um valor que não seja string for passado, ocorre TypeError
  • name é usado como o nome do sentinela e em seu repr
  • Objetos sentinela têm dois atributos públicos
    • __name__: nome do sentinela
    • __module__: nome do módulo em que sentinel() foi chamado
  • sentinel não pode ser subclassificado
  • Cada chamada de sentinel(name) retorna um novo objeto sentinela
  • Se o mesmo sentinela precisar ser usado em vários lugares, é necessário atribuí-lo a uma variável e reutilizar explicitamente o mesmo objeto, como no idiom existente MISSING = object()
    MISSING = sentinel('MISSING')  
    
    def read_value(default=MISSING):  
        ...  
    
  • Ao verificar se um determinado valor é um sentinela, recomenda-se usar o operador is, assim como com None
  • A comparação com == também funciona como esperado, retornando True apenas quando comparado com ele mesmo
  • Em geral, uma verificação de identidade como if value is MISSING: é mais apropriada do que uma verificação booleana como if value: ou if not value:
  • Objetos sentinela são truthy, e sua avaliação booleana resulta em True
    • Isso é igual ao comportamento padrão de classes arbitrárias e ao valor booleano de Ellipsis
    • É diferente de None, que é falsy
  • Se um objeto sentinela for copiado com copy.copy() ou copy.deepcopy(), o mesmo objeto será retornado
  • Sentinelas que podem ser importados pelo nome a partir do módulo em que foram definidos preservam a identidade após pickle e unpickle, seguindo o mecanismo padrão de pickle
    MISSING = sentinel('MISSING')  
    assert pickle.loads(pickle.dumps(MISSING)) is MISSING  
    
  • sentinel() registra o módulo de chamada como o atributo __module__ ao criar o sentinela
  • O pickle registra o sentinela por módulo e nome, e o unpickle importa o módulo e então recupera o sentinela pelo nome
  • Sentinelas que não podem ser importados por módulo e nome, como sentinelas criados em escopo local e não atribuídos a um nome correspondente em um global do módulo ou atributo de classe, não podem ser serializados com pickle
  • O repr de um objeto sentinela é o name passado para sentinel(), sem qualificador implícito de módulo
  • Se for necessário um repr qualificado, ele deve ser incluído explicitamente no nome
    >>> MyClass_NotGiven = sentinel('MyClass.NotGiven')  
    >>> MyClass_NotGiven  
    MyClass.NotGiven  
    
  • A comparação de ordem entre objetos sentinela não é definida
  • Sentinelas não oferecem suporte a weakref

Tipagem

  • Para tornar o uso de sentinelas em código Python tipado claro e simples, foi adicionado um tratamento especial para objetos sentinela ao sistema de tipos
  • Objetos sentinela podem ser usados dentro de uma expressão de tipo como valores que representam eles próprios
  • Isso é semelhante à forma como o sistema de tipos existente trata None
    MISSING = sentinel('MISSING')  
    
    def foo(value: int | MISSING = MISSING) -> int:  
        ...  
    
  • Verificadores de tipo devem reconhecer a criação de sentinela no formato NAME = sentinel('NAME') como criação de um novo objeto sentinela
  • Se o nome passado a sentinel() não corresponder ao nome do alvo da atribuição, o verificador de tipos deve emitir um erro
  • Sentinelas definidos com essa sintaxe podem ser usados em expressões de tipo
  • O tipo desse sentinela representa um tipo totalmente estático que tem como único membro o próprio objeto sentinela
  • Verificadores de tipo devem oferecer suporte ao estreitamento de union com sentinela usando os operadores is e is not
    from typing import assert_type  
    
    MISSING = sentinel('MISSING')  
    
    def foo(value: int | MISSING) -> None:  
        if value is MISSING:  
            assert_type(value, MISSING)  
        else:  
            assert_type(value, int)  
    
  • A implementação em runtime deve ter os métodos __or__ e __ror__ para dar suporte ao uso em expressões de tipo, e esses métodos retornam um objeto typing.Union
  • O Typing Council apoiou as partes relacionadas a tipos desta proposta

API C

  • Como sentinelas também podem ser úteis em extensões C, são propostas duas novas funções da API C
  • PyObject *PySentinel_New(const char *name, const char *module_name) cria um novo objeto sentinela
  • bool PySentinel_Check(PyObject *obj) verifica se um objeto é um sentinela
  • Código C pode usar o operador == ao verificar se é um sentinela específico

Compatibilidade e segurança

  • Ao adicionar um novo nome embutido, códigos que hoje presumem que o nome simples sentinel gera NameError deixarão de ver o mesmo resultado
  • Essa é uma consideração de compatibilidade comum ao adicionar novos nomes embutidos
  • Nomes locais, globais ou importados já existentes chamados sentinel não são afetados
  • Códigos que já usam o nome sentinel podem precisar ser ajustados para usar o novo objeto embutido, e podem receber novos avisos de linters que alertam sobre conflito com nomes embutidos
  • Considera-se suficiente a documentação geral para novos recursos embutidos, como docstring, documentação da biblioteca e a seção “What’s New”
  • Considera-se que esta proposta não tem implicações de segurança

Implementação de referência e backport

  • A implementação de referência é fornecida como um pull request do CPython [10]
  • A implementação de referência anterior está em um repositório separado no GitHub [7]
  • O esboço do comportamento pretendido é o seguinte
    class sentinel:  
        """Unique sentinel values."""  
    
        __slots__ = ("__name__", "_module_name")  
    
        def __init_subclass__(cls):  
            raise TypeError("type 'sentinel' is not an acceptable base type")  
    
        def __init__(self, name, /):  
            if not isinstance(name, str):  
                raise TypeError("sentinel name must be a string")  
            self.__name__ = name  
            self._module_name = sys._getframemodulename(1)  
    
        @property  
        def __module__(self):  
            return self._module_name  
    
        def __repr__(self):  
            return self.__name__  
    
        def __reduce__(self):  
            return self.__name__  
    
        def __copy__(self):  
            return self  
    
        def __deepcopy__(self, memo):  
            return self  
    
        def __or__(self, other):  
            return typing.Union[self, other]  
    
        def __ror__(self, other):  
            return typing.Union[other, self]  
    
    • O módulo typing-extensions tem um backport, mas ele ainda não corresponde exatamente ao comportamento da iteração atual do PEP

Alternativas rejeitadas

  • Uso de NotGiven = object()

    • Essa abordagem tem todas as desvantagens tratadas nos critérios de projeto da PEP
    • O repr é longo e pouco claro, é difícil deixar a assinatura de tipo clara, e podem surgir problemas relacionados a cópia ou pickling
  • Adicionar um único novo valor sentinela, como MISSING ou Sentinel

    • Se um único valor for usado em vários lugares para vários propósitos, nem sempre é fácil ter certeza de que, em algum caso de uso, esse próprio valor não será um valor válido
    • Valores sentinela dedicados e distintos podem ser usados com mais confiança sem precisar considerar possíveis casos de borda
    • Valores sentinela precisam poder fornecer nomes significativos e repr adequados ao contexto de uso
    • Essa opção foi muito impopular na votação, com apenas 12% das escolhas
  • Usar o valor sentinela existente Ellipsis

    • Ellipsis não foi originalmente pensado para esse tipo de uso
    • Seu uso tem aumentado para definir blocos vazios de classe ou função no lugar de pass, mas ele não pode ser usado com tanta segurança em todos os casos quanto valores sentinela dedicados e distintos
  • Usar Enum de valor único

    • O idiom proposto é o seguinte
    class NotGivenType(Enum):  
      NotGiven = &#039;NotGiven&#039;  
      NotGiven = NotGivenType.NotGiven  
    
  • Há repetição excessiva, e o repr é longo demais, como &lt;NotGivenType.NotGiven: &#039;NotGiven&#039;&gt;
  • É possível definir um `repr`` mais curto, mas isso aumenta ainda mais o código e a repetição
  • Foi a opção menos popular entre as 9 da votação, sendo a única a não receber nenhum voto
  • Decorador de classe sentinela

    • O idiom proposto é o seguinte
      @sentinel  
      class NotGivenType: pass  
      NotGiven = NotGivenType()  
      
    • A implementação do decorador em si pode ser simples e clara, mas o idiom é verboso demais, repetitivo e difícil de memorizar
  • Uso de objeto de classe

    • Como classes são essencialmente singletons, a ideia de usá-las como valores sentinela é possível
    • A forma mais simples é a seguinte
      class NotGiven: pass  
      
      • Para obter um repr claro, seria necessário um metaclass ou um decorador de classe
      class NotGiven(metaclass=SentinelMeta): pass  
      
      @Sentinel  
      class NotGiven: pass  
      
    • Usar classes dessa maneira é incomum e pode gerar confusão
    • Sem comentários, é difícil entender a intenção do código, e surgem comportamentos inesperados e indesejáveis, como o sentinela se tornar chamável
  • Definir apenas um idiom padrão recomendado, sem implementação

    • A maioria dos idioms existentes e comuns tem desvantagens importantes
    • Até agora, não foi encontrado um idiom claro e conciso que evite essas desvantagens
    • Na votação relacionada, a opção de recomendar um idiom teve pouca popularidade, e até a opção mais votada ficou em apenas 25%
  • Usar um novo módulo da biblioteca padrão

    • O rascunho inicial propunha adicionar a classe Sentinel a um novo módulo sentinels ou sentinellib
    • Adicionar um novo módulo para um único objeto chamável público é desnecessário
    • Usar um módulo torna o recurso menos conveniente do que o idiom existente com object()
    • O Steering Council também recomendou especificamente que isso fosse um recurso embutido, para ser tão fácil de usar quanto object()
    • O nome sentinels já entra em conflito com um pacote PyPI amplamente usado, e torná-lo um recurso embutido evita esse problema de nomenclatura
  • Usar um registro de nomes de sentinela por módulo

    • O rascunho inicial propunha tornar nomes de sentinela únicos dentro de cada módulo
    • Nesse projeto, chamadas repetidas de sentinel("MISSING") no mesmo módulo retornariam o mesmo objeto por meio de um registro global do processo, usando como chave o nome do módulo e o nome do sentinela
    • Esse comportamento foi rejeitado por ser implícito demais
    • Se for necessário um sentinela compartilhado, basta definir explicitamente um, como no padrão existente MISSING = object(), e reutilizá-lo pelo nome
    • Em escopos locais, pode ser desejável um novo sentinela a cada chamada ou repetição, então chamadas repetidas de sentinel(name) devem criar objetos distintos, assim como chamadas repetidas de object()
    • Remover o registro simplifica tanto a implementação quanto o modelo mental, restando apenas a regra de que sentinel(name) cria um novo objeto único cujo repr é name
  • Descoberta ou passagem automática do nome do módulo

    • O rascunho inicial propunha um argumento opcional module_name para dar suporte ao projeto baseado em registro
    • Com a remoção do registro, o argumento público module_name deixa de ser necessário para a proposta principal
    • A implementação registra internamente o módulo chamador, de forma semelhante a TypeVar, para que o pickle possa serializar sentinelas importáveis por módulo e nome
    • O nome interno do módulo não afeta o repr do sentinela
    • Se você quiser um repr com nome de módulo ou de classe, pode incluí-lo explicitamente no único argumento name, como em sentinel("mymodule.MISSING")
  • Permitir personalização de repr

    • Isso tinha a vantagem de permitir migrar valores sentinela existentes para esse modelo sem alterar o repr
    • Mas foi descartado por não se considerar que o benefício justificasse a complexidade adicional
  • Permitir personalização da avaliação booleana

    • Na discussão, foi considerada a possibilidade de tornar sentinelas explicitamente truthy, falsy ou impossíveis de converter com bool
    • Alguns sentinelas de terceiros oferecem comportamento falsy como parte da API pública
    • Vários participantes consideraram que lançar exceção em contexto booleano imporia melhor o uso de testes de identidade
    • A PEP mantém a proposta inicial mais simples preservando o comportamento truthy padrão de objetos comuns e recomendando testes de identidade
    • Comportamento booleano personalizado pode ser considerado mais tarde, caso se conclua que vale a complexidade extra de API e tipagem
  • Usar typing.Literal em anotações de tipo

    • Várias pessoas sugeriram isso na discussão, e a PEP também adotou inicialmente essa abordagem
    • No entanto, Literal["MISSING"] pode causar confusão porque aponta para o valor de string "MISSING", e não para uma referência futura ao valor sentinela MISSING
    • O uso de bare name também foi sugerido com frequência na discussão
    • A abordagem de bare name segue o precedente criado por None e um padrão bem conhecido, não exige importação e é muito mais curta

Diretrizes adicionais de uso

  • Ao definir um sentinela no escopo de uma classe, ao evitar conflitos de nome ou quando um repr qualificado for mais claro, deve-se passar explicitamente o nome qualificado desejado
    &gt;&gt;&gt; class MyClass:  
    ...    NotGiven = sentinel(&#039;MyClass.NotGiven&#039;)  
    &gt;&gt;&gt; MyClass.NotGiven  
    MyClass.NotGiven  
    
  • É permitido criar sentinelas dentro de funções ou métodos
  • Como cada chamada de sentinel() gera um objeto diferente, sentinelas criados em escopo local se comportam como valores criados com chamadas a object() naquele escopo
  • O valor booleano de NotImplemented é True, mas usar isso está obsoleto desde Python 3.9 e gera um deprecation warning
  • Essa obsolescência ocorre por causa de problemas específicos de NotImplemented descritos em bpo-35712 [8]
  • Se for necessário definir vários valores sentinela relacionados ou definir uma ordem entre eles, deve-se usar Enum ou abordagem semelhante
  • Quanto à tipagem desses sentinelas, várias opções foram discutidas na lista de e-mails typing-sig [9]

1 comentários

 
GN⁺ 1 시간 전
Comentários no Lobste.rs
  • O nome escolhido parece estranho, porque o significado é estreito demais
    Só pelo nome, algo como símbolo único teria parecido um primitivo mais flexível. Na prática, isso quase certamente vai se comportar como um símbolo, então até dá para usar assim, mas chamar isso de “Sentinels” soa esquisito. Talvez eu sinta isso por estar acostumado com Lisp

    • O objetivo parece ser fazer com que SENTINEL_A seja de um tipo diferente de SENTINEL_B, para que seja possível perguntar se um valor is_a SENTINEL_A
      Os símbolos do Ruby não funcionam assim: :beef.is_a? :droog.class #=> true
    • O raciocínio ao estilo Lisp faz sentido. Você está assumindo que o uso amplo é desejável e um problema que precisa ser resolvido, mas o Python já tem Literal e strings literais para a maioria dos casos de uso de símbolos Lisp
      Eles são sentinelas nomeados porque sentinel values são um conceito e padrão comuns em Python, e sentinelas tentam resolver de forma específica alguns dos problemas que surgem ao usar esse padrão. Exatamente como está explicado nas seções “Motivation” e “Rationale”
      Além disso, sentinelas não têm semântica de valor, então mesmo duas sentinelas com o mesmo nome são valores diferentes e não são iguais entre si. Portanto, elas também não se comportam como símbolos e não deveriam ser usadas dessa forma
  • No problema de valores padrão para argumentos nomeados, no Typst bastaria adicionar um valor auto junto com none para expressar quase toda interface de argumento nomeado que se queira
    none não faz muito sentido como valor padrão para a maioria dos argumentos nomeados. none é bom como valor de retorno padrão, mas quando entra como argumento de função muitas vezes não carrega o significado correto como substantivo. matrix(axes=None) é ambíguo: quer dizer remover os eixos, ou mantê-los como de costume? Também não fica claro se passar none é diferente de não passar nada. Se você recorrer a despacho múltiplo para distinguir a presença do parâmetro, perde um ponto central para documentar o comportamento desse parâmetro
    auto é um excelente valor padrão porque comunica exatamente “faça o que for apropriado com a informação disponível”. A assinatura auto | none pode ser usada como um booleano mais explícito, e T | auto | none transmite bastante informação sobre como a função vai usar o valor. Por exemplo, se T for color, auto provavelmente escolherá um valor padrão como branco/preto ou herdará do pai; T define explicitamente a cor; e none, dependendo do contexto, pode significar não definir cor nenhuma ou tratá-la como transparente

  • Interessante, e fico curioso sobre como a semântica de alguns pacotes pode mudar. Por exemplo, em vez de retornar Item | None, daria para usar algo como abaixo

    NOT_FOUND = sentinel("NOT_FOUND")
    def get_item(iid: str) -> Item | NOT_FOUND: ...
    

    Claro, também daria para transmitir significado adicional com várias sentinelas. Já era possível fazer isso antes, mas não havia uma forma “oficialmente recomendada” na documentação. Isso talvez acabe levando autores de pacotes para outro caminho

    MISSING_ID = sentinel("MISSING_ID")
    MISSING_VALUE = sentinel("MISSING_VALUE")
    
    def get_item(iid: str) -> Item | MISSING_ID | MISSING_VALUE: ...
    

    É um exemplo um pouco forçado, mas nesse caso dá para distinguir entre a situação em que o ID existe, mas não tem valor associado, e a situação em que a busca falhou porque esse ID nem existe. O jeito mais “pythônico” provavelmente seria usar exceções, mas isso parece uma abordagem mais funcional do que o normal ao escrever Python

    • Parece uma forma mais elegante de fazer o singleton que antes era feito criando uma classe vazia e instanciando por módulo
      class _MissingId: ...
      
      MISSING_ID = _MissingId()
      
      # elsewhere
      from ... import MISSING_ID
      
      Lembra Symbols
    • O PEP diz que, se você quiser definir vários valores sentinela relacionados ou até estabelecer uma ordem entre eles, então deveria usar Enum ou algo parecido
  • Acho que teria sido melhor simplesmente adotar a API Symbol do JavaScript. Ela é útil de forma geral e também resolveria o problema que estão tentando atacar aqui