PEP 661 – Valor sentinela aprovado após 5 anos
(peps.python.org)- PEP 661 propõe o objeto chamável embutido do Python
sentinel()e a API CPySentinel_New()para criar valores sentinela distintos em situações nas quaisNoneé um valor válido - O idiomatismo existente
_sentinel = object()faz com que areprem 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 umareprcurta, e para compartilhar o mesmo sentinela é preciso atribuí-lo a uma variável e reutilizá-lo explicitamente, como emMISSING = sentinel('MISSING') - Recomenda-se comparar sentinelas com
is; eles são avaliados como verdadeiros,copy.copy()ecopy.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
Nonepara esse tipo de uso, mas em contextos nos quais o próprioNoneé um valor válido, é necessário um valor sentinela separado que possa ser distinguido deNone - 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 suareprera longa demais e pouco informativa, tornando a assinatura da função difícil de ler>>> help(traceback.print_exception) Help on function print_exception in module traceback: print_exception(exc, /, value=<object object at 0x000002825DF09650>, tb=<object object at 0x000002825DF09650>, 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
isfalhem 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
ise 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
reprcurta 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
sentinelsousentinel, é 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, enamedeve obrigatoriamente ser umastr- Se um valor que não seja string for passado, ocorre
TypeError nameé usado como o nome do sentinela e em seurepr- Objetos sentinela têm dois atributos públicos
__name__: nome do sentinela__module__: nome do módulo em quesentinel()foi chamado
sentinelnã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 comNone - A comparação com
==também funciona como esperado, retornandoTrueapenas 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 comoif value:ouif 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
- Isso é igual ao comportamento padrão de classes arbitrárias e ao valor booleano de
- Se um objeto sentinela for copiado com
copy.copy()oucopy.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
reprde um objeto sentinela é onamepassado parasentinel(), sem qualificador implícito de módulo - Se for necessário um
reprqualificado, 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
NoneMISSING = 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
iseis notfrom 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 objetotyping.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 sentinelabool 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
sentinelgeraNameErrordeixarã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
sentinelnão são afetados - Códigos que já usam o nome
sentinelpodem 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
MISSINGouSentinel- 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
repradequados ao contexto de uso - Essa opção foi muito impopular na votação, com apenas 12% das escolhas
-
Usar o valor sentinela existente
EllipsisEllipsisnã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
Enumde valor único- O idiom proposto é o seguinte
class NotGivenType(Enum): NotGiven = 'NotGiven' NotGiven = NotGivenType.NotGiven - Há repetição excessiva, e o
repré longo demais, como<NotGivenType.NotGiven: 'NotGiven'> - É 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
- O idiom proposto é o seguinte
-
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
reprclaro, seria necessário um metaclass ou um decorador de classe
class NotGiven(metaclass=SentinelMeta): pass@Sentinel class NotGiven: pass - Para obter um
- 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
Sentinela um novo módulosentinelsousentinellib - 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
sentinelsjá entra em conflito com um pacote PyPI amplamente usado, e torná-lo um recurso embutido evita esse problema de nomenclatura
- O rascunho inicial propunha adicionar a classe
-
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 deobject() - 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 cujorepréname
-
Descoberta ou passagem automática do nome do módulo
- O rascunho inicial propunha um argumento opcional
module_namepara dar suporte ao projeto baseado em registro - Com a remoção do registro, o argumento público
module_namedeixa 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
reprdo sentinela - Se você quiser um
reprcom nome de módulo ou de classe, pode incluí-lo explicitamente no único argumentoname, como emsentinel("mymodule.MISSING")
- O rascunho inicial propunha um argumento opcional
-
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
- Isso tinha a vantagem de permitir migrar valores sentinela existentes para esse modelo sem alterar o
-
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
- Na discussão, foi considerada a possibilidade de tornar sentinelas explicitamente truthy, falsy ou impossíveis de converter com
-
Usar
typing.Literalem 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 sentinelaMISSING - 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
Nonee 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
reprqualificado for mais claro, deve-se passar explicitamente o nome qualificado desejado>>> class MyClass: ... NotGiven = sentinel('MyClass.NotGiven') >>> 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 aobject()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
NotImplementeddescritos em bpo-35712 [8] - Se for necessário definir vários valores sentinela relacionados ou definir uma ordem entre eles, deve-se usar
Enumou abordagem semelhante - Quanto à tipagem desses sentinelas, várias opções foram discutidas na lista de e-mails typing-sig [9]
1 comentários
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
SENTINEL_Aseja de um tipo diferente deSENTINEL_B, para que seja possível perguntar se um valoris_a SENTINEL_AOs símbolos do Ruby não funcionam assim:
:beef.is_a? :droog.class #=> trueLiterale strings literais para a maioria dos casos de uso de símbolos LispEles 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
autojunto comnonepara expressar quase toda interface de argumento nomeado que se queiraSó
nonenã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 passarnoneé 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âmetroautoé um excelente valor padrão porque comunica exatamente “faça o que for apropriado com a informação disponível”. A assinaturaauto | nonepode ser usada como um booleano mais explícito, eT | auto | nonetransmite bastante informação sobre como a função vai usar o valor. Por exemplo, seTforcolor,autoprovavelmente escolherá um valor padrão como branco/preto ou herdará do pai;Tdefine explicitamente a cor; enone, dependendo do contexto, pode significar não definir cor nenhuma ou tratá-la como transparenteInteressante, 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 abaixoClaro, 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
É 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
Acho que teria sido melhor simplesmente adotar a API
Symboldo JavaScript. Ela é útil de forma geral e também resolveria o problema que estão tentando atacar aqui