Sự tồn tại của tuple có tên có thể thay đổi trong Python?


121

Có ai có thể sửa đổi têntuple hoặc cung cấp một lớp thay thế để nó hoạt động cho các đối tượng có thể thay đổi được không?

Chủ yếu để dễ đọc, tôi muốn một cái gì đó tương tự như têntuple thực hiện điều này:

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)

Nó phải có khả năng chọn đối tượng kết quả. Và theo các đặc điểm của tuple được đặt tên, thứ tự của đầu ra khi được biểu diễn phải khớp với thứ tự của danh sách tham số khi xây dựng đối tượng.


3
Xem thêm: stackoverflow.com/q/5131044 . Có lý do gì bạn không thể chỉ sử dụng từ điển?
senshin

@senshin Cảm ơn vì liên kết. Tôi không thích sử dụng từ điển vì lý do được chỉ ra trong đó. Phản hồi đó cũng được liên kết với code.activestate.com/recipes/… , khá gần với những gì tôi đang theo dõi.
Alexander

Không giống như với namedtuples, có vẻ như bạn không cần phải có thể tham chiếu các thuộc tính theo chỉ mục, tức là như vậy p[0]p[1]sẽ là các cách thay thế để tham chiếu xytương ứng, đúng không?
martineau

Lý tưởng nhất là có, có thể lập chỉ mục theo vị trí như một tuple đơn giản ngoài tên và giải nén như một tuple. Công thức ActiveState này gần giống, nhưng tôi tin rằng nó sử dụng một từ điển thông thường thay vì một OrderedDict. code.activestate.com/recipes/500261
Alexander

2
Một trùng tên có thể thay đổi được gọi là một lớp.
gbtimmon

Câu trả lời:


132

Có một sự thay thế có thể thay đổi cho collections.namedtuple- lớp ghi .

Nó có cùng API và dấu chân bộ nhớ namedtuplevà nó hỗ trợ các bài tập (Nó cũng sẽ nhanh hơn). Ví dụ:

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)

Đối với python 3.6 trở lên recordclass(kể từ 0.5) hỗ trợ kiểu chữ:

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)

Có một ví dụ đầy đủ hơn (nó cũng bao gồm so sánh hiệu suất).

recordclassthư viện 0.9 cung cấp một biến thể khác - recordclass.structclasshàm nhà máy. Nó có thể tạo ra các lớp mà các cá thể của chúng chiếm ít bộ nhớ hơn các __slots__cá thể dựa trên cơ sở. Điều này có thể quan trọng đối với các trường hợp có giá trị thuộc tính, không có ý định có chu trình tham chiếu. Nó có thể giúp giảm mức sử dụng bộ nhớ nếu bạn cần tạo hàng triệu phiên bản. Đây là một ví dụ minh họa .


4
Thích nó. 'Thư viện này thực sự là một "bằng chứng về khái niệm" cho vấn đề thay thế "có thể thay đổi" của tuple được đặt tên
Alexander

1
recordclasschậm hơn, tốn nhiều bộ nhớ hơn và yêu cầu phần mở rộng C so với công thức của Antti Haapala và namedlist.
GrantJ

recordclasslà một phiên bản có thể thay đổi của collection.namedtuplenó kế thừa api của nó, dấu chân bộ nhớ, nhưng hỗ trợ các phép gán. namedlistthực sự là trường hợp của lớp python có khe. Sẽ hữu ích hơn nếu bạn không cần truy cập nhanh vào các trường của nó theo chỉ mục.
intellimath

Truy cập thuộc tính cho recordclassví dụ (python 3.5.2) là chậm hơn so với khoảng 2-3%namedlist
intellimath

Khi sử dụng namedtuplevà tạo lớp đơn giản Point = namedtuple('Point', 'x y'), Jedi có thể tự động điền các thuộc tính, trong khi điều này không đúng với trường hợp này recordclass. Nếu tôi sử dụng mã tạo dài hơn (dựa trên RecordClass), thì Jedi hiểu Pointlớp chứ không phải hàm tạo hoặc thuộc tính của nó ... Có cách nào recordclassđể làm việc hiệu quả với Jedi không?
PhilMacKay

34

type.SimpleNamespace được giới thiệu trong Python 3.3 và hỗ trợ các yêu cầu được yêu cầu.

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
Tôi đã tìm kiếm một thứ như thế này trong nhiều năm. Vĩ đại thay thế cho một thư viện dict rải rác như dotmap
Axwell

1
Điều này cần nhiều lượt ủng hộ hơn. Đó chính xác là những gì OP đang tìm kiếm, nó nằm trong thư viện tiêu chuẩn và không thể đơn giản hơn để sử dụng. Cảm ơn!
Tom Zych

3
-1 OP đã nói rất rõ ràng với các thử nghiệm của mình những gì anh ta cần và SimpleNamespacekhông thành công các thử nghiệm 6-10 (truy cập theo chỉ mục, giải nén lặp đi lặp lại, lặp lại, dict có thứ tự, thay thế tại chỗ) và 12, 13 (trường, vị trí). Lưu ý rằng tài liệu (mà bạn đã liên kết trong câu trả lời) cho biết cụ thể " SimpleNamespacecó thể hữu ích để thay thế class NS: pass. Tuy nhiên, namedtuple()thay vào đó , đối với loại bản ghi có cấu trúc, hãy sử dụng ".
Ali

1
-1 cũng vậy, SimpleNamespacetạo một đối tượng, không phải là một phương thức khởi tạo lớp và không thể thay thế cho nametuple. So sánh kiểu sẽ không hoạt động và dung lượng bộ nhớ sẽ cao hơn nhiều.
RedGlyph

26

Là một giải pháp thay thế rất Pythonic cho nhiệm vụ này, kể từ Python-3.7, bạn có thể sử dụng dataclassesmô-đun không chỉ hoạt động như một biến có thể thay đổi được NamedTuplevì chúng sử dụng các định nghĩa lớp bình thường mà còn hỗ trợ các tính năng lớp khác.

Từ PEP-0557:

Mặc dù chúng sử dụng một cơ chế rất khác, các Lớp Dữ liệu có thể được coi là "các nhóm có tên có thể thay đổi với các giá trị mặc định". Bởi vì Lớp dữ liệu sử dụng cú pháp định nghĩa lớp bình thường, bạn có thể tự do sử dụng kế thừa, metaclasses, docstrings, phương thức do người dùng định nghĩa, nhà máy lớp và các tính năng khác của lớp Python.

Một trình trang trí lớp được cung cấp để kiểm tra định nghĩa lớp cho các biến có chú thích kiểu như được định nghĩa trong PEP 526 , "Cú pháp cho chú thích biến". Trong tài liệu này, các biến như vậy được gọi là trường. Sử dụng các trường này, trình trang trí thêm các định nghĩa phương thức đã tạo vào lớp để hỗ trợ khởi tạo phiên bản, đại diện, các phương thức so sánh và các phương thức tùy chọn khác như được mô tả trong phần Đặc tả . Một lớp như vậy được gọi là Lớp dữ liệu, nhưng thực sự không có gì đặc biệt về lớp: trình trang trí thêm các phương thức đã tạo vào lớp và trả về cùng lớp mà nó đã được cấp.

Tính năng này được giới thiệu trong PEP-0557 mà bạn có thể đọc chi tiết hơn về nó trên liên kết tài liệu được cung cấp.

Thí dụ:

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

Bản giới thiệu:

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
Nó được làm rất rõ ràng với các bài kiểm tra trong OP những gì cần thiết và dataclasskhông thành công các bài kiểm tra 6-10 (truy cập theo chỉ mục, giải nén lặp đi lặp lại, lặp lại, lệnh dict, thay thế tại chỗ) và 12, 13 (trường, vị trí) trong Python 3.7 .1.
Ali

1
mặc dù đây có thể không phải là điều mà OP đang tìm kiếm cụ thể, nhưng nó chắc chắn đã giúp tôi :)
Martin CR

25

Danh sách có tên 1.7 mới nhất vượt qua tất cả các bài kiểm tra của bạn với cả Python 2.7 và Python 3.5 kể từ ngày 11 tháng 1 năm 2016. Đây là một triển khai python thuần túy trong khi đó recordclasslà một phần mở rộng C. Tất nhiên, nó phụ thuộc vào yêu cầu của bạn liệu phần mở rộng C có được ưu tiên hay không.

Kiểm tra của bạn (nhưng cũng có thể xem ghi chú bên dưới):

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

Đầu ra trên Python 2.7

1. Sự đột biến của các giá trị trường  
p: 10, 12

2. Chuỗi  
p: Điểm (x = 10, y = 12)

3. Đại diện  
Điểm (x = 10, y = 12) 

4. Kích thước  
kích thước của p: 64 

5. Truy cập theo tên trường  
p: 10, 12

6. Truy cập theo chỉ mục  
p: 10, 12

7. Giải nén lặp đi lặp lại  
p: 10, 12

8. Lặp lại  
p: [10, 12]

9. Số thứ tự  
p: OrderedDict ([('x', 10), ('y', 12)])

10. Thay thế tại chỗ (cập nhật?)  
p: Điểm (x = 100, y = 200)

11. Pickle and Unpickle  
Ngâm thành công

12. Các lĩnh vực  
p: ('x', 'y')

13. Slots  
p: ('x', 'y')

Sự khác biệt duy nhất với Python 3.5 là nó namedlistđã trở nên nhỏ hơn, kích thước là 56 (Python 2.7 báo cáo 64).

Lưu ý rằng tôi đã thay đổi bài kiểm tra 10 của bạn để thay thế tại chỗ. Các namedlistcó một _replace()phương pháp mà hiện một bản sao cạn, và làm cho cảm giác hoàn hảo đối với tôi bởi vì namedtupletrong thư viện chuẩn hành xử theo cùng một cách. Thay đổi ngữ nghĩa của _replace()phương thức sẽ rất khó hiểu. Theo tôi, _update()phương pháp này nên được sử dụng để cập nhật tại chỗ. Hoặc có thể tôi không hiểu được ý định của bài kiểm tra 10 của bạn?


Có sắc thái quan trọng. Các namedlistgiá trị lưu trữ trong trường hợp danh sách. Cái này là cpython's listthực sự là một mảng động. Theo thiết kế, nó phân bổ nhiều bộ nhớ hơn mức cần thiết để làm cho sự đột biến của danh sách trở nên rẻ hơn.
intellimath

1
Danh sách tên @intellimath hơi nhầm lẫn. Nó không thực sự kế thừa listvà theo mặc định sử dụng __slots__tối ưu hóa. Khi tôi đo, sử dụng bộ nhớ ít hơn recordclass: 96 byte vs 104 byte cho sáu trường trên Python 2.7
GrantJ

@GrantJ Có. recorclasssử dụng nhiều bộ nhớ hơn vì nó là một tupleđối tượng giống với kích thước bộ nhớ thay đổi.
intellimath

2
Phiếu phản đối ẩn danh không giúp ích cho ai cả. Câu trả lời có gì sai? Tại sao lại ủng hộ?
Ali

Tôi thích sự an toàn chống lại lỗi chính tả mà nó mang lại types.SimpleNamespace. Thật không may, pylint không thích nó :-(
xverges 21/12/18

23

Có vẻ như câu trả lời cho câu hỏi này là không.

Dưới đây là khá gần, nhưng nó không thể thay đổi về mặt kỹ thuật. Đây là tạo một phiên bản mới namedtuple()với giá trị x được cập nhật:

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

Mặt khác, bạn có thể tạo một lớp đơn giản bằng cách sử dụng __slots__sẽ hoạt động tốt để thường xuyên cập nhật các thuộc tính cá thể của lớp:

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

Để thêm vào câu trả lời này, tôi nghĩ __slots__cách sử dụng tốt ở đây vì nó hiệu quả về bộ nhớ khi bạn tạo nhiều cá thể lớp. Nhược điểm duy nhất là bạn không thể tạo các thuộc tính lớp mới.

Đây là một chuỗi có liên quan minh họa hiệu quả bộ nhớ - Dictionary vs Object - cái nào hiệu quả hơn và tại sao?

Nội dung được trích dẫn trong câu trả lời của chủ đề này là một lời giải thích rất ngắn gọn tại sao __slots__bộ nhớ hiệu quả hơn - Các khe cắm Python


1
Gần, nhưng lắt léo. Hãy nói rằng tôi muốn làm một + = chuyển nhượng, sau đó tôi sẽ cần phải làm: p._replace (x = px + 10) vs px + = 10
Alexander

1
vâng, nó không thực sự thay đổi bộ tuple hiện có, nó đang tạo ra một phiên bản mới
kennes

7

Sau đây là một giải pháp tốt cho Python 3: Một lớp tối thiểu sử dụng __slots__Sequencelớp cơ sở trừu tượng; không phát hiện lỗi ưa thích hoặc tương tự, nhưng nó hoạt động và hoạt động chủ yếu giống như một bộ tuple có thể thay đổi (ngoại trừ lỗi đánh máy).

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

Thí dụ:

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

Nếu bạn muốn, bạn cũng có thể có một phương thức để tạo lớp (mặc dù sử dụng một lớp rõ ràng sẽ minh bạch hơn):

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

Thí dụ:

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

Trong Python 2, bạn cần điều chỉnh nó một chút - nếu bạn kế thừa từ Sequence, lớp sẽ có dấu__dict____slots__sẽ ngừng hoạt động.

Giải pháp trong Python 2 là không kế thừa từ Sequence, nhưng object. Nếu isinstance(Point, Sequence) == Truemuốn, bạn cần đăng ký NamedMutableSequencedưới dạng lớp cơ sở để Sequence:

Sequence.register(NamedMutableSequence)

3

Hãy thực hiện điều này với tạo kiểu động:

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)

Thao tác này sẽ kiểm tra các thuộc tính để xem chúng có hợp lệ hay không trước khi cho phép tiếp tục hoạt động.

Vậy cái này có ngâm được không? Có nếu (và chỉ khi) bạn làm như sau:

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

Định nghĩa phải nằm trong không gian tên của bạn và phải tồn tại đủ lâu để pickle tìm thấy nó. Vì vậy, nếu bạn xác định điều này là trong gói của bạn, nó sẽ hoạt động.

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

Pickle sẽ không thành công nếu bạn làm như sau hoặc đặt định nghĩa tạm thời (vượt ra ngoài phạm vi khi hàm kết thúc, chẳng hạn):

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

Và có, nó bảo toàn thứ tự của các trường được liệt kê trong phần tạo kiểu.


Nếu bạn thêm một __iter__phương thức với for k in self._attrs_: yield getattr(self, k), phương thức đó sẽ hỗ trợ giải nén như một bộ tuple.
snapshoe

Nó cũng khá dễ dàng để thêm __len__, __getitem____setiem__phương pháp để hỗ trợ nhận valus bởi chỉ số, như p[0]. Với những bit cuối cùng này, đây có vẻ như là câu trả lời đầy đủ và chính xác nhất (với tôi dù sao).
snapshoe

__len____iter__tốt. __getitem____setitem__thực sự có thể được ánh xạ tới self.__dict__.__setitem__self.__dict__.__getitem__
MadMan2064

2

Tuples theo định nghĩa là bất biến.

Tuy nhiên, bạn có thể tạo một lớp con từ điển nơi bạn có thể truy cập các thuộc tính bằng ký hiệu dấu chấm;

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

Nếu bạn muốn hành vi tương tự như các nhóm có tên nhưng có thể thay đổi, hãy thử danh sách có tên

Lưu ý rằng để có thể thay đổi, nó không thể là một bộ.


Cảm ơn các liên kết. Đây có vẻ là gần nhất cho đến nay, nhưng tôi cần đánh giá nó chi tiết hơn. Btw, tôi hoàn toàn biết rằng các bộ giá trị là bất biến, đó là lý do tại sao tôi đang tìm kiếm một giải pháp như têntuple.
Alexander

0

Hiệu suất được cung cấp không quan trọng lắm, người ta có thể sử dụng một thủ thuật ngớ ngẩn như:

from collection import namedtuple

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

1
Câu trả lời này không được giải thích rõ ràng. Có vẻ khó hiểu nếu bạn không hiểu bản chất có thể thay đổi của danh sách. --- Trong ví dụ này ... để gán lại z, bạn phải gọi mutable_z.z.pop(0)sau đó mutable_z.z.append(new_value). Nếu bạn làm sai điều này, bạn sẽ có nhiều hơn 1 phần tử và chương trình của bạn sẽ hoạt động không như mong muốn.
byxor 19/09/17

1
@byxor đó, hoặc bạn có thể chỉ: mutable_z.z[0] = newValue. Nó thực sự là một vụ hack, như đã nói.
Srg

Ồ, tôi ngạc nhiên là tôi đã bỏ lỡ cách rõ ràng hơn để chỉ định lại nó.
byxor

Tôi thích nó, hack thực sự.
WebOrCode
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.