namedtuple et valeurs par défaut pour les arguments de mots clés facultatifs


300

J'essaie de convertir une classe de "données" creuse assez longue en un tuple nommé. Ma classe ressemble actuellement à ceci:

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

Après la conversion, namedtupleil ressemble à ceci:

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

Mais il y a un problème ici. Ma classe d'origine m'a permis de passer juste une valeur et a pris soin de la valeur par défaut en utilisant des valeurs par défaut pour les arguments nommés / mot-clé. Quelque chose comme:

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

Mais cela ne fonctionne pas dans le cas de mon tuple nommé refactorisé car il s'attend à ce que je passe tous les champs. Je peux bien sûr remplacer les occurrences de Node(val)to Node(val, None, None)mais ce n'est pas à mon goût.

Existe-t-il donc une bonne astuce qui peut réussir ma réécriture sans ajouter beaucoup de complexité de code (métaprogrammation) ou devrais-je simplement avaler la pilule et continuer avec la «recherche et remplacement»? :)


2
Pourquoi voulez-vous faire cette conversion? J'aime votre cours d'origine tel Nodequ'il est. Pourquoi convertir en tuple nommé?
steveha

34
Je voulais faire cette conversion parce que les Nodeclasses actuelles et autres sont de simples objets de valeur contenant des données avec un tas de champs différents ( Nodec'est juste l'un d'entre eux). Ces déclarations de classe ne sont rien de plus que le bruit de ligne à mon humble avis et ont donc voulu les supprimer. Pourquoi entretenir quelque chose qui n'est pas nécessaire? :)
sasuke

Vous n'avez aucune fonction de méthode sur vos classes? Vous n'avez pas, par exemple, une .debug_print()méthode qui parcourt l'arbre et l'imprime?
steveha

2
Bien sûr que oui, mais c'est pour la BinaryTreeclasse. Nodeet d'autres détenteurs de données ne nécessitent pas de telles méthodes spéciales, étant donné que les tuples nommés ont une représentation __str__et une __repr__représentation décentes . :)
sasuke

D'accord, cela semble raisonnable. Et je pense qu'Ignacio Vazquez-Abrams vous a donné la réponse: utilisez une fonction qui fait les valeurs par défaut pour votre nœud.
steveha

Réponses:


532

Python 3.7

Utilisez le paramètre par défaut .

>>> 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 mieux encore, utilisez la nouvelle bibliothèque de classes de données , qui est beaucoup plus agréable 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)

Avant Python 3.7

Définissez Node.__new__.__defaults__les valeurs par défaut.

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

Avant Python 2.6

Définissez Node.__new__.func_defaultsles valeurs par défaut.

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

Ordre

Dans toutes les versions de Python, si vous définissez moins de valeurs par défaut qu'il n'en existe dans le tuple nommé, les valeurs par défaut sont appliquées aux paramètres les plus à droite. Cela vous permet de conserver certains arguments comme arguments requis.

>>> 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 pour Python 2.6 à 3.6

Voici un wrapper pour vous, qui vous permet même (facultativement) de définir les valeurs par défaut sur autre chose que None. Cela ne prend pas en charge les arguments requis.

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

Exemple:

>>> 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
Voyons ... votre one-liner: a) est la réponse la plus courte / la plus simple, b) préserve l'efficacité de l'espace, c) ne casse pas isinstance... tous les avantages, pas les inconvénients ... dommage que vous ayez été un peu en retard la fête. C'est la meilleure réponse.
Gerrat

1
Un problème avec la version wrapper: contrairement aux collections intégrées. Namedtuple, cette version n'est pas pickleable / multiprocess sérialisable si le def () est inclus dans un module différent.
Michael Scott Cuthbert

2
J'ai donné une réponse positive à cette réponse car elle est préférable à la mienne. C'est dommage cependant que ma propre réponse continue à être votée: |
Justin Fay

3
@ishaaq, le problème est que ce (None)n'est pas un tuple, c'est None. Si vous utilisez à la (None,)place, cela devrait fonctionner correctement.
Mark Lodato

2
Excellent! Vous pouvez généraliser la définition des valeurs par défaut avec:Node.__new__.__defaults__= (None,) * len(Node._fields)
ankostis

142

J'ai sous-classé namedtuple et remplacé la __new__méthode:

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)

Cela préserve une hiérarchie de types intuitive, contrairement à la création d'une fonction d'usine déguisée en classe.


7
Cela peut nécessiter des propriétés d'emplacements et de champs afin de maintenir l'efficacité de l'espace d'un tuple nommé.
Pepijn

Pour une raison quelconque, __new__n'est pas appelé lorsqu'il _replaceest utilisé.

1
Veuillez jeter un œil à la réponse @ marc-lodato ci-dessous qui à mon humble avis est une meilleure solution que cela.
Justin Fay

1
mais la réponse de @ marc-lodato ne permet pas à une sous-classe d'avoir des valeurs par défaut différentes
Jason S

1
@JasonS, je soupçonne qu'une sous-classe d'avoir des valeurs par défaut différentes pourrait violer le LSP . Cependant, une sous-classe peut très bien vouloir avoir plus de valeurs par défaut. Dans tous les cas, il appartiendrait à la sous - classe d'utiliser la méthode de justinfay , et la classe de base conviendrait à la méthode de Marc .
Alexey

94

Enveloppez-le dans une fonction.

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

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

15
C'est intelligent, et peut être une bonne option, mais peut également causer des problèmes en se brisant isinstance(Node('val'), Node): il lèvera désormais une exception, plutôt que de renvoyer True. Bien que plus explicite, la réponse de @ justinfay (ci-dessous) préserve correctement les informations de hiérarchie de types, c'est donc probablement une meilleure approche si d'autres vont interagir avec les instances de Node.
Gabriel Grant

4
J'aime la brièveté de cette réponse. Peut-être que la préoccupation dans le commentaire ci-dessus peut être traitée en nommant la fonction def make_node(...):plutôt qu'en prétendant qu'il s'agit d'une définition de classe. De cette façon, les utilisateurs ne sont pas tentés de vérifier le polymorphisme de type sur la fonction mais utilisent la définition de tuple elle-même.
user1556435

Voir ma réponse pour une variation de cela qui ne souffre pas d'induire en erreur les gens pour une utilisation isinstanceincorrecte.
Elliot Cameron

70

Avec typing.NamedTuplePython 3.6.1+, vous pouvez fournir à la fois une valeur par défaut et une annotation de type à un champ NamedTuple. À utiliser typing.Anysi vous n'avez besoin que de l'ancien:

from typing import Any, NamedTuple


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

Usage:

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

De plus, au cas où vous auriez besoin à la fois de valeurs par défaut et d'une mutabilité facultative, Python 3.7 va avoir des classes de données (PEP 557) qui peuvent dans certains (beaucoup?) Remplacer les nommés.


Sidenote: une particularité de la spécification actuelle des annotations (expressions après :pour les paramètres et les variables et après ->pour les fonctions) en Python est qu'elles sont évaluées au moment de la définition * . Ainsi, puisque "les noms de classe sont définis une fois que le corps entier de la classe a été exécuté", les annotations 'Node'dans les champs de classe ci-dessus doivent être des chaînes pour éviter NameError.

Ce type d'indices de type est appelé "référence avant" ( [1] , [2] ), et avec PEP 563 Python 3.7+ va avoir une __future__importation (à activer par défaut dans 4.0) qui permettra d'utiliser des références avant sans devis, reportant leur évaluation.

* AFAICT seules les annotations de variables locales ne sont pas évaluées au moment de l'exécution. (source: PEP 526 )


4
Cela semble être la solution la plus propre pour les utilisateurs 3.6.1+. Notez que cet exemple est (légèrement) déroutant car l'indice de type pour les champs leftet right(c'est-à-dire Node) est le même type que la classe en cours de définition et doit donc être écrit sous forme de chaînes.
101

1
@ 101, merci, j'ai ajouté une note à ce sujet à la réponse.
Monk-Time

2
Quel est l'analogue de l'idiome my_list: List[T] = None self.my_list = my_list if my_list is not None else []? Ne pouvons-nous pas utiliser des paramètres par défaut comme celui-ci?
weberc2

@ weberc2 Grande question! Je ne sais pas si cette solution de contournement pour def mutable. est possible avec typing.NamedTuple. Mais avec les classes de données, vous pouvez utiliser des Field objets avec un default_factoryattr. pour cela, en remplaçant votre idiome par my_list: List[T] = field(default_factory=list).
heure du moine

20

Voici un exemple directement issu de la documentation :

Les valeurs par défaut peuvent être implémentées en utilisant _replace () pour personnaliser une instance de prototype:

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

Ainsi, l'exemple du PO serait:

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

Cependant, j'aime mieux certaines des autres réponses données ici. Je voulais juste ajouter ceci pour être complet.


2
+1. C'est très étrange qu'ils aient décidé d'aller avec une _méthode (ce qui signifie fondamentalement une méthode privée) pour quelque chose replacequi semble assez utile ..
sasuke

@sasuke - Je me le demandais aussi. Il est déjà un peu étrange que vous définissiez les éléments avec une chaîne séparée par des espaces au lieu de *args. Il se peut simplement qu'il ait été ajouté à la langue avant que beaucoup de ces choses soient normalisées.
Tim Tisdall

12
Le _préfixe est d'éviter la collision avec les noms des champs de tuple définis par l'utilisateur (citation appropriée du document: "Tout identifiant Python valide peut être utilisé pour un nom de champ à l'exception des noms commençant par un trait de soulignement."). En ce qui concerne la chaîne séparée par des espaces, je pense que c'est juste pour enregistrer quelques frappes (et vous pouvez passer une séquence de chaînes si vous préférez).
Søren Løvborg

1
Ah, oui, j'ai oublié que vous accédez aux éléments du tuple nommé en tant qu'attributs, donc _cela a beaucoup de sens alors.
Tim Tisdall

2
Votre solution est simple et la meilleure. Le reste est à mon humble avis plutôt moche. Je ferais seulement un petit changement. Au lieu de default_node, je préférerais node_default car cela fait une meilleure expérience avec IntelliSense. Si vous commencez à taper node, vous avez reçu tout ce dont vous avez besoin :)
Pavel Hanpari

19

Je ne suis pas sûr qu'il existe un moyen simple de nommer le double intégré. Il y a un joli module appelé recordtype qui a cette fonctionnalité:

>>> 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, pas possible d'utiliser un paquet tiers mais recordtypecela semble certainement intéressant pour les travaux futurs. +1
sasuke

Le module est assez petit et ne contient qu'un seul fichier , vous pouvez donc toujours l'ajouter à votre projet.
jterrace

Très bien, même si j'attendrai un peu plus de temps pour une solution de tuple nommée pure, il y en a une avant de marquer cette acceptation! :)
sasuke

Un python pur convenu serait bien, mais je ne pense pas qu'il y en ait un :(
jterrace

3
Juste pour noter que recordtypec'est mutable alors qu'il namedtuplene l'est pas. Cela peut être important si vous voulez que l'objet soit lavable (ce qui, je suppose, ne l'est pas, car il a commencé en tant que classe).
bavaza le

14

Voici une version plus compacte inspirée de la réponse 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
Attention, cela Node(1, 2)ne fonctionne pas avec cette recette, mais fonctionne dans la réponse de @ justinfay. Sinon, c'est assez chouette (+1).
jorgeca

12

Dans python3.7 +, il y a un tout nouvel argument defaults = keyword.

les valeurs par défaut peuvent être Noneou un itérable de valeurs par défaut. Étant donné que les champs avec une valeur par défaut doivent venir après tous les champs sans valeur par défaut, les valeurs par défaut sont appliquées aux paramètres les plus à droite. Par exemple, si les noms de champs sont ['x', 'y', 'z']et les valeurs par défaut sont (1, 2), alors xsera un argument requis, ypar défaut à 1et zpar défaut à 2.

Exemple d'utilisation:

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

Court, simple et ne conduit pas les gens à isinstancemal utiliser :

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 exemple légèrement étendu pour initialiser tous les arguments manquants avec 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: introduction de defaultsparam dans la définition namedtuple.

Exemple comme indiqué dans la documentation:

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

Lisez plus ici .


4

Vous pouvez également utiliser ceci:

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)

Cela vous donne essentiellement la possibilité de construire n'importe quel tuple nommé avec une valeur par défaut et de remplacer uniquement les paramètres dont vous avez besoin, par exemple:

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

Combiner les approches de @Denis et @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) 

Cela devrait prendre en charge la création du tuple avec des arguments positionnels et également avec des cas mixtes. Cas de 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)

mais prend également en charge TypeError:

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

3

Je trouve cette version plus facile à lire:

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)

Ce n'est pas aussi efficace que cela nécessite la création de l'objet deux fois, mais vous pouvez changer cela en définissant le duple par défaut à l'intérieur du module et en demandant simplement à la fonction de faire la ligne de remplacement.


3

Puisque vous utilisez en namedtupletant que classe de données, vous devez savoir que python 3.7 introduira un @dataclassdécorateur à cet effet - et bien sûr, il a des valeurs par défaut.

Un exemple de la documentation :

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

Beaucoup plus propre, lisible et utilisable que le piratage namedtuple. Il n'est pas difficile de prévoir que l'utilisation de namedtuples diminuera avec l'adoption de 3.7.


2

Inspiré par cette réponse à une autre question, voici ma solution proposée basée sur une métaclasse et utilisant super(pour gérer correctement les futurs sous-calculs). C'est assez similaire à la réponse 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__ = ()

Ensuite:

>>> 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 réponse de jterrace à l'utilisation de recordtype est excellente, mais l'auteur de la bibliothèque recommande d'utiliser son projet namedlist , qui fournit à la fois des implémentations mutable ( namedlist) et immutable ( 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

Voici une réponse générique courte et simple avec une belle syntaxe pour un tuple nommé avec des arguments par défaut:

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

Usage:

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

Minifié:

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

En utilisant la NamedTupleclasse de ma Advanced Enum (aenum)bibliothèque et en utilisant la classsyntaxe, c'est assez simple:

from aenum import NamedTuple

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

Le seul inconvénient potentiel est l'exigence d'une __doc__chaîne pour tout attribut avec une valeur par défaut (c'est facultatif pour les attributs simples). En cours d'utilisation, il ressemble à:

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

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

Les avantages que cela a sur 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)

est la simplicité, tout en étant metaclassbasé au lieu de execbasé.


0

Une autre solution:

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

Usage:

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

Voici une version moins flexible, mais plus concise du wrapper de Mark Lodato: il prend les champs et les valeurs par défaut comme un dictionnaire.

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

Exemple:

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'a aucune garantie de commande.
Ethan Furman
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.