web-dev-qa-db-fra.com

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

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, il se présente comme suit:

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

Mais il y a un problème ici. Ma classe d'origine me permettait de ne transmettre qu'une valeur et prenait soin de la valeur par défaut en utilisant des valeurs par défaut pour les arguments named/keyword. Quelque chose comme:

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

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

Donc, existe-t-il un bon truc qui peut rendre ma réécriture réussie sans ajouter beaucoup de complexité de code (métaprogrammation) ou devrais-je simplement avaler la pilule et poursuivre avec la "recherche et remplacement"? :)

216
sasuke

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)

Avant Python 3.7

Définissez Node.__new__.__defaults__ aux 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_defaults aux 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 en existe dans la syntaxe nommée, les valeurs par défaut sont appliquées aux paramètres les plus à droite. Cela vous permet de conserver certains arguments en tant qu'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 for Python 2.6 à 3.6

Voici un emballage pour vous, qui vous permet même de définir (éventuellement) les valeurs par défaut sur autre chose que None. Cela ne supporte pas 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)
387
Mark Lodato

J'ai sous-classé namedtuple et écrasé la 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 types intuitive, contrairement à la création d'une fonction fabrique déguisée en classe.

133
justinfay

Enveloppez-le dans une fonction.

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

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

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 typing.Any si 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, si vous avez besoin à la fois de valeurs par défaut et d'une mutabilité optionnelle, Python 3.7 aura des classes data (PEP 557) qui peuvent, dans certains cas (beaucoup?), Remplacer les éléments nommés .


Remarque: une particularité de la spécification actuelle de annotations (expressions après : pour les paramètres et les variables et après -> pour les fonctions) est qu’elles sont évaluées au moment de la définition.*. Ainsi, puisque "les noms de classe sont définis une fois que tout le corps 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 conseils de type s’appelle "référence en aval" ( [1] , [2] ), et avec PEP 563 Python 3.7+ va avoir une importation __future__ (à être activé par défaut en 4.0) qui permettra d’utiliser des références en aval sans guillemets, en reportant leur évaluation.

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

45
monk-time

Je ne suis pas sûr qu'il existe un moyen simple d'utiliser uniquement le nom baptisé. Il y a un module Nice 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)
19
jterrace

Ceci est un exemple directement tiré de la documentation :

Les valeurs par défaut peuvent être implémentées en utilisant _replace () pour personnaliser un fichier 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. 

19
Tim Tisdall

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)
13
Gustav Larsson

En python3.7 +, il existe un nouvel argument de mot clé defaults = .

defaults peut être None ou un itérable de valeurs par défaut. Comme les champs avec une valeur par défaut doivent précéder tous les champs sans valeur par défaut, les defaults sont appliqués aux paramètres les plus à droite. Par exemple, si les noms de champ sont ['x', 'y', 'z'] et les valeurs par défaut sont (1, 2), alors x sera un argument obligatoire, y sera par défaut à 1 et z sera 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)
8
Anthony Sottile

Bref, simple et ne conduit pas les gens à utiliser isinstance de 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))
5
Elliot Cameron

Un exemple légèrement étendu pour initialiser tous 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
Dennis Golomazov

Vous pouvez aussi 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
acerisara

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

4
Julian Camilleri

Combinaison des 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 de position et également avec des cas mixtes .

>>> 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 supporte aussi TypeError:

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

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 car cela nécessite la création de l'objet deux fois, mais vous pouvez le changer en définissant le doublon par défaut dans le module et en laissant simplement la fonction faire la ligne de remplacement. 

3
Dave31415

Puisque vous utilisez namedtuple en tant que classe de données, vous devez savoir que python 3.7 introduira un décorateur @dataclass à cette fin - et bien sûr, il a des valeurs par défaut. 

Un exemple tiré 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édire que l’utilisation de namedtuples diminuera avec l’adoption de 3.7.

2
P-Gn

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-analyses futures). C'est assez similaire à 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)))
1
Alexey

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

Voici une réponse simple et générique avec une syntaxe de Nice 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)

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

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

En utilisant la classe NamedTuple de ma Advanced Enum (aenum) library, et en utilisant la syntaxe class, c'est assez simple:

from aenum import NamedTuple

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

L'inconvénient potentiel est la nécessité d'une chaîne __doc__ pour tout attribut avec une valeur par défaut (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 basé sur metaclass au lieu de exec.

0
Ethan Furman