namedtuple и значения по умолчанию для необязательных аргументов ключевых слов

Я пытаюсь преобразовать класс longish hollow "data" в именованный кортеж. Мой класс В настоящее время выглядит так:

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

после преобразования в namedtuple выглядит так:

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

но здесь есть проблема. Мой исходный класс позволил мне передать только значение и позаботился о значении по умолчанию, используя значения по умолчанию для Аргументов named/keyword. Что-то вроде:

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

но это не сработает в случае моей рефакторингу именованный кортеж с он ожидает, что я пройду все поля. Я конечно могу заменить Node(val) до Node(val, None, None) но мне это не нравится.

Итак, существует ли хороший трюк, который может сделать мою перезапись успешной, не добавляя много сложности кода (метапрограммирование), или я должен просто проглотить таблетку и продолжить "поиск и замену"? :)

21 ответов


Set Node.__new__.__defaults__ (или Node.__new__.func_defaults перед Python 2.6) к значениям по умолчанию.

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

вы также можете иметь необходимые поля, сделав __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)

фантик

вот хорошая обертка для вас, которая даже позволяет вам (необязательно) установить значения по умолчанию на что-то другое, чем None. (Это не поддерживает требуемые аргументы.):

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

пример:

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

Я подкласс namedtuple и преодолел __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)

это сохраняет интуитивную иерархию типов, чего не делает создание фабричной функции, замаскированной под класс.


оберните его в функцию.

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

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

С typing.NamedTuple в Python 3.6.1+ вы можете указать как значение по умолчанию, так и аннотацию типа для поля NamedTuple. Использовать typing.Any Если вам нужно только первое:

from typing import Any, NamedTuple


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

использование:

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

кроме того, если вам нужны значения по умолчанию и необязательная изменчивость, Python 3.7 будет иметь классы данных (PEP 557) что может в некоторых (многих?) случаи заменяют namedtuples.


Sidenote: одна причуда течения спецификация аннотации (выражений после ключевого слова : для параметров и переменных и после -> для функций) в Python заключается в том, что они оцениваются во время определения*. Итак, поскольку "имена классов определяются после выполнения всего тела класса", аннотации для 'Node' в полях класса выше должны быть строки, чтобы избежать NameError.

этот вид подсказок типа называется "прямой ссылкой" ([1], [2]) и с PEP 563 Python 3.7+ будет иметь __future__ импорт (будет включен по умолчанию в 4.0), что позволит использовать прямые ссылки без кавычек, откладывая их оценку.

* afaict только локальные аннотации переменных не оцениваются во время выполнения. (источник: PEP 526)


Я не уверен, что есть простой способ только со встроенным namedtuple. Есть хороший модуль под названием recordtype который имеет эту функциональность:

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

Это пример прямо из docs:

значения по умолчанию могут быть реализованы с помощью _replace() для настройки экземпляр прототипа:

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

Итак, пример OP будет:

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

тем не менее, мне нравятся некоторые другие ответы, приведенные здесь лучше. Я просто хотел добавить для полноты картины.


вот более компактная версия, вдохновленная ответом justinfay:

from collections import namedtuple
from functools import partial

Node = namedtuple('Node', ('val left right'))
Node.__new__ = partial(Node.__new__, left=None, right=None)

в python3.7+ есть совершенно новый по умолчанию= аргумент сайта.

по умолчанию может быть None или iterable значений по умолчанию. Поскольку поля со значением по умолчанию должны быть после любых полей без значения по умолчанию,по умолчанию применяются к самым правым параметрам. Например, если имена полей ['x', 'y', 'z'] и по умолчанию (1, 2), потом x будет обязательным аргументом, y по умолчанию 1 и z по умолчанию 2.

пример использования:

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

немного расширенный пример для инициализации все отсутствуют аргументы с 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)

вы также можете использовать это:

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)

это в основном дает вам возможность построить любой именованный кортеж со значением по умолчанию, и переопределить только те параметры которые вам нужны, например:

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)

объединение подходов @Denis и @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) 

это должно поддерживать создание кортежа с позиционными аргументами, а также со смешанными случаями. Тесты:

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

но также поддержка TypeError:

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

короткий, простой и не приводит людей к использованию isinstance неправильно:

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

Python 3.7: введение defaults param в определении namedtuple.

например, как показано в документации:

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

подробнее здесь.


Я нахожу эту версию легче читать:

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)

Это не так эффективно, как требует создания объекта дважды, но вы можете изменить это, определив дубль по умолчанию внутри модуля и просто имея функцию заменить строку.


если вы используете namedtuple как класс данных, вы должны знать, что python 3.7 представит @dataclass декоратор для этой цели-и, конечно, он имеет значения по умолчанию.

пример из docs:

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

гораздо чище, читаемый и пригодный для использования, чем взлом namedtuple. Нетрудно предсказать, что использование namedtuples упадет с принятием 3.7.


вдохновленный ответ к другому вопросу, вот мое предлагаемое решение, основанное на метакласс и с помощью super (будущему subcalssing правильно). Это очень похоже на ответ 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__ = ()

затем:

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

С помощью NamedTuple - класс от моей Advanced Enum (aenum) библиотека и использование class синтаксис, это довольно просто:

from aenum import NamedTuple

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

одним из потенциальных недостатков является требование для __doc__ строку для любого атрибута со значением по умолчанию (это необязательно для простых атрибутов). В использовании это выглядит так:

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

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

преимущества над 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)

- это простота, а как metaclass на основе вместо exec на основе.


другое решение:

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

использование:

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

вот короткий, простой общий ответ с хорошим синтаксисом для именованного кортежа с аргументами по умолчанию:

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

использование:

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

ответ jterrace на использование recordtype велик, но автор библиотеки рекомендует использовать его namedlist проект, который обеспечивает как Мутабельный (namedlist) и неизменяемые (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)

вот менее гибкая, но более сжатая версия оболочки Марка Лодато: она принимает поля и значения по умолчанию в качестве словаря.

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

пример:

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)