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


300

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

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)но это мне не нравится.

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


2
Почему вы хотите сделать это преобразование? Мне нравится ваш оригинальный Nodeкласс просто так, как он есть. Зачем конвертировать в именованный кортеж?
Steveha

34
Я хотел сделать это преобразование, потому что текущий Nodeи другие классы являются простыми объектами значений держателя данных с кучей различных полей ( Nodeэто только одно из них). Эти объявления классов - не более чем линейный шум, поэтому ИМХО хотел их обрезать. Зачем поддерживать то, что не требуется? :)
Саске

У вас нет каких-либо методических функций в ваших классах вообще? У вас нет, например, .debug_print()метода, который прогуливается по дереву и печатает его?
Steveha

2
Конечно, но это для BinaryTreeкласса. Nodeи другие держатели данных не требуют таких специальных методов особ , учитывая , что названные кортежи имеют порядочный __str__и __repr__представление. :)
Саске

Хорошо, кажется разумным. И я думаю, что Игнасио Васкес-Абрамс дал вам ответ: используйте функцию, которая делает значения по умолчанию для вашего узла.
Steveha

Ответы:


532

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)

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

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

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

порядок

Во всех версиях 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)

22
Давайте посмотрим ... ваш однострочник: а) самый короткий / самый простой ответ, б) сохраняет эффективность пространства, в) не ломается isinstance... все плюсы, нет минусов ... жаль, что вы немного опоздали вечеринка. Это лучший ответ.
Геррат

1
Одна проблема с версией обертки: в отличие от встроенной коллекции collection.namedtuple, эта версия не может быть рассортирована / сериализуема из нескольких процессов, если def () включен в другой модуль.
Майкл Скотт Катберт

2
Я дал этому ответу голосование, поскольку оно предпочтительнее моего. Жаль, однако, что мой собственный ответ продолжает получать голосование: |
Джастин Фэй

3
@ ishaaq, проблема в том, что (None)это не кортеж None. Если вы используете (None,)вместо этого, он должен работать нормально.
Марк Лодато

2
Превосходно! Вы можете обобщить настройки по умолчанию с помощью:Node.__new__.__defaults__= (None,) * len(Node._fields)
ankostis

142

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

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


7
Для этого могут потребоваться свойства слотов и полей, чтобы поддерживать эффективность пространства именованного кортежа.
Пепейн

Почему-то __new__не вызывается, когда _replaceиспользуется.

1
Пожалуйста, посмотрите на ответ @ marc-lodato ниже, который, IMHO, является лучшим решением, чем этот.
Джастин Фэй,

1
но ответ @ marc-lodato не дает возможность подклассу иметь разные значения по умолчанию
Jason S

1
@JasonS, я подозреваю, что для подкласса с разными значениями по умолчанию может нарушаться LSP . Однако подкласс может очень хотеть иметь больше значений по умолчанию. В любом случае, для подкласса будет использоваться метод justinfay , а базовый класс будет в порядке с методом Марка .
Алексей

94

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

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

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

15
Это умно, и может быть хорошим вариантом, но может также вызвать проблемы, ломая isinstance(Node('val'), Node): теперь это вызовет исключение, а не возвращает True. Хотя ответ @ justinfay (ниже) немного более подробный, он правильно сохраняет информацию об иерархии типов, поэтому, вероятно, это лучший подход, если другие будут взаимодействовать с экземплярами Node.
Габриэль Грант

4
Мне нравится краткость этого ответа. Возможно, проблема в комментарии выше может быть решена путем присвоения имени функции, def make_node(...):а не притворения, что это определение класса. Таким образом, пользователи не испытывают желания проверять полиморфизм типов в функции, а используют само определение кортежа.
user1556435

См. Мой ответ для варианта этого, который не страдает от того, чтобы вводить людей в заблуждение, чтобы использовать isinstanceнеправильно.
Эллиот Кэмерон

70

В typing.NamedTuplePython 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), которые в некоторых (многих?) Случаях могут заменить именованные кортежи.


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

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

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


4
Это кажется самым чистым решением для пользователей 3.6.1+. Обратите внимание, что этот пример (немного) сбивает с толку как подсказку типа для полей leftи right(т.е. Node) имеет тот же тип, что и определяемый класс, и поэтому должен быть записан как строки.
101

1
@ 101, спасибо, я добавил примечание к ответу.
монах

2
Какой аналог для идиомы my_list: List[T] = None self.my_list = my_list if my_list is not None else []? Разве мы не можем использовать параметры по умолчанию, как это?
weberc2

@ weberc2 Отличный вопрос! Я не уверен, что это обходной путь для изменяемого def. значения возможны с typing.NamedTuple. Но с классами данных вы можете использовать Field объекты с default_factoryатрибутом. для этого, заменив вашу идиому на my_list: List[T] = field(default_factory=list).
монах

20

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

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

Итак, пример ОП будет следующим:

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

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


2
+1. Это очень странно, что они решили пойти с _методом (который в основном означает частный) для чего-то вроде, replaceчто кажется довольно полезным ..
Саске

@sasuke - мне тоже было интересно. Уже немного странно, что вместо элементов вы определяете элементы строкой, разделенной пробелом *args. Может случиться так, что он был добавлен в язык до того, как многие из этих вещей были стандартизированы.
Тим Тисдалл

12
_Префикс , чтобы избежать столкновения с именами пользовательских полей кортежа (соответствующий док цитата: «Любой действительный Python идентификатор может быть использован для имя_поля имен , начинающихся с символа подчеркивания , за исключением.»). Что касается разделенной пробелами строки, я думаю, что это просто для сохранения нескольких нажатий клавиш (и вы можете передать последовательность строк, если хотите).
Сёрен Лёвборг

1
Ах, да, я забыл, что вы обращаетесь к элементам именованного кортежа в качестве атрибутов, так что тогда это _имеет большой смысл.
Тим Тисдалл

2
Ваше решение простое и лучшее. Остальное ИМХО довольно некрасиво. Я бы сделал только одно небольшое изменение. Вместо default_node я бы предпочел node_default, потому что он лучше работает с IntelliSense. Если вы начнете набирать узел, вы получите все, что вам нужно :)
Павел Ханпари

19

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

2
Ах, невозможно использовать сторонний пакет, хотя, recordtypeбезусловно, выглядит интересным для будущей работы. +1
Саске

Модуль довольно маленький и содержит только один файл, поэтому вы всегда можете просто добавить его в свой проект.
Jterrace

Справедливо, хотя я подожду еще немного времени для решения с чисто именованным кортежем, если оно есть, прежде чем отмечать это как принятое! :)
Саске

Согласен чистый питон был бы хорош, но я не думаю, что есть один :(
jterrace

3
Просто чтобы заметить, что recordtypeэто изменчиво, а namedtupleнет. Это может иметь значение, если вы хотите, чтобы объект был хэшируемым (что, я полагаю, нет, поскольку он начинался как класс).
Баваза

14

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

from collections import namedtuple
from functools import partial

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

7
Остерегайтесь, Node(1, 2)это не работает с этим рецептом, но работает в ответе @ justinfay. В противном случае это довольно изящно (+1).
Хорхе

12

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

7

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

5

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

Python 3.7: введение defaultsparam в определение именованных кортежей.

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

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

Узнайте больше здесь .


4

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

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)

4

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

3

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

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)

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


3

Поскольку вы используете 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. Нетрудно предсказать, что использование namedtuples упадет с принятием 3.7.


2

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

2

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

1

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

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

0

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


0

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

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)

-1

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

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)

4
dictне имеет гарантии заказа.
Итан Фурман
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.