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

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

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

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

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

21 196

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.


Примечание: одна причуда тока спецификация аннотации (выражений после ключевого слова : для параметров и переменных и после -> для функций) в 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)

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

значения по умолчанию могут быть реализованы с помощью _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+ есть совершенно новый defaults= ключевое слово аргумент.

по умолчанию может быть 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)

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

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

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

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

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

С помощью 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)

сокращен:

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)