значення nametuple та типові значення для необов'язкових аргументів ключового слова


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

Але тут є проблема. Мій оригінальний клас дозволив мені передати лише значення та подбав про типові, використовуючи значення за замовчуванням для іменних / ключових слів. Щось на зразок:

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

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

Чи існує хороший трюк, який може зробити моє перезапис успішним без додавання великої складності коду (метапрограмування) чи мені просто проковтнути таблетку і продовжувати "пошук і заміна"? :)


2
Чому ви хочете зробити це перетворення? Мені подобається ваш оригінальний Nodeклас саме таким, яким він є. Навіщо перетворюватися на названий кортеж?
steveha

34
Я хотів здійснити це перетворення, тому що поточний Nodeта інші класи - це прості об'єкти значень власника даних з купою різних полів ( Nodeце лише одне з них). Ці декларації класу - це не що інше, як лінійний шум IMHO, тому він хотів їх обрізати. Навіщо підтримувати щось, що не потрібно? :)
sasuke

У вас взагалі немає ніяких функцій методу на своїх заняттях? У вас, наприклад, немає .debug_print()методу, який ходить по дереву і друкує його?
steveha

2
Звичайно, я це роблю, але це для BinaryTreeкласу. Nodeта інші власники даних не вимагають таких спеціальних методів, враховуючи, що названі кортежі мають гідне __str__та __repr__представництво. :)
sasuke

Гаразд, здається розумним. І я думаю, що Ігнасіо Васкес-Абрамс дав вам відповідь: використовуйте функцію, яка робить значення за замовчуванням для вашого вузла.
steveha

Відповіді:


532

Пітон 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)

Або ще краще, використовуйте нову бібліотеку класів даних , що набагато приємніше, ніж nametuple.

>>> 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, якщо ви встановите менше значень за замовчуванням, ніж існує у найменуванніtutu, за замовчуванням застосовуються найправіші параметри. Це дозволяє зберігати деякі аргументи як необхідні аргументи.

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

Я підкласирував nametuple і переосмислив __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
Для цього можуть знадобитися властивості слотів і полів, щоб підтримувати ефективність простору названого кортежу.
Pepijn

Чомусь __new__не викликається, коли _replaceвикористовується.

1
Будь ласка, подивіться на відповідь @ marc-lodato, нижче якої ІМХО є кращим рішенням, ніж це.
Джастін Фей

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(...):а не роблячи вигляд, що це визначення класу. Таким чином користувачі не спокушаються перевіряти поліморфізм типу функції, але використовують саме визначення кортежу.
користувач1556435

Дивіться моєї відповіді на варіант цього, який не страждає від оману людей, щоб 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), які можуть у деяких (багатьох?) Випадках замінити названі пара.


Ідентифікатор: одна хитрість поточної специфікації анотацій (вирази after :для параметрів і змінних та after ->для функцій) в Python полягає в тому, що вони оцінюються в час визначення * . Отже, оскільки "імена класів визначаються після того, як буде виконано все тіло класу", анотації 'Node'до полів класу вище повинні бути рядками, щоб уникнути NameError.

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

* AFAICT лише локальні примітки зі змінними не оцінюються під час виконання. (джерело: PEP 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_factoryattr. для цього, замінивши свою ідіому на 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

@sasuke - мені теж було цікаво. Уже трохи дивно, що ви визначаєте елементи за допомогою пробілу, розділеного рядком *args. Це може бути просто те, що воно було додане до мови, перш ніж багато цих речей були стандартизовані.
Тім Тісдалл

12
_Префікс , щоб уникнути зіткнення з іменами призначених для користувача полів кортежу (відповідний док цитата: «Будь-який дійсний Python ідентифікатор може бути використаний для ім'я_поля імен , що починаються з символу підкреслення , за винятком.»). Що стосується розділеного пробілом рядка, я думаю, що це просто для збереження декількох натискань клавіш (і ви можете передавати послідовність рядків, якщо хочете).
Søren Løvborg

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
sasuke

Модуль досить маленький і лише один файл, тому ви завжди можете просто додати його до свого проекту.
jterrace

Досить справедливо, хоча я зачекаю ще деякого часу, щоб чистий названий кортеж - це рішення є там, перш ніж відзначити це прийнятим! :)
sasuke

Погоджений чистий пітон був би непоганим, але я не думаю, що є такий :(
jterrace

3
Зауважимо лише, що recordtypeце є змінним, тоді namedtupleяк ні. Це може мати значення, якщо ви хочете, щоб об'єкт був хешируемым (чого, мабуть, ви не робите, оскільки він починався як клас).
bavaza

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

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: введення defaultsпарамуму у визначеному nametuple.

Приклад, як показано в документації:

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

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.