namedtuple y valores predeterminados para argumentos opcionales de palabras clave

300

Estoy tratando de convertir una clase de "datos" hueca alargada en una tupla con nombre. Mi clase actualmente se ve así:

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

Después de la conversión namedtuplese ve así:

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

Pero hay un problema aquí. Mi clase original me permitió pasar solo un valor y se encargó del valor predeterminado mediante el uso de valores predeterminados para los argumentos con nombre / palabra clave. Algo como:

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

Pero esto no funciona en el caso de mi tupla refactorizada, ya que espera que pase todos los campos. Por supuesto, puedo reemplazar las ocurrencias de Node(val)to Node(val, None, None)pero no es de mi agrado.

Entonces, ¿existe un buen truco que pueda hacer que mi reescritura sea exitosa sin agregar mucha complejidad de código (metaprogramación) o debería simplemente tragarme la píldora y continuar con la "búsqueda y reemplazo"? :)

sasuke
fuente
2
¿Por qué quieres hacer esta conversión? Me gusta tu Nodeclase original tal como es. ¿Por qué convertir a tupla nombrada?
steveha 05 de
34
Quería hacer esta conversión porque las Nodeclases actuales y otras son simples objetos de valor de titular de datos con un montón de campos diferentes ( Nodees solo uno de ellos). Estas declaraciones de clase no son más que ruido de línea en mi humilde opinión, por lo que quería recortarlas. ¿Por qué mantener algo que no se requiere? :)
sasuke
¿No tienes ninguna función de método en tus clases? ¿No tienes, por ejemplo, un .debug_print()método que recorra el árbol y lo imprima?
steveha 05 de
2
Claro que sí, pero eso es para la BinaryTreeclase. Nodey otros titulares de los datos no requieren este tipo de métodos especiales especialmente teniendo en cuenta que el nombre tuplas tienen una decente __str__y __repr__la representación. :)
sasuke
De acuerdo, parece razonable. Y creo que Ignacio Vázquez-Abrams le ha dado la respuesta: use una función que haga los valores predeterminados para su nodo.
steveha

Respuestas:

532

Python 3.7

Use el parámetro predeterminado .

>>> 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 mejor aún, use la nueva biblioteca de clases de datos , que es mucho más agradable que 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)

Antes de Python 3.7

Establecer Node.__new__.__defaults__a los valores predeterminados.

>>> 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 de Python 2.6

Establecer Node.__new__.func_defaultsa los valores predeterminados.

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

Orden

En todas las versiones de Python, si establece menos valores predeterminados que los que existen en la tupla nombrada, los valores predeterminados se aplican a los parámetros más a la derecha. Esto le permite mantener algunos argumentos como argumentos requeridos.

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

Envoltorio para Python 2.6 a 3.6

Aquí hay un contenedor para usted, que incluso le permite (opcionalmente) establecer los valores predeterminados en algo diferente None. Esto no es compatible con los argumentos requeridos.

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

Ejemplo:

>>> 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
fuente
22
Veamos ... su respuesta: a) es la respuesta más corta / más simple, b) conserva la eficiencia del espacio, c) no se rompe isinstance... todos los pros, sin contras ... lástima que llegaste un poco tarde a La fiesta. Esta es la mejor respuesta.
Gerrat
1
Un problema con la versión del contenedor: a diferencia de la colección incorporada. Namedtuple, esta versión no es seleccionable / multiproceso serializable si el def () está incluido en un módulo diferente.
Michael Scott Cuthbert
2
He dado una respuesta a esta respuesta, ya que es preferible a la mía. Sin embargo, es una pena que mi propia respuesta siga siendo votada: |
Justin Fay
3
@ishaaq, el problema es que (None)no es una tupla, lo es None. Si lo utiliza (None,), debería funcionar bien.
Mark Lodato
2
¡Excelente! Puede generalizar la configuración de los valores predeterminados con:Node.__new__.__defaults__= (None,) * len(Node._fields)
ankostis
142

Subclasifiqué namedtuple y anulé el __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)

Esto conserva una jerarquía de tipos intuitiva, que la creación de una función de fábrica disfrazada de clase no.

Justin Fay
fuente
77
Esto podría necesitar propiedades de ranuras y campos para mantener la eficiencia espacial de una tupla con nombre.
Pepijn
Por alguna razón, __new__no se llama cuando _replacese usa.
1
Por favor, eche un vistazo a la respuesta de @ marc-lodato a continuación, que en mi humilde opinión es una mejor solución que esta.
Justin Fay
1
pero la respuesta de @marc-lodato no proporciona la capacidad para que una subclase tenga valores predeterminados diferentes
Jason S
1
@JasonS, sospecho que para una subclase tener valores predeterminados diferentes podría violar el LSP . Sin embargo, una subclase puede querer tener más valores predeterminados. En cualquier caso, sería para la subclase usar el método de justinfay , y la clase base estaría bien con el método de Marc .
Alexey
94

Envuélvelo en una función.

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

def Node(val, left=None, right=None):
  return NodeT(val, left, right)
Ignacio Vazquez-Abrams
fuente
15
Esto es inteligente y puede ser una buena opción, pero también puede causar problemas al romperse isinstance(Node('val'), Node): ahora generará una excepción, en lugar de devolver True. Si bien es un poco más detallado, la respuesta de @ justinfay (a continuación) conserva la información de la jerarquía de tipos correctamente, por lo que probablemente sea un mejor enfoque si otros van a interactuar con instancias de Node.
Gabriel Grant
44
Me gusta la brevedad de esta respuesta. Quizás la preocupación en el comentario anterior se puede abordar nombrando la función en def make_node(...):lugar de pretender que es una definición de clase. De esa manera, los usuarios no se sienten tentados a verificar el tipo de polimorfismo en la función, sino que usan la definición de tupla misma.
user1556435
Vea mi respuesta para una variación de esto que no sufre de personas engañosas para usar isinstanceincorrectamente.
Elliot Cameron
70

Con typing.NamedTuplePython 3.6.1+ puede proporcionar tanto un valor predeterminado como una anotación de tipo a un campo NamedTuple. Úselo typing.Anysi solo necesita lo primero:

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)

Además, en caso de que necesite valores predeterminados y mutabilidad opcional, Python 3.7 tendrá clases de datos (PEP 557) que en algunos casos (¿muchos?) Pueden reemplazar a las tuplas con nombre.


Nota al margen: una peculiaridad de la especificación actual de anotaciones (expresiones después :para parámetros y variables y después ->para funciones) en Python es que se evalúan en el momento de la definición * . Entonces, dado que "los nombres de clase se definen una vez que se ha ejecutado todo el cuerpo de la clase", las anotaciones 'Node'en los campos de clase anteriores deben ser cadenas para evitar NameError.

Este tipo de sugerencias de tipo se denomina "referencia directa" ( [1] , [2] ), y con PEP 563 Python 3.7+ tendrá una __future__importación (que se habilitará por defecto en 4.0) que permitirá utilizar referencias directas sin comillas, posponiendo su evaluación.

* AFAICT solo las anotaciones de variables locales no se evalúan en tiempo de ejecución. (fuente: PEP 526 )

tiempo de monje
fuente
44
Esta parece ser la solución más limpia para los usuarios 3.6.1+. Tenga en cuenta que este ejemplo es (ligeramente) confuso como la sugerencia de tipo para los campos lefty right(es decir Node) es el mismo tipo que la clase que se está definiendo y, por lo tanto, debe escribirse como cadenas.
101
1
@ 101, gracias, agregué una nota sobre esto a la respuesta.
tiempo de monje
2
¿Cuál es el análogo para el idioma my_list: List[T] = None self.my_list = my_list if my_list is not None else []? ¿No podemos usar parámetros predeterminados como este?
weberc2
@ weberc2 ¡Gran pregunta! No estoy seguro de si esta solución para la definición mutable. los valores son posibles con typing.NamedTuple. Pero con las clases de datos puede usar Field objetos con un default_factoryatributo. para esto, reemplazando tu idioma con my_list: List[T] = field(default_factory=list).
tiempo de monje el
20

Este es un ejemplo directamente de los documentos :

Los valores predeterminados se pueden implementar utilizando _replace () para personalizar una instancia de 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')

Entonces, el ejemplo del OP sería:

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

Sin embargo, me gustan algunas de las otras respuestas dadas aquí mejor. Solo quería agregar esto para completar.

Tim Tisdall
fuente
2
+1. Es muy extraño que decidieron ir con un _método (que básicamente significa una privada) para algo así como replacelo que parece bastante útil ..
Sasuke
@ Sasuke: Me preguntaba eso también. Ya es un poco extraño que defina los elementos con una cadena separada por espacios en lugar de *args. Puede ser que se haya agregado al lenguaje antes de que muchas de esas cosas fueran estandarizadas.
Tim Tisdall
12
El _prefijo es evitar chocar con los nombres de los campos de tupla definidos por el usuario (cita relevante del documento: "Cualquier identificador válido de Python puede usarse para un nombre de campo, excepto los nombres que comienzan con un guión bajo"). En cuanto a la cadena separada por espacios, creo que es solo para guardar algunas pulsaciones de teclas (y puede pasar una secuencia de cadenas si lo prefiere).
Søren Løvborg
1
Ah, sí, olvidé que tienes acceso a los elementos de la tupla nombrada como atributos, por lo que _tiene mucho sentido.
Tim Tisdall
2
Su solución es simple y la mejor. El resto es en mi humilde opinión bastante feo. Solo haría un pequeño cambio. En lugar de default_node, preferiría node_default porque mejora la experiencia con IntelliSense. En caso de que comience a escribir el nodo, recibió todo lo que necesita :)
Pavel Hanpari
19

No estoy seguro de si hay una manera fácil con solo la tupla nombrada incorporada. Hay un buen módulo llamado tipo de registro que tiene esta funcionalidad:

>>> 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
fuente
2
Ah, no es posible usar un paquete de terceros, aunque recordtypeciertamente parece interesante para el trabajo futuro. +1
sasuke
El módulo es bastante pequeño y solo tiene un único archivo, por lo que siempre puede agregarlo a su proyecto.
jterrace
Es justo, aunque esperaré un poco más de tiempo para una solución de tupla con nombre puro, ¡hay una por ahí antes de marcar esto como aceptado! :)
sasuke
La pitón pura acordada sería agradable, pero no creo que haya una :(
jterrace
3
Solo para notar que recordtypees mutable mientras namedtupleque no lo es. Esto podría importar si desea que el objeto sea hashable (lo que supongo que no, ya que comenzó como una clase).
Bavaza
14

Aquí hay una versión más compacta inspirada en la respuesta 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
fuente
77
Tenga en cuenta que Node(1, 2)no funciona con esta receta, pero funciona en la respuesta de @ justinfay. De lo contrario, es bastante ingenioso (+1).
jorgeca
12

En python3.7 + hay un nuevo valor predeterminado = argumento de palabra clave.

los valores predeterminados pueden ser Noneo un iterable de valores predeterminados. Dado que los campos con un valor predeterminado deben aparecer después de cualquier campo sin un valor predeterminado, los valores predeterminados se aplican a los parámetros más a la derecha. Por ejemplo, si los nombres de campo son ['x', 'y', 'z']y los valores predeterminados son (1, 2), entonces xserá un argumento obligatorio, yse establecerá de forma predeterminada 1y zse establecerá de forma predeterminada 2.

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

Corto, simple y no hace que las personas lo usen isinstanceincorrectamente:

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

Un ejemplo ligeramente extendido para inicializar todos los argumentos faltantes 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)
Dennis Golomazov
fuente
5

Python 3.7: introducción de defaultsparam en la definición de tupla nombrada.

Ejemplo como se muestra en la documentación:

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

Lee más aquí .

Julian Camilleri
fuente
4

También puedes usar esto:

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)

Básicamente, esto le brinda la posibilidad de construir cualquier tupla con un valor predeterminado y anular solo los parámetros que necesita, por ejemplo:

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

Combinando enfoques de @Denis y @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) 

Eso debería apoyar la creación de la tupla con argumentos posicionales y también con casos mixtos. Casos de prueba:

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

pero también es compatible con TypeError:

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

Encuentro esta versión más fácil de leer:

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)

Esto no es tan eficiente, ya que requiere la creación del objeto dos veces, pero puede cambiarlo definiendo el duplicado predeterminado dentro del módulo y simplemente haciendo que la función haga la línea de reemplazo.

Dave31415
fuente
3

Como está utilizando namedtupleuna clase de datos, debe tener en cuenta que python 3.7 presentará un @dataclassdecorador para este mismo propósito, y por supuesto tiene valores predeterminados.

Un ejemplo de los documentos :

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

Mucho más limpio, legible y utilizable que la piratería namedtuple. No es difícil predecir que el uso de namedtuples disminuirá con la adopción de 3.7.

P-Gn
fuente
2

Inspirado por esta respuesta a una pregunta diferente, aquí está mi solución propuesta basada en una metaclase y usando super(para manejar correctamente las subcalificaciones futuras). Es bastante similar a la respuesta 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__ = ()

Luego:

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

La respuesta de jterrace para usar el tipo de registro es excelente, pero el autor de la biblioteca recomienda usar su proyecto de lista de nombres , que proporciona implementaciones tanto mutable ( namedlist) como inmutable ( 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
fuente
1

Aquí hay una respuesta genérica corta y simple con una buena sintaxis para una tupla nombrada con argumentos predeterminados:

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
Matthew D. Scholefield
fuente
0

Usando la NamedTupleclase de mi Advanced Enum (aenum)biblioteca, y usando la classsintaxis, esto es bastante simple:

from aenum import NamedTuple

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

El único inconveniente potencial es el requisito de una __doc__cadena para cualquier atributo con un valor predeterminado (es opcional para atributos simples). En uso se ve así:

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

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

Las ventajas que esto tiene 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)

es simplicidad, además de estar metaclassbasado en lugar de estar execbasado.

Ethan Furman
fuente
0

Otra solución:

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

Aquí hay una versión menos flexible pero más concisa del contenedor de Mark Lodato: toma los campos y los valores predeterminados como un diccionario.

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

Ejemplo:

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
fuente
44
dictNo tiene garantía de pedido.
Ethan Furman