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
декоратор для этой цели-и, конечно, он имеет значения по умолчанию.
@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
(будущему 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)