valores nominais e padrão para argumentos de palavras-chave opcionais


300

Estou tentando converter uma classe "data" oca longa em uma tupla nomeada. Atualmente, minha turma tem a seguinte aparência:

class Node(object):
    def __init__(self, val, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

Após a conversão, namedtupleele se parece com:

from collections import namedtuple
Node = namedtuple('Node', 'val left right')

Mas há um problema aqui. Minha classe original me permitiu passar apenas um valor e cuidou do padrão usando valores padrão para os argumentos nomeado / palavra-chave. Algo como:

class BinaryTree(object):
    def __init__(self, val):
        self.root = Node(val)

Mas isso não funciona no caso da minha tupla nomeada refatorada, pois espera que eu passe por todos os campos. É claro que posso substituir as ocorrências de Node(val)to, Node(val, None, None)mas não é do meu agrado.

Existe um bom truque que pode tornar minha reescrita bem-sucedida sem adicionar muita complexidade de código (metaprogramação) ou devo apenas engolir a pílula e prosseguir com a "pesquisa e substituição"? :)


2
Por que você deseja fazer essa conversão? Eu gosto da sua Nodeclasse original do jeito que está. Por que converter em tupla nomeada?
— Steveha

34
Eu queria fazer essa conversão porque as Nodeclasses atuais e outras são simples objetos de valor do detentor de dados com vários campos diferentes ( Nodeé apenas um deles). Essas declarações de classe nada mais são do que o ruído da linha IMHO, portanto, queria apará-las. Por que manter algo que não é necessário? :)
— sasuke

Você não tem nenhuma função de método em suas aulas? Você não tem, por exemplo, um .debug_print()método que anda na árvore e a imprime?
— Steveha

2
Claro que sim, mas isso é para a BinaryTreeclasse. Nodee outros titulares de dados não exigem métodos especiais, especialmente porque as tuplas nomeadas têm uma representação __str__e um valor decente __repr__. :)
— sasuke

Ok, parece razoável. E acho que Ignacio Vazquez-Abrams lhe deu a resposta: use uma função que faça os valores padrão para o seu nó.
— Steveha

Respostas:


532

Python 3.7

Use o parâmetro padrão .

>>> from collections import namedtuple
>>> fields = ('val', 'left', 'right')
>>> Node = namedtuple('Node', fields, defaults=(None,) * len(fields))
>>> Node()
Node(val=None, left=None, right=None)

Ou, melhor ainda, use a nova biblioteca de classes de dados , que é muito melhor do que o nome de duplo.

>>> from dataclasses import dataclass
>>> from typing import Any
>>> @dataclass
... class Node:
...     val: Any = None
...     left: 'Node' = None
...     right: 'Node' = None
>>> Node()
Node(val=None, left=None, right=None)

Antes do Python 3.7

Defina Node.__new__.__defaults__com os valores padrão.

>>> from collections import namedtuple
>>> Node = namedtuple('Node', 'val left right')
>>> Node.__new__.__defaults__ = (None,) * len(Node._fields)
>>> Node()
Node(val=None, left=None, right=None)

Antes do Python 2.6

Defina Node.__new__.func_defaultscom os valores padrão.

>>> from collections import namedtuple
>>> Node = namedtuple('Node', 'val left right')
>>> Node.__new__.func_defaults = (None,) * len(Node._fields)
>>> Node()
Node(val=None, left=None, right=None)

Ordem

Em todas as versões do Python, se você definir menos valores padrão do que os existentes no parâmetro nomeado, os padrões serão aplicados aos parâmetros mais à direita. Isso permite que você mantenha alguns argumentos como argumentos necessários.

>>> Node.__new__.__defaults__ = (1,2)
>>> Node()
Traceback (most recent call last):
  ...
TypeError: __new__() missing 1 required positional argument: 'val'
>>> Node(3)
Node(val=3, left=1, right=2)

Wrapper para Python 2.6 a 3.6

Aqui está um invólucro para você, que ainda permite (opcionalmente) definir os valores padrão para algo diferente de None. Isso não suporta os argumentos necessários.

import collections
def namedtuple_with_defaults(typename, field_names, default_values=()):
    T = collections.namedtuple(typename, field_names)
    T.__new__.__defaults__ = (None,) * len(T._fields)
    if isinstance(default_values, collections.Mapping):
        prototype = T(**default_values)
    else:
        prototype = T(*default_values)
    T.__new__.__defaults__ = tuple(prototype)
    return T

Exemplo:

>>> Node = namedtuple_with_defaults('Node', 'val left right')
>>> Node()
Node(val=None, left=None, right=None)
>>> Node = namedtuple_with_defaults('Node', 'val left right', [1, 2, 3])
>>> Node()
Node(val=1, left=2, right=3)
>>> Node = namedtuple_with_defaults('Node', 'val left right', {'right':7})
>>> Node()
Node(val=None, left=None, right=7)
>>> Node(4)
Node(val=4, left=None, right=7)

22
Vamos ver ... sua linha única: a) é a resposta mais curta / mais simples, b) preserva a eficiência do espaço, c) não quebra isinstance... todos os profissionais, sem contras ... pena que você estava um pouco atrasado para a festa. Esta é a melhor resposta.
— Gerrat

1
Um problema com a versão do invólucro: diferentemente das coleções internas. Nomeado duplo, esta versão não é selecionável / multiprocesso serializável se o def () estiver incluído em um módulo diferente.
— Michael Scott Cuthbert

2
Eu dei a esta resposta um voto positivo, pois é preferível à minha. É uma pena, no entanto, que minha própria resposta continue sendo votada: |
— Justin Fay

3
@ishaaq, o problema é que (None)não é uma tupla, é None. Se você usar (None,), ele deve funcionar bem.
— Mark Lodato

2
Excelente! Você pode generalizar a configuração dos padrões com:Node.__new__.__defaults__= (None,) * len(Node._fields)
— ankostis 31/08/2015

142

Subclassifiquei nomeado duplo e substituí o __new__método:

from collections import namedtuple

class Node(namedtuple('Node', ['value', 'left', 'right'])):
    __slots__ = ()
    def __new__(cls, value, left=None, right=None):
        return super(Node, cls).__new__(cls, value, left, right)

Isso preserva uma hierarquia de tipos intuitiva, que a criação de uma função de fábrica disfarçada de classe não.


7
Isso pode precisar de propriedades de slots e campos para manter a eficiência de espaço de uma tupla nomeada.
— Pepijn

Por alguma razão, __new__não está sendo chamado quando _replaceé usado.

1
Por favor, dê uma olhada na resposta @ marc-lodato abaixo da qual o IMHO é uma solução melhor que essa.
— 23615 Justin Fay

1
Mas a resposta de @ Marc-lodato não fornece a capacidade de uma subclasse de ter diferentes padrões
— Jason S

1
@ JasonS, eu suspeito que, para uma subclasse ter padrões diferentes, poderia violar o LSP . No entanto, uma subclasse pode muito bem querer ter mais padrões. Em qualquer caso, seria para a subclasse usar o método justinfay , e a classe base ficaria bem com o método Marc .
— 21418 Alexey

94

Envolva-o em uma função.

NodeT = namedtuple('Node', 'val left right')

def Node(val, left=None, right=None):
  return NodeT(val, left, right)

15
Isso é inteligente e pode ser uma boa opção, mas também pode causar problemas ao quebrar isinstance(Node('val'), Node): agora isso gera uma exceção, em vez de retornar True. Embora um pouco mais detalhada, a resposta de @ justinfay (abaixo) preserva as informações da hierarquia de tipos corretamente, portanto, provavelmente é uma abordagem melhor se outras pessoas interagirem com instâncias do Node.
— Gabriel Grant

4
Eu gosto da brevidade desta resposta. Talvez a preocupação no comentário acima possa ser resolvida nomeando a função em def make_node(...):vez de fingir que é uma definição de classe. Dessa forma, os usuários não são tentados a verificar o polimorfismo de tipo na função, mas usam a própria definição de tupla.
— user1556435

Veja minha resposta para uma variação disso que não sofre com o uso isinstanceincorreto de pessoas enganosas .
— 22716 Elliot Cameron

70

No typing.NamedTuplePython 3.6.1+, você pode fornecer um valor padrão e uma anotação de tipo para um campo NamedTuple. Use typing.Anyse você precisar apenas do primeiro:

from typing import Any, NamedTuple


class Node(NamedTuple):
    val: Any
    left: 'Node' = None
    right: 'Node' = None

Uso:

>>> Node(1)
Node(val=1, left=None, right=None)
>>> n = Node(1)
>>> Node(2, left=n)
Node(val=2, left=Node(val=1, left=None, right=None), right=None)

Além disso, caso você precise de valores padrão e mutabilidade opcional, o Python 3.7 terá classes de dados (PEP 557) que podem em alguns casos (muitos?) Substituir os nomes nomeados.


Nota: uma das peculiaridades da especificação atual de anotações (expressões depois :para parâmetros e variáveis ​​e depois ->para funções) no Python é que elas são avaliadas no momento da definição * . Portanto, como "os nomes das classes são definidos após a execução de todo o corpo da classe", as anotações 'Node'nos campos da classe acima devem ser cadeias de caracteres para evitar NameError.

Esse tipo de dica de tipo é chamado "referência direta" ( [1] , [2] ) e, com o PEP 563, o Python 3.7+ terá uma __future__importação (a ser ativada por padrão no 4.0) que permitirá o uso de referências avançadas sem aspas, adiando sua avaliação.

* Somente anotações de variáveis ​​locais AFAICT não são avaliadas em tempo de execução. (fonte: PEP 526 )


4
Essa parece ser a solução mais limpa para os usuários 3.6.1+. Observe que este exemplo é (um pouco) confuso como a dica de tipo para os campos lefte right(ou seja Node) é o mesmo tipo da classe que está sendo definida e, portanto, deve ser escrito como seqüências de caracteres.
— 101 de

1
@ 101, obrigado, adicionei uma nota sobre isso à resposta.
— Monge-tempo

2
Qual é o analógico para o idioma my_list: List[T] = None self.my_list = my_list if my_list is not None else []? Não podemos usar parâmetros padrão como este?
— Weberc2

@ weberc2 Ótima pergunta! Não tenho certeza se esta solução alternativa para def mutável. valores é possível com typing.NamedTuple. Mas com classes de dados você pode usar Field objetos com um default_factoryattr. para isso, substituindo seu idioma por my_list: List[T] = field(default_factory=list).
— monk-time.

20

Este é um exemplo direto dos documentos :

Os valores padrão podem ser implementados usando _replace () para personalizar uma instância de protótipo:

>>> Account = namedtuple('Account', 'owner balance transaction_count')
>>> default_account = Account('<owner name>', 0.0, 0)
>>> johns_account = default_account._replace(owner='John')
>>> janes_account = default_account._replace(owner='Jane')

Portanto, o exemplo do OP seria:

from collections import namedtuple
Node = namedtuple('Node', 'val left right')
default_node = Node(None, None, None)
example = default_node._replace(val="whut")

No entanto, gosto mais de outras respostas dadas aqui. Eu só queria adicionar isso para completar.


2
+1. É muito estranho que eles decidiram ir com um _método (que basicamente significa um privado) para algo como replaceo que parece bastante útil ..
— sasuke

@ Sasuke - eu queria saber isso também. Já é um pouco estranho que você defina os elementos com uma sequência separada por espaço em vez de *args. Pode ser que tenha sido adicionado ao idioma antes de muitas dessas coisas serem padronizadas.
— precisa saber é o seguinte

12
O _prefixo é evitar colidir com os nomes dos campos de tupla definidos pelo usuário (citação relevante do documento: "Qualquer identificador Python válido pode ser usado para um nome de campo, exceto para nomes que começam com um sublinhado."). Quanto à string separada por espaço, acho que é apenas para salvar algumas teclas (e você pode passar uma sequência de strings, se preferir).
— Søren Løvborg 9/09/15

1
Ah, sim, eu esqueci que você acessa os elementos da tupla nomeada como atributos, então _faz muito sentido.
— Tim Tisdall

2
Sua solução é simples e a melhor. O resto é IMHO bastante feio. Eu faria apenas uma pequena alteração. Em vez de default_node, eu preferiria o node_default porque ele melhora a experiência com o IntelliSense. No caso de você começar nó de digitação você recebeu tudo que você precisa :)
— Pavel Hanpari

19

Não tenho certeza se existe uma maneira fácil apenas com o duplo nomeado embutido. Há um bom módulo chamado recordtype que possui esta funcionalidade:

>>> from recordtype import recordtype
>>> Node = recordtype('Node', [('val', None), ('left', None), ('right', None)])
>>> Node(3)
Node(val=3, left=None, right=None)
>>> Node(3, 'L')
Node(val=3, left=L, right=None)

2
Ah, não é possível usar um pacote de terceiros, embora recordtypecertamente pareça interessante para trabalhos futuros. 1
— sasuke

O módulo é bastante pequeno e possui apenas um arquivo, portanto você sempre pode adicioná-lo ao seu projeto.
— jterrace

É justo, embora eu espere mais um tempo para uma solução de tupla pura nomeada, se houver uma lá fora antes de marcar isso como aceito! :)
— sasuke

Python puro concordou seria bom, mas eu não acho que há uma :(
— jterrace

3
Só para observar que recordtypeé mutável, enquanto que namedtuplenão é. Isso pode importar se você deseja que o objeto seja lavável (o que eu acho que não, desde que começou como uma classe).
— bavaza

14

Aqui está uma versão mais compacta inspirada na resposta de justinfay:

from collections import namedtuple
from functools import partial

Node = namedtuple('Node', ('val left right'))
Node.__new__ = partial(Node.__new__, left=None, right=None)

7
Cuidado que Node(1, 2)não funciona com esta receita, mas funciona na resposta do @ justinfay. Caso contrário, é bastante bacana (+1).
— jorgeca

12

No python3.7 +, há um novo argumento padrão = palavra-chave.

os padrões podem ser Noneou iteráveis ​​dos valores padrão. Como os campos com um valor padrão devem vir após qualquer campo sem um padrão, os padrões são aplicados aos parâmetros mais à direita. Por exemplo, se os nomes de campo forem ['x', 'y', 'z']e os padrões forem (1, 2), xserá um argumento necessário, yserá o padrão 1e zo padrão será 2.

Exemplo de uso:

$ ./python
Python 3.7.0b1+ (heads/3.7:4d65430, Feb  1 2018, 09:28:35) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import namedtuple
>>> nt = namedtuple('nt', ('a', 'b', 'c'), defaults=(1, 2))
>>> nt(0)
nt(a=0, b=1, c=2)
>>> nt(0, 3)  
nt(a=0, b=3, c=2)
>>> nt(0, c=3)
nt(a=0, b=1, c=3)

7

Curto, simples e não leva as pessoas a usar isinstanceindevidamente:

class Node(namedtuple('Node', ('val', 'left', 'right'))):
    @classmethod
    def make(cls, val, left=None, right=None):
        return cls(val, left, right)

# Example
x = Node.make(3)
x._replace(right=Node.make(4))

5

Um exemplo levemente estendido para inicializar todos os argumentos ausentes com None:

from collections import namedtuple

class Node(namedtuple('Node', ['value', 'left', 'right'])):
    __slots__ = ()
    def __new__(cls, *args, **kwargs):
        # initialize missing kwargs with None
        all_kwargs = {key: kwargs.get(key) for key in cls._fields}
        return super(Node, cls).__new__(cls, *args, **all_kwargs)

5

Python 3.7: introdução de defaultsparam na definição de nome nomeado.

Exemplo, como mostrado na documentação:

>>> Account = namedtuple('Account', ['type', 'balance'], defaults=[0])
>>> Account._fields_defaults
{'balance': 0}
>>> Account('premium')
Account(type='premium', balance=0)

Leia mais aqui .


4

Você também pode usar isso:

import inspect

def namedtuple_with_defaults(type, default_value=None, **kwargs):
    args_list = inspect.getargspec(type.__new__).args[1:]
    params = dict([(x, default_value) for x in args_list])
    params.update(kwargs)

    return type(**params)

Isso basicamente oferece a possibilidade de construir qualquer tupla nomeada com um valor padrão e substituir apenas os parâmetros necessários, por exemplo:

import collections

Point = collections.namedtuple("Point", ["x", "y"])
namedtuple_with_defaults(Point)
>>> Point(x=None, y=None)

namedtuple_with_defaults(Point, x=1)
>>> Point(x=1, y=None)

4

Combinando abordagens de @Denis e @Mark:

from collections import namedtuple
import inspect

class Node(namedtuple('Node', 'left right val')):
    __slots__ = ()
    def __new__(cls, *args, **kwargs):
        args_list = inspect.getargspec(super(Node, cls).__new__).args[len(args)+1:]
        params = {key: kwargs.get(key) for key in args_list + kwargs.keys()}
        return super(Node, cls).__new__(cls, *args, **params) 

Isso deve apoiar a criação da tupla com argumentos posicionais e também com casos mistos. Casos de teste:

>>> print Node()
Node(left=None, right=None, val=None)

>>> print Node(1,2,3)
Node(left=1, right=2, val=3)

>>> print Node(1, right=2)
Node(left=1, right=2, val=None)

>>> print Node(1, right=2, val=100)
Node(left=1, right=2, val=100)

>>> print Node(left=1, right=2, val=100)
Node(left=1, right=2, val=100)

>>> print Node(left=1, right=2)
Node(left=1, right=2, val=None)

mas também suporta TypeError:

>>> Node(1, left=2)
TypeError: __new__() got multiple values for keyword argument 'left'

3

Acho esta versão mais fácil de ler:

from collections import namedtuple

def my_tuple(**kwargs):
    defaults = {
        'a': 2.0,
        'b': True,
        'c': "hello",
    }
    default_tuple = namedtuple('MY_TUPLE', ' '.join(defaults.keys()))(*defaults.values())
    return default_tuple._replace(**kwargs)

Isso não é tão eficiente quanto requer a criação do objeto duas vezes, mas você pode mudar isso definindo o duple padrão dentro do módulo e apenas fazendo com que a função faça a linha de substituição.


3

Como você está usando namedtuplecomo uma classe de dados, você deve estar ciente de que o python 3.7 apresentará um @dataclassdecorador para esse propósito - e, é claro, ele possui valores padrão.

Um exemplo dos documentos :

@dataclass
class C:
    a: int       # 'a' has no default value
    b: int = 0   # assign a default value for 'b'

Muito mais limpo, legível e utilizável que o hacking namedtuple. Não é difícil prever que o uso de namedtuples cairá com a adoção do 3.7.


2

Inspirado por esta resposta a uma pergunta diferente, aqui está a minha solução proposta com base em uma metaclasse e usando super(para manipular corretamente a subescalonagem futura). É bastante semelhante à resposta de justinfay .

from collections import namedtuple

NodeTuple = namedtuple("NodeTuple", ("val", "left", "right"))

class NodeMeta(type):
    def __call__(cls, val, left=None, right=None):
        return super(NodeMeta, cls).__call__(val, left, right)

class Node(NodeTuple, metaclass=NodeMeta):
    __slots__ = ()

Então:

>>> Node(1, Node(2, Node(4)),(Node(3, None, Node(5))))
Node(val=1, left=Node(val=2, left=Node(val=4, left=None, right=None), right=None), right=Node(val=3, left=None, right=Node(val=5, left=None, right=None)))

2

A resposta do jterrace para usar o recordtype é ótima, mas o autor da biblioteca recomenda o uso do projeto namedlist , que fornece implementações mutáveis ​​( namedlist) e imutáveis ​​( namedtuple).

from namedlist import namedtuple
>>> Node = namedtuple('Node', ['val', ('left', None), ('right', None)])
>>> Node(3)
Node(val=3, left=None, right=None)
>>> Node(3, 'L')
Node(val=3, left=L, right=None)

1

Aqui está uma resposta genérica curta e simples, com uma boa sintaxe para uma tupla nomeada com argumentos padrão:

import collections

def dnamedtuple(typename, field_names, **defaults):
    fields = sorted(field_names.split(), key=lambda x: x in defaults)
    T = collections.namedtuple(typename, ' '.join(fields))
    T.__new__.__defaults__ = tuple(defaults[field] for field in fields[-len(defaults):])
    return T

Uso:

Test = dnamedtuple('Test', 'one two three', two=2)
Test(1, 3)  # Test(one=1, three=3, two=2)

Minificado:

def dnamedtuple(tp, fs, **df):
    fs = sorted(fs.split(), key=df.__contains__)
    T = collections.namedtuple(tp, ' '.join(fs))
    T.__new__.__defaults__ = tuple(df[i] for i in fs[-len(df):])
    return T

0

Usando a NamedTupleclasse da minha Advanced Enum (aenum)biblioteca e usando a classsintaxe, isso é bastante simples:

from aenum import NamedTuple

class Node(NamedTuple):
    val = 0
    left = 1, 'previous Node', None
    right = 2, 'next Node', None

A única desvantagem potencial é a exigência de uma __doc__sequência para qualquer atributo com um valor padrão (é opcional para atributos simples). Em uso, parece com:

>>> Node()
Traceback (most recent call last):
  ...
TypeError: values not provided for field(s): val

>>> Node(3)
Node(val=3, left=None, right=None)

As vantagens que isso tem sobre justinfay's answer:

from collections import namedtuple

class Node(namedtuple('Node', ['value', 'left', 'right'])):
    __slots__ = ()
    def __new__(cls, value, left=None, right=None):
        return super(Node, cls).__new__(cls, value, left, right)

é simplicidade, além de ser metaclassbaseada em vez de execbaseada.


0

Outra solução:

import collections


def defaultargs(func, defaults):
    def wrapper(*args, **kwargs):
        for key, value in (x for x in defaults[len(args):] if len(x) == 2):
            kwargs.setdefault(key, value)
        return func(*args, **kwargs)
    return wrapper


def namedtuple(name, fields):
    NamedTuple = collections.namedtuple(name, [x[0] for x in fields])
    NamedTuple.__new__ = defaultargs(NamedTuple.__new__, [(NamedTuple,)] + fields)
    return NamedTuple

Uso:

>>> Node = namedtuple('Node', [
...     ('val',),
...     ('left', None),
...     ('right', None),
... ])
__main__.Node

>>> Node(1)
Node(val=1, left=None, right=None)

>>> Node(1, 2, right=3)
Node(val=1, left=2, right=3)

-1

Aqui está uma versão menos flexível, porém mais concisa, do invólucro de Mark Lodato: usa os campos e os padrões como um dicionário.

import collections
def namedtuple_with_defaults(typename, fields_dict):
    T = collections.namedtuple(typename, ' '.join(fields_dict.keys()))
    T.__new__.__defaults__ = tuple(fields_dict.values())
    return T

Exemplo:

In[1]: fields = {'val': 1, 'left': 2, 'right':3}

In[2]: Node = namedtuple_with_defaults('Node', fields)

In[3]: Node()
Out[3]: Node(val=1, left=2, right=3)

In[4]: Node(4,5,6)
Out[4]: Node(val=4, left=5, right=6)

In[5]: Node(val=10)
Out[5]: Node(val=10, left=2, right=3)

4
dictnão tem garantia de encomenda.
— Ethan Furman
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.