オプションのキーワード引数の名前付きタプルとデフォルト値


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)to の出現を置き換えるNode(val, None, None)ことはできますが、それは私の好みではありません。

それで、コードの複雑さを大幅に追加せずに(メタプログラミング)、私の書き換えを成功させることができる優れたトリックはありますか?それとも、錠剤を飲み込んで「検索と置換」を続行する必要がありますか?:)


2
なぜこの変換をしたいのですか?私はあなたの元のNodeクラスがそのままで好きです。名前付きタプルに変換する理由
steveha

34
現在Nodeのクラスと他のクラスは、さまざまなフィールドがたくさんある単純なデータホルダー値オブジェクトです(Nodeその1つにすぎません)。これらのクラス宣言は、ラインノイズIMHOにすぎないため、それらを削除する必要がありました。なぜ必要のないものを維持するのですか?:)
sasuke

クラスにメソッド関数がまったくありませんか?たとえば、.debug_print()ツリーをウォークして印刷するメソッドはありませんか?
steveha

2
確かにそうですが、それはBinaryTreeクラスのためです。Nodeその他のデータ保有者は、ESPという名前のタプルがまとも持っていることを考えると、このような特殊な方法を必要としない__str____repr__表現。:)
sasuke

さて、合理的なようです。そして、私はIgnacio Vazquez-Abramsがあなたに答えを与えたと思います:ノードのデフォルト値を実行する関数を使用してください。
steveha

回答:


532

Python 3.7

defaultsパラメータを使用します。

>>> 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よりもはるかに優れた新しいdataclassesライブラリを使用してください。

>>> 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
見てみましょう...あなたのワンライナー:a)最も短い/最も簡単な答えです、b)スペース効率を維持します、c)壊れませんisinstance...すべての長所、短所...少し遅すぎて残念でしたパーティー。これが最良の答えです。
Gerrat

1
ラッパーバージョンの1つの問題:組み込みのcollections.namedtupleとは異なり、このバージョンは、def()が別のモジュールに含まれている場合、ピクル可能/マルチプロセスシリアライズ可能ではありません。
Michael Scott Cuthbert 2014年

2
自分よりも好ましいので、この回答には賛成票を与えました。しかし、私自身の答えが賛成され続けているのは残念です:|
ジャスティンフェイ

3
@ishaaq、問題は(None)タプルではなく、それNoneです。(None,)代わりに使用すれば、問題なく動作するはずです。
Mark Lodato、2015

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
名前付きタプルのスペース効率を維持するために、スロットとフィールドのプロパティが必要になる場合があります。
Pepijn 2014

何らかの理由で、が使用__new__されているときに呼び出されていません_replace

1
下記の@ marc-lodatoの回答をご覧ください。IMHOはこれよりも優れたソリューションです。
ジャスティンフェイ

1
しかし、@ marc-lodatoの答えは、サブクラスが異なるデフォルトを持つ能力を提供しません
Jason S

1
@JasonS、私はサブクラスが異なるデフォルトを持つことがLSPに違反する可能性があると思います。ただし、サブクラスはより多くのデフォルトを必要とする場合があります。いずれの場合でも、サブクラスはjustinfayのmethodを使用し、基本クラスはMarcのmethodで問題ありません。
アレクセイ2018

94

関数で囲みます。

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

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

15
これは賢い方法であり、適切なオプションになる可能性がありますが、中断によって問題を引き起こす可能性もあります。Trueをisinstance(Node('val'), Node)返すのではなく、例外が発生するようになりました。少し冗長ですが、@ justinfayの回答(下記)は型階層情報を適切に保持するため、他の人がNodeインスタンスと対話する場合はおそらくより良いアプローチです。
ガブリエルグラント

4
この回答の簡潔さが気に入っています。おそらく、上のコメントでの懸念は、関数をdef make_node(...):クラス定義であるように見せかけるのではなく、関数に名前を付けることで対処できます。このようにして、ユーザーは関数の型ポリモーフィズムをチェックする気にならず、タプル定義自体を使用します。
user1556435 2016年

誤解を招く人々の誤用に悩まされないこのバリエーションについては、私の回答を参照してくださいisinstance
Elliot Cameron

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)があります。


補足:Python での注釈:パラメーターと変数の後->、関数の後の式)の現在の仕様の1つの奇妙な点は、定義時に評価されることです*。したがって、「クラスの本体全体が実行されるとクラス名が定義されるようになる」ため'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素晴らしい質問です!可変デフのこの回避策かどうかはわかりません。値はで可能typing.NamedTupleです。ただし、データクラスでは、attrを使用して 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')

したがって、OPの例は次のようになります。

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

しかし、私はここで与えられた他のいくつかの答えがより好きです。私はこれを完全性のために追加したかっただけです。


2
+1。それは、彼らが一緒に行くことに決めたことは非常に奇妙だ_ような何かのために(基本的には民間の1を意味する)方法replace...かなり便利思われる
サスケ

@sasuke-私もそう思っていました。要素をの代わりにスペースで区切られた文字列で定義するのはすでに少し奇妙です*args。それらの多くが標準化される前に言語に追加されただけかもしれません。
Tim Tisdall、2015

12
_プレフィックスは、ユーザー定義のタプルフィールドの名前との衝突を避けるためにある(関連ドキュメントの引用:「任意の有効なPython識別子がアンダースコアで始まる名前を除くフィールド名に使用することができます。」)。スペースで区切られた文字列については、キーストロークをいくつか保存するだけだと思います(必要に応じて、文字列のシーケンスを渡すこともできます)。
セーレンLøvborg

1
ああ、ええ、名前付きタプルの要素に属性としてアクセスするのを忘れていたので、それは_それでかなり意味があります。
Tim Tisdall、2015

2
あなたのソリューションはシンプルで最高です。残りは私見ではなく醜いです。小さな変更を1つだけ行います。default_nodeの代わりにIntelliSenseのエクスペリエンスを向上させるため、node_defaultを使用します。ノードを入力し始めた場合、必要なものがすべて
届きました

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
サスケ

モジュールは非常に小さく、ファイル1つだけなので、いつでもプロジェクトに追加できます。
jterrace

十分に公正ですが、純粋な名前付きタプルソリューションがあるのでしばらく待ってから、これが受け入れられるとマークする前にそこにあります!:)
sasuke

純粋なpythonはいいと思いますが、これはないと思います:(
jterrace

3
これrecordtypeは変更可能ですが、変更namedtupleできないことに注意してください。これは、オブジェクトをハッシュ可能にしたい場合に重要になる可能性があります(クラスとして開始されたため、そうしないと思います)。
bavaza 2013

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 14

12

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)

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名前付きタプル定義でのparamの導入。

ドキュメントに示されている例:

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

これはオブジェクトを2回作成する必要があるので効率的ではありませんが、モジュール内でデフォルトのdupleを定義し、関数にreplace行を実行させるだけで変更できます。


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'

ハッキングよりもはるかにクリーンで、読みやすく、使用可能ですnamedtuplenamedtuple3.7の採用により、sの使用が減少することを予測することは難しくありません。


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

使用RECORDTYPEにjterraceによって答えは素晴らしいですが、ライブラリの作者は、彼の使用することをお勧めします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)

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

潜在的な欠点の1つは、__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

Mark Lodatoのラッパーの柔軟性は低くなりますが、より簡潔なバージョンです。フィールドとデフォルトを辞書として使用します。

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注文の保証はありません。
Ethan Furman、2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.