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"? :)

sasuke
fonte
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)
Mark Lodato
fonte
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.

Justin Fay
fonte
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)
Ignacio Vazquez-Abrams
fonte
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 )

tempo monge
fonte
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.

Tim Tisdall
fonte
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)
jterrace
fonte
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)
Gustav Larsson
fonte
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)
Anthony Sottile
fonte
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))
Elliot Cameron
fonte
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)
Dennis Golomazov
fonte
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 .

Julian Camilleri
fonte
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)
acerisara
fonte
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'
teodor
fonte
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.

Dave31415
fonte
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.

P-Gn
fonte
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)))
Alexey
fonte
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)
nbarraille
fonte
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
Matthew D. Scholefield
fonte
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.

Ethan Furman
fonte
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)
sirex
fonte
-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)
Li-Wen Yip
fonte
4
dictnão tem garantia de encomenda.
Ethan Furman