Benoemde tuple en standaardwaarden voor optionele trefwoordargumenten

Ik probeer een vrij lange ‘data’-klasse om te zetten in een tuple met een naam. Mijn klas ziet er momenteel zo uit:

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

Na conversie naar namedtupleziet het er als volgt uit:

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

Maar er is hier een probleem. Mijn oorspronkelijke klas stond me toe om slechts een waarde door te geven en zorgde voor de standaardwaarde door standaardwaarden te gebruiken voor de genoemde / trefwoordargumenten. Iets als:

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

Maar dit werkt niet in het geval van mijn gerefactorde tuple met de naam, omdat het verwacht dat ik alle velden doorloop. Ik kan natuurlijk de voorkomens van Node(val)vervangen door Node(val, None, None)maar het is niet naar mijn zin.

Dus bestaat er een goede truc die mijn herschrijven succesvol kan maken zonder veel codecomplexiteit toe te voegen (metaprogrammering) of moet ik gewoon de pil doorslikken en doorgaan met “zoeken en vervangen”? 🙂


Antwoord 1, autoriteit 100%

Python 3.7

Gebruik de parameter defaults.

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

Of beter nog, gebruik de nieuwe dataclasses-bibliotheek, die veel leuker is dan nametuple.

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

Vóór Python 3.7

Stel Node.__new__.__defaults__in op de standaardwaarden.

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

Vóór Python 2.6

Stel Node.__new__.func_defaultsin op de standaardwaarden.

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

Bestellen

Als je in alle versies van Python minder standaardwaarden instelt dan er in de genoemde tuple staan, worden de standaardwaarden toegepast op de meest rechtse parameters. Hierdoor kunt u enkele argumenten als vereiste argumenten behouden.

>>> 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 voor Python 2.6 tot 3.6

Hier is een wrapper voor u, waarmee u zelfs (optioneel) de standaardwaarden kunt instellen op iets anders dan None. Dit ondersteunt geen vereiste argumenten.

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

Voorbeeld:

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

Antwoord 2, autoriteit 23%

Ik heb namedtuple gesubklasseerd en de methode __new__overschreven:

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)

Hierdoor blijft een intuïtieve typehiërarchie behouden, wat niet het geval is bij het maken van een fabrieksfunctie, vermomd als een klasse.


Antwoord 3, autoriteit 15%

Wikkel het in een functie.

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

Antwoord 4, autoriteit 14%

Met typing.NamedTuplein Python 3.6.1+ kun je zowel een standaardwaarde als een typeannotatie opgeven voor een NamedTuple-veld. Gebruik typing.Anyals je alleen de eerste nodig hebt:

from typing import Any, NamedTuple
class Node(NamedTuple):
    val: Any
    left: 'Node' = None
    right: 'Node' = None

Gebruik:

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

Als je zowel standaardwaarden als optionele veranderlijkheid nodig hebt, heeft Python 3.7 ook dataklassen (PEP 557)die in sommige (veel?) gevallen namedtuples kunnen vervangen.


Sidenote: een eigenaardigheid van de huidige specificatie van annotaties(uitdrukkingen na :voor parameters en variabelen en na ->voor functies) in Python is dat ze worden geëvalueerd op het moment van de definitie*. Dus, aangezien “klassenamen worden gedefinieerd zodra de hele hoofdtekst van de klasse is uitgevoerd”, moeten de annotaties voor 'Node'in de klassenvelden hierboven strings zijn om NameError te voorkomen.

p>

Dit soort hints wordt “forward reference” genoemd ([ 1], [2] ), en met PEP 563gaat Python 3.7+ hebben een __future__import (standaard ingeschakeld in 4.0) die het mogelijk maakt om doorverwijzingen zonder aanhalingstekens te gebruiken, waardoor hun evaluatie wordt uitgesteld.

*AFAICT alleen lokale variabele annotaties worden niet geëvalueerd tijdens runtime. (bron: PEP 526)


Antwoord 5, autoriteit 3%

Dit is een voorbeeld rechtstreeks uit de documenten:

Standaardwaarden kunnen worden geïmplementeerd door _replace() te gebruiken om a . aan te passen
prototype instantie:

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

Dus het voorbeeld van de OP zou zijn:

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

Sommige van de andere antwoorden die hier worden gegeven, vind ik echter beter. Ik wilde dit even voor de volledigheid toevoegen.


Antwoord 6, autoriteit 3%

Ik weet niet zeker of er een gemakkelijke manier is met alleen de ingebouwde nametuple. Er is een mooie module genaamd recordtypedie deze functionaliteit heeft:

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

Antwoord 7, autoriteit 2%

Hier is een compactere versie, geïnspireerd op het antwoord van justinfay:

from collections import namedtuple
from functools import partial
Node = namedtuple('Node', ('val left right'))
Node.__new__ = partial(Node.__new__, left=None, right=None)

Antwoord 8, autoriteit 2%

In python3.7+ is er een gloednieuwe defaults=zoekwoordargument.

standaardwaardenkunnen Nonezijn of een herhaling van standaardwaarden. Aangezien velden met een standaardwaarde na alle velden zonder standaardwaarde moeten komen, worden de standaardwaardentoegepast op de meest rechtse parameters. Als de veldnamen bijvoorbeeld ['x', 'y', 'z']zijn en de standaardwaarden (1, 2), dan xis een verplicht argument, yis standaard 1en zis standaard 2.

Voorbeeld van gebruik:

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

Antwoord 9

Kort, eenvoudig en leidt er niet toe dat mensen isinstanceonjuist gebruiken:

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

Antwoord 10

Python 3.7: introductie van defaultsparam in namedtuple-definitie.

Voorbeeld zoals getoond in de documentatie:

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

Lees meer hier.


Antwoord 11

Een enigszins uitgebreid voorbeeld om alleontbrekende argumenten te initialiseren met 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)

12

U kunt dit ook gebruiken:

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)

Dit geeft u in principe de mogelijkheid om een ​​genoemde tuple met een standaardwaarde te construeren en alleen de parameters te negeren die u nodig hebt, bijvoorbeeld:

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)

13

Ik vind deze versie gemakkelijker om te lezen:

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)

Dit is niet zo efficiënt omdat het een creatie van het object twee keer vereist, maar dat zou u kunnen veranderen door de standaarddupel in de module te definiëren en alleen de functie te hebben, de vervanglijn vervangen.


14

Combinatie van benaderingen van @Denis en @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) 

Dat moet ondersteunen met het maken van de tuple met positionele argumenten en ook met gemengde gevallen.
Testcases:

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

Maar ook ondersteuning TypeError:

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

15

Aangezien u namedtupleals gegevensklasse gebruikt, moet u zich ervan bewust zijn dat Python 3.7 een @dataclassdecorator voor dit doel – en natuurlijk zal invoeren heeft standaardwaarden.

een voorbeeld van de documenten :

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

Veel reiniger, leesbaar en bruikbaar dan hacking namedtuple. Het is niet moeilijk om te voorspellen dat het gebruik van namedtuples zal dalen met de goedkeuring van 3.7.


16

Hier is een kort, eenvoudig generiek antwoord met een leuke syntaxis voor een genoemde tuple met standaardargumenten:

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

Gebruik:

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

Minialiseer:

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

17

Gebruik van de namedtupleKLASSE uit mijn Advanced Enum (aenum)bibliotheek en gebruik van de classsyntaxis, dit is vrij eenvoudig:

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

Het enige potentiaal nadeel is de vereiste voor een __doc__string voor elk kenmerk met een standaardwaarde (het is optioneel voor eenvoudige attributen). In gebruik lijkt het eruit:

>>> Node()
Traceback (most recent call last):
  ...
TypeError: values not provided for field(s): val
>>> Node(3)
Node(val=3, left=None, right=None)

De voordelen die dit heeft over 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)

is een eenvoud, evenals zijn metaclassgebaseerd in plaats van execgebaseerd.


18

een andere oplossing:

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

Gebruik:

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

19

Als u de mogelijkheid wilt behouden van het gebruik van typeannotatie, helaas is de zeer mooie oplossing door @ Mark-Lodato niet bruikbaar (het mislukt voor mij op het instellen van __defaults__).
Een alternatief gebruikt igns :

import attr
@attr.s
class Node(object):
    val: str = attr.ib()
    left: 'Node' = attr.ib(None)
    right: 'Node' = attr.ib(None)

Dit heeft:

  • Type annotaties
  • Nice __str__EN __repr__
  • aanpasbaar, omdat het een echte klas is
  • dezelfde implementatie met alle Python-versies

20

Hier is een minder flexibele, maar meer beknopte versie van Mark Lodato’s Wrapper: het duurt de velden en standaardinstellingen als een woordenboek.

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

Voorbeeld:

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)

Other episodes