Pythonで変更可能な名前付きタプルの存在?


120

誰かがnamedtupleを修正したり、変更可能なオブジェクトで機能するように代替クラスを提供したりできますか?

主に読みやすくするために、これを行うnamedtupleに似たものを望みます。

from Camelot import namedgroup

Point = namedgroup('Point', ['x', 'y'])
p = Point(0, 0)
p.x = 10

>>> p
Point(x=10, y=0)

>>> p.x *= 10
Point(x=100, y=0)

結果のオブジェクトをピクルすることが可能でなければなりません。また、名前付きタプルの特性に従って、表現されるときの出力の順序は、オブジェクトを構築するときのパラメーターリストの順序と一致する必要があります。


3
参照:stackoverflow.com/q/5131044。辞書だけじゃダメなの?
senshin 2015年

@senshinリンクありがとうございます。私はそれで指摘された理由のために辞書を使用しないことを好みます。その応答は、code.activestate.com / recipes /…にもリンクしています。これは、私が求めているものにかなり近いものです。
アレクサンダー

異なりnamedtuple、S、とてもつまり、あなたがインデックスによって属性を参照することができるようにする必要がない表示されますp[0]し、p[1]参照する別の方法だろうxy、それぞれ正しいですか?
martineau 2015年

理想的には、はい。名前だけでなくプレーンなタプルのように位置によってインデックス付けでき、タプルのようにアンパックします。このActiveStateレシピは近いですが、OrderedDictではなく通常の辞書を使用していると思います。code.activestate.com/recipes/500261
Alexander

2
可変の名前付きタプルはクラスと呼ばれます。
gbtimmon 2017

回答:


131

変更可能な代替ありcollections.namedtuple- recordclassが

と同じAPIおよびメモリフットプリントをnamedtuple持ち、割り当てをサポートします(より高速である必要があります)。例えば:

from recordclass import recordclass

Point = recordclass('Point', 'x y')

>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
>>> print(p.x, p.y)
1 2
>>> p.x += 2; p.y += 3; print(p)
Point(x=3, y=5)

Python 3.6 recordclass以降(0.5以降)ではtypehintsをサポートします:

from recordclass import recordclass, RecordClass

class Point(RecordClass):
   x: int
   y: int

>>> Point.__annotations__
{'x':int, 'y':int}
>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
>>> print(p.x, p.y)
1 2
>>> p.x += 2; p.y += 3; print(p)
Point(x=3, y=5)

より完全ながあります(パフォーマンスの比較も含まれます)。

0.9以降、recordclassライブラリは別のバリアント- recordclass.structclassファクトリ関数-を提供します。クラスを生成でき__slots__ます。そのインスタンスは、ベースのインスタンスよりも少ないメモリを使用します。これは、参照サイクルを持つことを意図していない、属性値を持つインスタンスにとって重要な場合があります。数百万のインスタンスを作成する必要がある場合は、メモリ使用量を削減するのに役立ちます。以下に例を示します


4
いいね。「このライブラリは、実際には、名前付きタプルの「変更可能な」代替の問題の「概念実証」です。」
Alexander

1
recordclassAntti Haapalaのレシピやと比較して、速度が遅く、メモリを多く消費し、C拡張が必要namedlistです。
GrantJ 2016

recordclassの変更可能なバージョンでcollection.namedtuple、API、メモリフットプリントを継承していますが、割り当てをサポートしています。 namedlist実際にはスロットを持つpythonクラスのインスタンスです。インデックスでフィールドにすばやくアクセスする必要がない場合は、より便利です。
Intellimath 16

recordclassインスタンス(python 3.5.2)の属性アクセスは、よりも2〜3%遅くなりますnamedlist
intellimath

namedtuple単純なクラス作成を使用する場合Point = namedtuple('Point', 'x y')、Jediは属性をオートコンプリートできますが、これは当てはまりませんrecordclass。(に基づくRecordClass)より長い作成コードを使用すると、JediはPointクラスを認識しますが、そのコンストラクターや属性は認識しません... recordclassJediを適切に使用する方法はありますか?
PhilMacKay

34

types.SimpleNamespaceはPython 3.3で導入され、要求された要件をサポートしています。

from types import SimpleNamespace
t = SimpleNamespace(foo='bar')
t.ham = 'spam'
print(t)
namespace(foo='bar', ham='spam')
print(t.foo)
'bar'
import pickle
with open('/tmp/pickle', 'wb') as f:
    pickle.dump(t, f)

1
このようなものを何年も探していました。dotmapのようなドット付きdictライブラリの優れた代替
axwell

1
これにはもっと賛成票が必要です。それはまさにOPが探していたものであり、標準ライブラリーにあり、使用するのは簡単ではありません。ありがとう!
トムジッチ

3
-1 OPは、彼のテストで彼が必要とするものを非常に明確にし、テストSimpleNamespace6-10(インデックスによるアクセス、反復アンパッキング、反復、順序付けされたdict、インプレース置換)および12、13(フィールド、スロット)に失敗しました。(回答でリンクした)ドキュメントには、SimpleNamespaceの代替として役立つ可能性がありますclass NS: pass。ただし、namedtuple()代わりに構造化レコードタイプを使用する」
Ali

1
-1も、SimpleNamespaceクラスコンストラクターではなくオブジェクトを作成し、namedtupleの代わりにはなりません。タイプの比較は機能せず、メモリフットプリントははるかに高くなります。
RedGlyph

26

このタスクの非常にPython的な代替手段として、Python-3.7以降dataclassesでは、NamedTuple通常のクラス定義を使用するために変更可能なように動作するだけでなく、他のクラス機能もサポートするモジュールを使用でき ます。

PEP-0557から:

非常に異なるメカニズムを使用しますが、データクラスは「デフォルトの可変の名前付きタプル」と考えることができます。データクラスは通常のクラス定義構文を使用するため、継承、メタクラス、ドキュメント文字列、ユーザー定義メソッド、クラスファクトリ、およびその他のPythonクラス機能を自由に使用できます。

PEP 526「Syntax for Variable Annotations」で定義されているように、型アノテーションを持つ変数のクラス定義を検査するクラスデコレータが提供されています。このドキュメントでは、そのような変数をフィールドと呼びます。これらのフィールドを使用して、デコレータはクラスに生成されたメソッド定義を追加し、インスタンスの初期化、repr、比較メソッド、およびオプションで、仕様セクションで説明されている他のメソッドをサポートします。そのようなクラスはデータクラスと呼ばれますが、クラスについて特別なことは何もありません。デコレータは生成されたメソッドをクラスに追加し、指定されたものと同じクラスを返します。

この機能はPEP-0557で導入され、提供されているドキュメントリンクの詳細で読むことができます。

例:

In [20]: from dataclasses import dataclass

In [21]: @dataclass
    ...: class InventoryItem:
    ...:     '''Class for keeping track of an item in inventory.'''
    ...:     name: str
    ...:     unit_price: float
    ...:     quantity_on_hand: int = 0
    ...: 
    ...:     def total_cost(self) -> float:
    ...:         return self.unit_price * self.quantity_on_hand
    ...:    

デモ:

In [23]: II = InventoryItem('bisc', 2000)

In [24]: II
Out[24]: InventoryItem(name='bisc', unit_price=2000, quantity_on_hand=0)

In [25]: II.name = 'choco'

In [26]: II.name
Out[26]: 'choco'

In [27]: 

In [27]: II.unit_price *= 3

In [28]: II.unit_price
Out[28]: 6000

In [29]: II
Out[29]: InventoryItem(name='choco', unit_price=6000, quantity_on_hand=0)

1
OPのテストで何が必要かが明確になりdataclass、Python 3.7のテスト6-10(インデックスによるアクセス、反復アンパッキング、反復、順序付けされたdict、インプレース置換)と12、13(フィールド、スロット)が失敗しました。 .1。
Ali

1
これはOPが探していたものではないかもしれませんが、確かに私には役立ちました:)
Martin CR

25

最新の名前付きリスト 1.7は、2016年1月11日の時点で、Python 2.7とPython 3.5の両方を使用したすべてのテストに合格していますこれは純粋なpython実装であり、recordclassCの拡張機能です。もちろん、C拡張が望ましいかどうかは、要件によって異なります。

テスト(ただし、下の注も参照):

from __future__ import print_function
import pickle
import sys
from namedlist import namedlist

Point = namedlist('Point', 'x y')
p = Point(x=1, y=2)

print('1. Mutation of field values')
p.x *= 10
p.y += 10
print('p: {}, {}\n'.format(p.x, p.y))

print('2. String')
print('p: {}\n'.format(p))

print('3. Representation')
print(repr(p), '\n')

print('4. Sizeof')
print('size of p:', sys.getsizeof(p), '\n')

print('5. Access by name of field')
print('p: {}, {}\n'.format(p.x, p.y))

print('6. Access by index')
print('p: {}, {}\n'.format(p[0], p[1]))

print('7. Iterative unpacking')
x, y = p
print('p: {}, {}\n'.format(x, y))

print('8. Iteration')
print('p: {}\n'.format([v for v in p]))

print('9. Ordered Dict')
print('p: {}\n'.format(p._asdict()))

print('10. Inplace replacement (update?)')
p._update(x=100, y=200)
print('p: {}\n'.format(p))

print('11. Pickle and Unpickle')
pickled = pickle.dumps(p)
unpickled = pickle.loads(pickled)
assert p == unpickled
print('Pickled successfully\n')

print('12. Fields\n')
print('p: {}\n'.format(p._fields))

print('13. Slots')
print('p: {}\n'.format(p.__slots__))

Python 2.7での出力

1.フィールド値の変更  
p:10、12

2.文字列  
p:ポイント(x = 10、y = 12)

3.表現  
ポイント(x = 10、y = 12) 

4. Sizeof  
pのサイズ:64 

5.フィールド名によるアクセス  
p:10、12

6.インデックスによるアクセス  
p:10、12

7.反復的な解凍  
p:10、12

8.反復  
p:[10、12]

9.命令された口述  
p:OrderedDict([( 'x'、10)、( 'y'、12)])

10.インプレース置換(更新?)  
p:ポイント(x = 100、y = 200)

11.ピクルスとアンピクル  
酢漬け

12.フィールド  
p:( 'x'、 'y')

13.スロット  
p:( 'x'、 'y')

Python 3.5との唯一の違いはnamedlist、サイズが小さくなり、サイズが56になることです(Python 2.7は64を報告します)。

インプレース交換のためにテスト10を変更しました。namedlist持っている_replace()浅いコピーを行う方法を、そしてので、それは私には完璧な理にかなっているnamedtuple標準ライブラリでは、同じように動作します。_replace()メソッドのセマンティクスを変更すると、混乱を招きます。私の意見では、この_update()方法はインプレース更新に使用する必要があります。それとも、テスト10の目的を理解できなかったのでしょうか。


重要なニュアンスがあります。namedlistリストインスタンスのストア値。事はことであるcpythonのは、list実際には動的配列です。設計上、リストの変更を安くするために、必要以上のメモリを割り当てます。
intellimath

1
@intellimathの名前付きリストは少し誤った名称です。それは実際には継承せずlist、デフォルトで__slots__最適化を使用します。私が測定したところ、メモリ使用量は以下でしたrecordclass:Python 2.7の6つのフィールドで96バイトvs 104バイト
GrantJ

@GrantJはい。recorclasstupleメモリサイズが可変のオブジェクトに似ているため、より多くのメモリを使用します。
Intellimath 2016

2
匿名の反対投票は誰の助けにもなりません。答えの何が問題になっていますか?なぜ反対票か。
Ali

私はそれが提供するタイプミスに対する安全性が大好きtypes.SimpleNamespaceです。残念ながら、pylintはそれを気に入らない:-(
xverges

23

この質問への答えはノーのようです。

以下はかなり近いですが、技術的には変更可能ではありません。これはnamedtuple()、更新されたx値で新しいインスタンスを作成しています:

Point = namedtuple('Point', ['x', 'y'])
p = Point(0, 0)
p = p._replace(x=10) 

一方、__slots__クラスインスタンスの属性を頻繁に更新する場合は、これを使用して簡単なクラスを作成できます。

class Point:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

この回答に追加するに__slots__は、多くのクラスインスタンスを作成するときにメモリ効率が良いので、ここでの使用が良いと思います。唯一の欠点は、新しいクラス属性を作成できないことです。

メモリ効率を示す1つの関連するスレッドは次のとおりです- 辞書とオブジェクトのどちらがより効率的で、なぜですか?

このスレッドの回答の引用された内容は、__slots__メモリ効率が高い理由を非常に簡潔に説明しています-Pythonスロット


1
近いが不格好。+ =割り当てを実行したいとしましょう。その後、次のようにする必要があります:p._replace(x = px + 10)vs. px + = 10
Alexander

1
ええ、それは実際に既存のタプルを変更するのではなく、新しいインスタンスを作成しています
ケネス

7

以下は、Python 3の優れたソリューションです。最小限のクラスを使用し__slots__Sequence抽象基本クラス。派手なエラー検出などは行いませんが、機能し、(typecheckを除いて)変更可能なタプルのように動作します。

from collections import Sequence

class NamedMutableSequence(Sequence):
    __slots__ = ()

    def __init__(self, *a, **kw):
        slots = self.__slots__
        for k in slots:
            setattr(self, k, kw.get(k))

        if a:
            for k, v in zip(slots, a):
                setattr(self, k, v)

    def __str__(self):
        clsname = self.__class__.__name__
        values = ', '.join('%s=%r' % (k, getattr(self, k))
                           for k in self.__slots__)
        return '%s(%s)' % (clsname, values)

    __repr__ = __str__

    def __getitem__(self, item):
        return getattr(self, self.__slots__[item])

    def __setitem__(self, item, value):
        return setattr(self, self.__slots__[item], value)

    def __len__(self):
        return len(self.__slots__)

class Point(NamedMutableSequence):
    __slots__ = ('x', 'y')

例:

>>> p = Point(0, 0)
>>> p.x = 10
>>> p
Point(x=10, y=0)
>>> p.x *= 10
>>> p
Point(x=100, y=0)

必要に応じて、クラスを作成するメソッドも用意できます(ただし、明示的なクラスを使用する方が透過的です)。

def namedgroup(name, members):
    if isinstance(members, str):
        members = members.split()
    members = tuple(members)
    return type(name, (NamedMutableSequence,), {'__slots__': members})

例:

>>> Point = namedgroup('Point', ['x', 'y'])
>>> Point(6, 42)
Point(x=6, y=42)

Python 2では少し調整する必要があります- から継承したSequence場合、クラスにはが__dict__あり、__slots__は機能しなくなります。

Python 2での解決策は、から継承するのでSequenceはなく、から継承することobjectです。isinstance(Point, Sequence) == True必要な場合はNamedMutableSequence、を基本クラスとしてを登録する必要があります Sequence

Sequence.register(NamedMutableSequence)

3

これを動的型作成で実装しましょう:

import copy
def namedgroup(typename, fieldnames):

    def init(self, **kwargs): 
        attrs = {k: None for k in self._attrs_}
        for k in kwargs:
            if k in self._attrs_:
                attrs[k] = kwargs[k]
            else:
                raise AttributeError('Invalid Field')
        self.__dict__.update(attrs)

    def getattribute(self, attr):
        if attr.startswith("_") or attr in self._attrs_:
            return object.__getattribute__(self, attr)
        else:
            raise AttributeError('Invalid Field')

    def setattr(self, attr, value):
        if attr in self._attrs_:
            object.__setattr__(self, attr, value)
        else:
            raise AttributeError('Invalid Field')

    def rep(self):
         d = ["{}={}".format(v,self.__dict__[v]) for v in self._attrs_]
         return self._typename_ + '(' + ', '.join(d) + ')'

    def iterate(self):
        for x in self._attrs_:
            yield self.__dict__[x]
        raise StopIteration()

    def setitem(self, *args, **kwargs):
        return self.__dict__.__setitem__(*args, **kwargs)

    def getitem(self, *args, **kwargs):
        return self.__dict__.__getitem__(*args, **kwargs)

    attrs = {"__init__": init,
                "__setattr__": setattr,
                "__getattribute__": getattribute,
                "_attrs_": copy.deepcopy(fieldnames),
                "_typename_": str(typename),
                "__str__": rep,
                "__repr__": rep,
                "__len__": lambda self: len(fieldnames),
                "__iter__": iterate,
                "__setitem__": setitem,
                "__getitem__": getitem,
                }

    return type(typename, (object,), attrs)

これにより、属性をチェックして、操作の続行を許可する前に属性が有効かどうかを確認します。

これは漬物ですか?はい、次の場合に限ります。

>>> import pickle
>>> Point = namedgroup("Point", ["x", "y"])
>>> p = Point(x=100, y=200)
>>> p2 = pickle.loads(pickle.dumps(p))
>>> p2.x
100
>>> p2.y
200
>>> id(p) != id(p2)
True

定義は名前空間にある必要があり、pickleが定義を見つけるのに十分な長さで存在する必要があります。したがって、これをパッケージに定義すると、動作するはずです。

Point = namedgroup("Point", ["x", "y"])

次の操作を行うか、定義を一時的にすると(関数が終了したときにスコープ外になるなど)、Pickleは失敗します。

some_point = namedgroup("Point", ["x", "y"])

そして、はい、型の作成でリストされたフィールドの順序は保持されます。


__iter__メソッドを追加するとfor k in self._attrs_: yield getattr(self, k)、タプルのようなアンパックがサポートされます。
snapshoe

それはかなり簡単に追加することもあります__len____getitem____setiem__同様に、インデックスによって取得valusをサポートするための方法をp[0]。これらの最後のビットで、これは(とにかく私にとって)最も完全で正しい答えのようです。
snapshoe 2015

__len____iter__良いです。 __getitem__そして__setitem__、本当ににマッピングすることができるself.__dict__.__setitem__self.__dict__.__getitem__
MadMan2064

2

タプルは本質的に不変です。

ただし、ドット表記で属性にアクセスできるディクショナリサブクラスを作成できます。

In [1]: %cpaste
Pasting code; enter '--' alone on the line to stop or use Ctrl-D.
:class AttrDict(dict):
:
:    def __getattr__(self, name):
:        return self[name]
:
:    def __setattr__(self, name, value):
:        self[name] = value
:--

In [2]: test = AttrDict()

In [3]: test.a = 1

In [4]: test.b = True

In [5]: test
Out[5]: {'a': 1, 'b': True}

2

namedtuplesと同様の動作が必要だが変更可能な場合は、namedlistを試してください

変更可能であるためには、タプルにすることはできません


リンクをありがとう。これはこれまでのところ最も近いように見えますが、より詳細に評価する必要があります。ところで、私はタプルが不変であることを完全に認識しているので、namedtupleのようなソリューションを探しています。
アレクサンダー

0

パフォーマンスがそれほど重要でない場合、次のような愚かなハックを使用できます。

from collection import namedtuple

Point = namedtuple('Point', 'x y z')
mutable_z = Point(1,2,[3])

1
この答えはあまりよく説明されていません。リストの変更可能な性質を理解していないと、混乱を招きます。---この例では...再割り当てzするには、mutable_z.z.pop(0)次に呼び出す必要がありますmutable_z.z.append(new_value)。これを間違えると、要素が2つ以上になり、プログラムが予期しない動作をします。
byxor 2017

1
@byxorこと、またはあなただけでした:mutable_z.z[0] = newValue。述べたように、それは確かにハックです。
Srg 2018

そうそう、それを再割り当てするためのより明白な方法を見逃していたのには驚きました。
byxor

私はそれが好きです、本当のハック。
WebOrCode
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.