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

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

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)

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

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

Ответ 1

Python 3.7

Используйте параметр по умолчанию.

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

До Python 3.7

Установите Node.__new__.__defaults__ на значения по умолчанию.

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

До Python 2.6

Установите Node.__new__.func_defaults на значения по умолчанию.

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

Order

ЗаказатьВо всех версиях Python, если вы устанавливаете меньше значений по умолчанию, чем существует в namedtuple, значения по умолчанию применяются к самым правым параметрам. Это позволяет вам сохранять некоторые аргументы в качестве обязательных аргументов.

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

Оболочка для Python 2.6 до 3.6

Вот вам обертка, которая даже позволяет (опционально) установить значения по умолчанию на что-то отличное от 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)

Ответ 2

Я подклассифицировал 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)

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

Ответ 3

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

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

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

Ответ 4

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

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

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

Ответ 5

Это пример прямо из документации:

Значения по умолчанию могут быть реализованы с помощью _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")

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

Ответ 6

Я не уверен, есть ли простой способ только с встроенным 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)

Ответ 7

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

from collections import namedtuple
from functools import partial

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

Ответ 8

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

значения по умолчанию могут быть None или итерацией значений по умолчанию. Поскольку поля со значением по умолчанию должны идти после любых полей без значения по умолчанию, значения по умолчанию применяются к самым правым параметрам. Например, если имена полей ['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)

Ответ 9

Короткий, простой и не приводит людей к неправильному использованию 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))

Ответ 10

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

Ответ 11

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

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)

Ответ 12

Сочетание подходов @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'

Ответ 13

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

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

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

Подробнее читайте здесь.

Ответ 14

Мне легче читать эту версию:

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)

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

Ответ 15

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

Пример из документов:

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

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

Ответ 16

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

Ответ 17

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

Ответ 18

Используя класс 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 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.

Ответ 19

Если вы никогда не выполняете эти переменные (они всегда None), вы можете просто сделать

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

и просто сохраните его просто.

Вы всегда можете вставить NodeVariable = Node(self, val, right, left) внутри оператора try, но я получаю большое чувство, что вы не хотите делать.

Ответ 20

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

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)

Ответ 21

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

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)

уменьшенная:

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

Ответ 22

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

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)