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 ответ:
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
. Нетрудно предсказать, что использованиеnamedtuple
s упадет с принятием 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)