namedtuple e valori predefiniti per argomenti di parole chiave opzionali


300

Sto provando a convertire una classe "dati" vuota longish in una tupla con nome. La mia classe attualmente si presenta così:

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

Dopo la conversione namedtuplesembra:

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

Ma c'è un problema qui. La mia classe originale mi ha permesso di passare solo un valore e si è preso cura del valore predefinito usando i valori predefiniti per gli argomenti nominati / parola chiave. Qualcosa di simile a:

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

Ma questo non funziona nel caso della mia tupla di nome refactored poiché mi aspetta che passi tutti i campi. Posso sostituire, naturalmente, le occorrenze di Node(val)a Node(val, None, None), ma non è di mio gradimento.

Quindi esiste un buon trucco che può rendere la mia riscrittura riuscita senza aggiungere molta complessità al codice (metaprogrammazione) o dovrei semplicemente ingoiare la pillola e procedere con la "ricerca e sostituzione"? :)


2
Perché vuoi fare questa conversione? Mi piace la tua Nodelezione originale così com'è. Perché convertire in tupla nominata?
steveha,

34
Volevo fare questa conversione perché la Nodeclasse corrente e le altre classi sono semplici oggetti valore titolare di dati con un mucchio di campi diversi ( Nodeè solo uno di questi). Queste dichiarazioni di classe non sono altro che un rumore di linea che IMHO ha quindi voluto eliminare. Perché mantenere qualcosa che non è richiesto? :)
sasuke,

Non hai alcuna funzione di metodo nelle tue classi? Ad esempio, non hai un .debug_print()metodo che cammina l'albero e lo stampa?
steveha,

2
Certo che lo faccio, ma è per la BinaryTreeclasse. Nodee altri detentori di dati non richiedono metodi così speciali, specialmente se le tuple nominate hanno un aspetto decente __str__e __repr__rappresentativo. :)
sasuke,

Va bene, sembra ragionevole. E penso che Ignacio Vazquez-Abrams ti abbia dato la risposta: usa una funzione che fa i valori di default per il tuo nodo.
Steveha,

Risposte:


532

Python 3.7

Utilizzare il parametro predefinito .

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

O meglio ancora, usa la nuova libreria dataclasses , che è molto più bella di namedtuple.

>>> 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)

Prima di Python 3.7

Impostare Node.__new__.__defaults__sui valori predefiniti.

>>> 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)

Prima di Python 2.6

Impostare Node.__new__.func_defaultssui valori predefiniti.

>>> 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)

Ordine

In tutte le versioni di Python, se si impostano meno valori predefiniti di quelli presenti nella namedtuple, i valori predefiniti vengono applicati ai parametri più a destra. Ciò consente di conservare alcuni argomenti come argomenti richiesti.

>>> 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 per Python da 2.6 a 3.6

Ecco un wrapper per te, che ti consente anche (facoltativamente) di impostare i valori predefiniti su qualcosa di diverso da None. Questo non supporta gli argomenti richiesti.

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

Esempio:

>>> 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
Vediamo ... il tuo one-liner: a) è la risposta più breve / semplice, b) conserva l'efficienza dello spazio, c) non si rompe isinstance... tutti i pro, nessun contro ... peccato che tu fossi un po 'in ritardo per la festa. Questa è la risposta migliore
Gerrat,

1
Un problema con la versione wrapper: a differenza di builtin collections.namedtuple, questa versione non è selezionabile / serializzabile multiprocesso se def () è incluso in un modulo diverso.
Michael Scott Cuthbert,

2
Ho dato a questa risposta un voto in quanto è preferibile alla mia. È un peccato però che la mia risposta continui a essere votata: |
Justin Fay,

3
@ishaaq, il problema è che (None)non è una tupla, lo è None. Se si utilizza (None,)invece, dovrebbe funzionare bene.
Mark Lodato,

2
Eccellente! Puoi generalizzare l'impostazione delle impostazioni predefinite con:Node.__new__.__defaults__= (None,) * len(Node._fields)
ankostis

142

Ho eseguito la sottoclasse nametuple e ho annullato il __new__metodo:

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)

Ciò preserva una gerarchia di tipi intuitiva, a cui la creazione di una funzione di fabbrica mascherata da classe non lo è.


7
Ciò potrebbe richiedere proprietà di slot e campi per mantenere l'efficienza dello spazio di una tupla denominata.
Pepijn,

Per qualche motivo, __new__non viene chiamato quando _replaceviene utilizzato.

1
Dai un'occhiata alla risposta @ marc-lodato sotto la quale IMHO è una soluzione migliore di questa.
Justin Fay,

1
ma la risposta di @ marc-lodato non fornisce la possibilità per una sottoclasse di avere impostazioni predefinite diverse
Jason S

1
@JasonS, sospetto che una sottoclasse con impostazioni predefinite diverse possa violare l' LSP . Tuttavia, una sottoclasse potrebbe voler avere più valori predefiniti. In ogni caso, sarebbe per la sottoclasse utilizzare il metodo justinfay e la classe base andrebbe bene con il metodo di Marc .
Alexey,

94

Avvolgilo in una funzione.

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

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

15
Questo è intelligente e può essere una buona opzione, ma può anche causare problemi rompendo isinstance(Node('val'), Node): ora genererà un'eccezione, piuttosto che restituire True. Sebbene un po 'più dettagliata, la risposta di @ justinfay (sotto) conserva correttamente le informazioni sulla gerarchia dei tipi, quindi è probabilmente un approccio migliore se altri interagiranno con le istanze di Node.
Gabriel Grant,

4
Mi piace la brevità di questa risposta. Forse la preoccupazione nel commento sopra può essere affrontata nominando la funzione def make_node(...):piuttosto che fingere che sia una definizione di classe. In questo modo gli utenti non sono tentati di verificare la presenza di polimorfismi di tipo sulla funzione, ma usano la definizione di tupla stessa.
user1556435

Vedi la mia risposta per una variazione di questo che non soffre di persone fuorvianti da usare in isinstancemodo errato.
Elliot Cameron,

70

Con typing.NamedTuplein Python 3.6.1+ puoi fornire sia un valore predefinito che un'annotazione di tipo a un campo NamedTuple. Utilizzare typing.Anyse è necessario solo il primo:

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)

Inoltre, nel caso abbiate bisogno sia di valori predefiniti che di mutabilità opzionale, Python 3.7 avrà classi di dati (PEP 557) che in alcuni (molti?) Casi possono sostituire le denominazioni.


Sidenote: una stranezza dell'attuale specifica delle annotazioni (espressioni dopo :per parametri e variabili e dopo ->per funzioni) in Python è che vengono valutate al momento della definizione * . Quindi, poiché "i nomi delle classi vengono definiti una volta che l'intero corpo della classe è stato eseguito", le annotazioni per 'Node'nei campi di classe sopra devono essere stringhe per evitare NameError.

Questo tipo di suggerimenti sul tipo è chiamato "riferimento in avanti" ( [1] , [2] ) e con PEP 563 Python 3.7+ avrà __future__un'importazione (che sarà abilitata di default in 4.0) che permetterà di usare i riferimenti in avanti senza virgolette, rinviando la loro valutazione.

* AFAICT solo le annotazioni delle variabili locali non vengono valutate in fase di esecuzione. (fonte: PEP 526 )


4
Questa sembra la soluzione più pulita per gli utenti 3.6.1+. Si noti che questo esempio è (leggermente) confuso come il suggerimento del tipo per i campi lefte right(cioè Node) è dello stesso tipo della classe che si sta definendo e quindi deve essere scritto come stringhe.
101

1
@ 101, grazie, ho aggiunto una nota al riguardo alla risposta.
tempo di Monaco

2
Qual è l'analogo per il linguaggio my_list: List[T] = None self.my_list = my_list if my_list is not None else []? Non possiamo usare parametri predefiniti come questo?
weberc2,

@ weberc2 Ottima domanda! Non sono sicuro se questa soluzione alternativa per def mutable. i valori sono possibili con typing.NamedTuple. Ma con le classi di dati è possibile utilizzare gli Field oggetti con un default_factoryattr. per questo, sostituendo il tuo linguaggio con my_list: List[T] = field(default_factory=list).
monk-time

20

Questo è un esempio direttamente dai documenti :

I valori predefiniti possono essere implementati utilizzando _replace () per personalizzare un'istanza del prototipo:

>>> 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')

Quindi, l'esempio del PO sarebbe:

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

Tuttavia, mi piacciono alcune delle altre risposte fornite qui meglio. Volevo solo aggiungere questo per completezza.


2
+1. È molto strano che abbiano deciso di optare per un _metodo (che in pratica significa uno privato) per qualcosa del genere replaceche sembra piuttosto utile ..
sasuke,

@sasuke - Me lo stavo chiedendo anche io. È già un po 'strano definire gli elementi con una stringa separata da spazio anziché *args. Può darsi che sia stato aggiunto alla lingua prima che molte di queste cose fossero standardizzate.
Tim Tisdall,

12
Il _prefisso è evitare di scontrarsi con i nomi dei campi di tupla definiti dall'utente (citazione del documento pertinente: "Qualsiasi identificatore Python valido può essere utilizzato per un nome campo tranne i nomi che iniziano con un carattere di sottolineatura."). Per quanto riguarda la stringa separata da spazio, penso che sia solo per salvare alcune sequenze di tasti (e puoi passare una sequenza di stringhe se preferisci).
Søren Løvborg,

1
Ah, sì, ho dimenticato che accedi agli elementi della tupla nominata come attributi, quindi allora _ha molto senso.
Tim Tisdall,

2
La tua soluzione è semplice e la migliore. Il resto è IMHO piuttosto brutto. Vorrei fare solo una piccola modifica. Invece di default_node preferirei node_default perché fa una migliore esperienza con IntelliSense. Nel caso in cui inizi a digitare il nodo hai ricevuto tutto ciò di cui hai bisogno :)
Pavel Hanpari

19

Non sono sicuro che ci sia un modo semplice con solo il namedtuple integrato. C'è un bel modulo chiamato recordtype che ha questa funzionalità:

>>> 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, non è possibile utilizzare un pacchetto di terze parti anche se recordtypesicuramente sembra interessante per il lavoro futuro. +1
sasuke

Il modulo è piuttosto piccolo e solo un singolo file, quindi puoi sempre aggiungerlo al tuo progetto.
jterrace,

Abbastanza giusto, anche se aspetterò ancora un po 'di tempo per una soluzione di tupla con nome puro, ce n'è uno là fuori prima di contrassegnare questo accettato! :)
sasuke,

Il puro pitone concordato sarebbe carino, ma non credo che ce ne sia uno :(
jterrace

3
Solo per notare che recordtypeè mutevole mentre namedtuplenon lo è. Questo potrebbe importare se vuoi che l'oggetto sia hash (cosa che immagino tu non abbia, visto che è iniziato come una classe).
bavaza,

14

Ecco una versione più compatta ispirata alla risposta di 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
Attenzione che Node(1, 2)non funziona con questa ricetta, ma funziona nella risposta di @ justinfay. Altrimenti, è abbastanza elegante (+1).
Jorgeca,

12

In python3.7 + c'è un nuovissimo default = argomento della parola chiave.

le impostazioni predefinite possono essere Noneo un iterabile di valori predefiniti. Poiché i campi con un valore predefinito devono venire dopo tutti i campi senza un valore predefinito, i valori predefiniti vengono applicati ai parametri più a destra. Ad esempio, se i nomi dei campi sono ['x', 'y', 'z']e i valori predefiniti lo sono (1, 2), allora xsarà un argomento obbligatorio, yverrà impostato 1e zimpostato automaticamente su 2.

Esempio di utilizzo:

$ ./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

Breve, semplice e non induce le persone a utilizzare in isinstancemodo improprio:

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

Un esempio leggermente esteso per inizializzare tutti gli argomenti mancanti con 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: introduzione di defaultsparam nella definizione di namedtuple.

Esempio come mostrato nella documentazione:

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

Leggi di più qui .


4

Puoi anche usare questo:

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)

Questo in pratica ti dà la possibilità di costruire qualsiasi tupla nominata con un valore predefinito e sovrascrivere solo i parametri necessari, ad esempio:

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

Combinazione di approcci di @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) 

Ciò dovrebbe supportare la creazione della tupla con argomenti posizionali e anche con casi misti. Casi test:

>>> 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)

ma supporta anche TypeError:

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

3

Trovo questa versione più facile da leggere:

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)

Questo non è così efficiente in quanto richiede la creazione dell'oggetto due volte, ma è possibile modificarlo definendo il duple predefinito all'interno del modulo e semplicemente facendo in modo che la funzione faccia la riga di sostituzione.


3

Dato che stai usando namedtuplecome classe di dati, dovresti essere consapevole che python 3.7 introdurrà un @dataclassdecoratore proprio per questo scopo - e ovviamente ha valori predefiniti.

Un esempio dai documenti :

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

Molto più pulito, leggibile e utilizzabile dell'hacking namedtuple. Non è difficile prevedere che l'utilizzo di namedtuples diminuirà con l'adozione di 3.7.


2

Ispirato da questa risposta a una domanda diversa, ecco la mia soluzione proposta basata su una metaclasse e sull'utilizzo super(per gestire correttamente il sottocalcolo futuro). È abbastanza simile alla risposta di 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__ = ()

Poi:

>>> 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

La risposta di jterrace all'utilizzo di recordtype è ottima, ma l'autore della libreria raccomanda di usare il suo progetto di lista dei nomi , che fornisce implementazioni mutabili ( namedlist) e immutabili ( 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

Ecco una risposta generica breve e semplice con una bella sintassi per una tupla nominata con argomenti predefiniti:

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)

minified:

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 la NamedTupleclasse dalla mia Advanced Enum (aenum)libreria e usando la classsintassi, questo è abbastanza semplice:

from aenum import NamedTuple

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

L'unico potenziale svantaggio è il requisito di una __doc__stringa per qualsiasi attributo con un valore predefinito (è facoltativo per gli attributi semplici). In uso sembra:

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

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

I vantaggi che questo ha oltre 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)

è semplicità, oltre ad essere metaclassbasata anziché execbasata.


0

Un'altra soluzione:

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

Ecco una versione meno flessibile, ma più concisa del wrapper di Mark Lodato: prende i campi e le impostazioni predefinite come dizionario.

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

Esempio:

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
dictnon ha alcuna garanzia di ordinazione.
Ethan Furman,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.