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

J’essaie de convertir une longue classe “data” creuse 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 en namedtuple cela ressemble à:

 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 ne transmettre qu’une valeur et de prendre en charge la valeur par défaut en utilisant les 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 reflet nommé tuple car il attend de moi que tous les champs passent. Je peux bien sûr remplacer les occurrences de Node(val) par Node(val, None, None) mais ce n’est pas à mon goût.

Existe-t-il un bon truc qui peut réussir ma ré-écriture sans append beaucoup de complexité de code (métaprogrammation) ou devrais-je simplement avaler la pilule et aller de l’avant avec la “recherche et remplacement”? 🙂

Définissez Node.__new__.__defaults__ (ou Node.__new__.func_defaults avant Python 2.6) sur 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) 

Vous pouvez également avoir des champs obligatoires en __defaults__ liste __defaults__ .

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

Emballage

Voici un joli 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 requirejs):

 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) 

J’ai sous-classé namedtuple et __new__ méthode __new__ :

 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 type intuitive, ce qui n’est pas le cas lors de la création d’une fonction d’usine déguisée en classe.

Enveloppez-le dans une fonction.

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

Avec typing.NamedTuple dans Python 3.6.1+, vous pouvez fournir une valeur par défaut et une annotation de type à un champ NamedTuple. Utilisez la typing.Any si vous avez seulement besoin 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, si vous avez besoin à la fois de valeurs par défaut et de mutabilité facultative, Python 3.7 aura des classes de données (PEP 557) qui, dans certains cas (nombreux?), Pourront remplacer les éléments nommés.


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

Ce type de conseil de type est appelé “forward reference” ( [1] , [2] ), et avec PEP 563, Python 3.7+ va avoir une importation __future____future__ par défaut en 4.0) qui permettra d’utiliser forward références sans guillemets, reportant leur évaluation.

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

Je ne suis pas sûr qu’il y ait un moyen facile avec seulement le groupe de nommage 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) 

Voici un exemple directement tiré 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('', 0.0, 0) >>> johns_account = default_account._replace(owner='John') >>> janes_account = default_account._replace(owner='Jane') 

Ainsi, l’exemple de l’OP 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 certaines des autres réponses données ici mieux. Je voulais juste append ceci pour être complet.

Voici une version plus compacte inspirée par 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) 

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) 

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 la possibilité de construire un tuple nommé avec une valeur par défaut et de ne remplacer que les parameters 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) 

Combinaison d’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 permettre de créer le tuple avec des arguments positionnels et aussi 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 aussi supporte TypeError:

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

Court, simple et ne conduit pas les utilisateurs à utiliser isinstance manière incorrecte:

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

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’object deux fois mais vous pouvez changer cela en définissant le duple par défaut dans le module et en ayant simplement la fonction pour remplacer la ligne.

Dans python3.7 + (au moment de la rédaction: pas encore publié), il y a un tout nouvel argument defaults = mot-clé.

Les valeurs par défaut peuvent être None ou une valeur par défaut des valeurs par défaut. Comme les champs avec une valeur par défaut doivent se trouver après des champs sans valeur par défaut, les parameters par défaut sont appliqués aux parameters les plus à droite. Par exemple, si les noms de champ sont ['x', 'y', 'z'] et que les valeurs par défaut sont (1, 2) , alors x sera un argument obligatoire, y sera par défaut 1 et z par 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) 

Étant donné que vous utilisez namedtuple tant que classe de données, vous devez savoir que python 3.7 introduira un décorateur @dataclass à cet effet – et bien sûr, il a des valeurs par défaut.

Un exemple des docs :

 @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 Il n’est pas difficile de prédire que l’utilisation de namedtuple avec l’adoption de la version 3.7.

Inspiré par cette réponse à une question différente, voici ma solution proposée basée sur une métaclasse et utilisant super (pour gérer correctement les sous-versions futures). 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__ = () 

Alors:

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

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) 

En utilisant la classe NamedTuple de ma bibliothèque Advanced Enum (aenum) et en utilisant la syntaxe de class , ceci 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 la nécessité d’une chaîne __doc__ pour tout atsortingbut avec une valeur par défaut (c’est facultatif pour les atsortingbuts 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 basé sur la metaclass au lieu de la base exec .

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) 

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 

Python 3.7: introduction de param par defaults 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) 

Lire plus ici .