Dictionary vs Object - cái nào hiệu quả hơn và tại sao?


126

Python nào hiệu quả hơn về mặt sử dụng bộ nhớ và mức tiêu thụ CPU - Dictionary hay Object?

Bối cảnh: Tôi phải tải một lượng lớn dữ liệu vào Python. Tôi đã tạo một đối tượng chỉ là một thùng chứa trường. Tạo các bản sao 4M và đưa chúng vào từ điển mất khoảng 10 phút và ~ 6GB bộ nhớ. Sau khi từ điển đã sẵn sàng, truy cập nó là một cái chớp mắt.

Ví dụ: Để kiểm tra hiệu suất, tôi đã viết hai chương trình đơn giản giống nhau - một là sử dụng các đối tượng, từ điển khác:

Đối tượng (thời gian thực hiện ~ 18 giây):

class Obj(object):
  def __init__(self, i):
    self.i = i
    self.l = []
all = {}
for i in range(1000000):
  all[i] = Obj(i)

Từ điển (thời gian thực hiện ~ 12 giây):

all = {}
for i in range(1000000):
  o = {}
  o['i'] = i
  o['l'] = []
  all[i] = o

Câu hỏi: Tôi đang làm gì đó sai hay từ điển chỉ nhanh hơn đối tượng? Nếu thực sự từ điển hoạt động tốt hơn, ai đó có thể giải thích tại sao?


10
Bạn thực sự nên sử dụng xrange thay vì phạm vi khi tạo các chuỗi lớn như thế. Tất nhiên, vì bạn đang xử lý vài giây thời gian thực hiện, nó sẽ không tạo ra nhiều khác biệt, nhưng vẫn là một thói quen tốt.
Xiong Chiamiov

2
trừ khi đó là python3
Barney

Câu trả lời:


157

Bạn đã thử sử dụng __slots__?

Từ tài liệu :

Theo mặc định, các thể hiện của cả các lớp kiểu cũ và kiểu mới đều có một từ điển để lưu trữ thuộc tính. Điều này làm lãng phí không gian cho các đối tượng có rất ít biến thể hiện. Tiêu thụ không gian có thể trở nên cấp tính khi tạo ra số lượng lớn các trường hợp.

Mặc định có thể được ghi đè bằng cách định nghĩa __slots__trong định nghĩa lớp kiểu mới. Các __slots__tuyên bố mất một chuỗi các biến dụ và dự trữ chỉ đủ không gian trong mỗi trường hợp để giữ giá trị cho mỗi biến. Không gian được lưu vì __dict__không được tạo cho mỗi phiên bản.

Vì vậy, điều này có tiết kiệm thời gian cũng như bộ nhớ?

So sánh ba cách tiếp cận trên máy tính của tôi:

test_slots.py:

class Obj(object):
  __slots__ = ('i', 'l')
  def __init__(self, i):
    self.i = i
    self.l = []
all = {}
for i in range(1000000):
  all[i] = Obj(i)

test_obj.py:

class Obj(object):
  def __init__(self, i):
    self.i = i
    self.l = []
all = {}
for i in range(1000000):
  all[i] = Obj(i)

test_dict.py:

all = {}
for i in range(1000000):
  o = {}
  o['i'] = i
  o['l'] = []
  all[i] = o

test_namedtuple.py (được hỗ trợ trong 2.6):

import collections

Obj = collections.namedtuple('Obj', 'i l')

all = {}
for i in range(1000000):
  all[i] = Obj(i, [])

Chạy điểm chuẩn (sử dụng CPython 2.5):

$ lshw | grep product | head -n 1
          product: Intel(R) Pentium(R) M processor 1.60GHz
$ python --version
Python 2.5
$ time python test_obj.py && time python test_dict.py && time python test_slots.py 

real    0m27.398s (using 'normal' object)
real    0m16.747s (using __dict__)
real    0m11.777s (using __slots__)

Sử dụng CPython 2.6.2, bao gồm kiểm tra bộ dữ liệu có tên:

$ python --version
Python 2.6.2
$ time python test_obj.py && time python test_dict.py && time python test_slots.py && time python test_namedtuple.py 

real    0m27.197s (using 'normal' object)
real    0m17.657s (using __dict__)
real    0m12.249s (using __slots__)
real    0m12.262s (using namedtuple)

Vì vậy, có (không thực sự gây ngạc nhiên), sử dụng __slots__là tối ưu hóa hiệu suất. Sử dụng một tuple có tên có hiệu suất tương tự __slots__.


2
Điều đó thật tuyệt - cảm ơn! Tôi đã thử tương tự trên máy của mình - đối tượng có khe là cách tiếp cận hiệu quả nhất (tôi có ~ 7 giây).
tkokoszka

6
Ngoài ra còn có các tuples, docs.python.org/l Library / collections.html # collections.namedtuple , một nhà máy lớp cho các đối tượng có vị trí. Nó chắc chắn gọn gàng hơn và thậm chí có thể tối ưu hơn.
Jochen Ritzel

Tôi cũng đã thử nghiệm các bộ dữ liệu có tên và cập nhật câu trả lời với kết quả.
codeape

1
Tôi đã chạy mã của bạn một vài lần và rất ngạc nhiên khi kết quả của tôi khác nhau - slot = 3sec obj = 11sec dict = 12sec namtuple = 16sec. Tôi đang sử dụng CPython 2.6.6 trên Win7 64 bit
Jonathan

Để nhấn mạnh đến cú đấm - têntuple có kết quả tồi tệ nhất thay vì tốt nhất
Jonathan

15

Quyền truy cập thuộc tính trong một đối tượng sử dụng quyền truy cập từ điển phía sau hậu trường - vì vậy bằng cách sử dụng quyền truy cập thuộc tính, bạn sẽ thêm chi phí phụ. Ngoài ra, trong trường hợp đối tượng, bạn phải chịu thêm chi phí vì ví dụ phân bổ bộ nhớ bổ sung và thực thi mã (ví dụ: của __init__phương thức).

Trong mã của bạn, nếu olà một Objthể hiện, o.attrtương đương o.__dict__['attr']với một lượng nhỏ chi phí phụ.


Bạn đã kiểm tra điều này? o.__dict__["attr"]là một cái có thêm chi phí hoạt động, lấy thêm một bytecode op; obj.attr là nhanh hơn. (Tất nhiên quyền truy cập thuộc tính sẽ không chậm hơn quyền truy cập đăng ký - đó là đường dẫn mã được tối ưu hóa nghiêm trọng.)
Glenn Maynard

2
Rõ ràng nếu bạn thực sự làm o .__ dict __ ["attr"] thì nó sẽ chậm hơn - tôi chỉ có ý nói rằng nó tương đương với điều đó, chứ không phải nó được thực hiện chính xác theo cách đó. Tôi đoán nó không rõ ràng từ ngữ của tôi. Tôi cũng đã đề cập đến các yếu tố khác như phân bổ bộ nhớ, thời gian gọi của nhà xây dựng, v.v.
Vinay Sajip

Đây có còn là trường hợp với các phiên bản gần đây của python3, 11 năm sau không?
matanster

9

Bạn đã xem xét sử dụng một têntuple ? ( liên kết cho python 2.4 / 2.5 )

Đó là cách tiêu chuẩn mới để biểu diễn dữ liệu có cấu trúc mang lại cho bạn hiệu suất của một tuple và sự tiện lợi của một lớp.

Nó chỉ có nhược điểm so với từ điển là (như bộ dữ liệu) nó không cung cấp cho bạn khả năng thay đổi thuộc tính sau khi tạo.


5

Đây là bản sao câu trả lời @hughdbrown cho python 3.6.1, tôi đã làm cho số đếm lớn hơn 5x và thêm một số mã để kiểm tra dấu chân bộ nhớ của quá trình python vào cuối mỗi lần chạy.

Trước khi các downvoters có nó, hãy lưu ý rằng phương pháp đếm kích thước của các đối tượng là không chính xác.

from datetime import datetime
import os
import psutil

process = psutil.Process(os.getpid())


ITER_COUNT = 1000 * 1000 * 5

RESULT=None

def makeL(i):
    # Use this line to negate the effect of the strings on the test 
    # return "Python is smart and will only create one string with this line"

    # Use this if you want to see the difference with 5 million unique strings
    return "This is a sample string %s" % i

def timeit(method):
    def timed(*args, **kw):
        global RESULT
        s = datetime.now()
        RESULT = method(*args, **kw)
        e = datetime.now()

        sizeMb = process.memory_info().rss / 1024 / 1024
        sizeMbStr = "{0:,}".format(round(sizeMb, 2))

        print('Time Taken = %s, \t%s, \tSize = %s' % (e - s, method.__name__, sizeMbStr))

    return timed

class Obj(object):
    def __init__(self, i):
       self.i = i
       self.l = makeL(i)

class SlotObj(object):
    __slots__ = ('i', 'l')
    def __init__(self, i):
       self.i = i
       self.l = makeL(i)

from collections import namedtuple
NT = namedtuple("NT", ["i", 'l'])

@timeit
def profile_dict_of_nt():
    return [NT(i=i, l=makeL(i)) for i in range(ITER_COUNT)]

@timeit
def profile_list_of_nt():
    return dict((i, NT(i=i, l=makeL(i))) for i in range(ITER_COUNT))

@timeit
def profile_dict_of_dict():
    return dict((i, {'i': i, 'l': makeL(i)}) for i in range(ITER_COUNT))

@timeit
def profile_list_of_dict():
    return [{'i': i, 'l': makeL(i)} for i in range(ITER_COUNT)]

@timeit
def profile_dict_of_obj():
    return dict((i, Obj(i)) for i in range(ITER_COUNT))

@timeit
def profile_list_of_obj():
    return [Obj(i) for i in range(ITER_COUNT)]

@timeit
def profile_dict_of_slot():
    return dict((i, SlotObj(i)) for i in range(ITER_COUNT))

@timeit
def profile_list_of_slot():
    return [SlotObj(i) for i in range(ITER_COUNT)]

profile_dict_of_nt()
profile_list_of_nt()
profile_dict_of_dict()
profile_list_of_dict()
profile_dict_of_obj()
profile_list_of_obj()
profile_dict_of_slot()
profile_list_of_slot()

Và đây là kết quả của tôi

Time Taken = 0:00:07.018720,    provile_dict_of_nt,     Size = 951.83
Time Taken = 0:00:07.716197,    provile_list_of_nt,     Size = 1,084.75
Time Taken = 0:00:03.237139,    profile_dict_of_dict,   Size = 1,926.29
Time Taken = 0:00:02.770469,    profile_list_of_dict,   Size = 1,778.58
Time Taken = 0:00:07.961045,    profile_dict_of_obj,    Size = 1,537.64
Time Taken = 0:00:05.899573,    profile_list_of_obj,    Size = 1,458.05
Time Taken = 0:00:06.567684,    profile_dict_of_slot,   Size = 1,035.65
Time Taken = 0:00:04.925101,    profile_list_of_slot,   Size = 887.49

Kết luận của tôi là:

  1. Slots có dung lượng bộ nhớ tốt nhất và hợp lý về tốc độ.
  2. dicts là nhanh nhất, nhưng sử dụng nhiều bộ nhớ nhất.

Man, bạn nên biến điều này thành một câu hỏi. Tôi cũng chạy nó trên máy tính của mình, chỉ để đảm bảo (tôi chưa cài đặt psutil, vì vậy tôi đã lấy phần đó ra). Dù sao, điều này gây trở ngại cho tôi, và có nghĩa là câu hỏi ban đầu không được trả lời đầy đủ. Tất cả các câu trả lời khác giống như "nametuple là tuyệt vời" và "sử dụng vị trí ", và rõ ràng một đối tượng chính tả hoàn toàn mới mỗi lần nhanh hơn chúng? Tôi đoán dicts thực sự được tối ưu hóa?
Multihunter

1
Nó dường như là kết quả của hàm makeL trả về một chuỗi. Thay vào đó, nếu bạn trả về một danh sách trống, kết quả gần như khớp với hughdbrown từ python2. Ngoại trừ các tên được đặt luôn chậm hơn SlotObj :(
Multihunter

Có thể có một vấn đề nhỏ: makeL có thể chạy với tốc độ khác nhau trong mỗi vòng '@timeit' do các chuỗi được lưu trong bộ nhớ python - nhưng có lẽ tôi đã sai.
Barney

@BarnabasSzabolcs nên tạo một chuỗi mới mỗi lần vì nó phải thay thế trong giá trị "Đây là chuỗi mẫu% s"% i
Jarrod Chesney

Vâng, điều đó đúng trong vòng lặp, nhưng trong thử nghiệm thứ hai tôi lại bắt đầu từ 0.
Barney

4
from datetime import datetime

ITER_COUNT = 1000 * 1000

def timeit(method):
    def timed(*args, **kw):
        s = datetime.now()
        result = method(*args, **kw)
        e = datetime.now()

        print method.__name__, '(%r, %r)' % (args, kw), e - s
        return result
    return timed

class Obj(object):
    def __init__(self, i):
       self.i = i
       self.l = []

class SlotObj(object):
    __slots__ = ('i', 'l')
    def __init__(self, i):
       self.i = i
       self.l = []

@timeit
def profile_dict_of_dict():
    return dict((i, {'i': i, 'l': []}) for i in xrange(ITER_COUNT))

@timeit
def profile_list_of_dict():
    return [{'i': i, 'l': []} for i in xrange(ITER_COUNT)]

@timeit
def profile_dict_of_obj():
    return dict((i, Obj(i)) for i in xrange(ITER_COUNT))

@timeit
def profile_list_of_obj():
    return [Obj(i) for i in xrange(ITER_COUNT)]

@timeit
def profile_dict_of_slotobj():
    return dict((i, SlotObj(i)) for i in xrange(ITER_COUNT))

@timeit
def profile_list_of_slotobj():
    return [SlotObj(i) for i in xrange(ITER_COUNT)]

if __name__ == '__main__':
    profile_dict_of_dict()
    profile_list_of_dict()
    profile_dict_of_obj()
    profile_list_of_obj()
    profile_dict_of_slotobj()
    profile_list_of_slotobj()

Các kết quả:

hbrown@hbrown-lpt:~$ python ~/Dropbox/src/StackOverflow/1336791.py 
profile_dict_of_dict ((), {}) 0:00:08.228094
profile_list_of_dict ((), {}) 0:00:06.040870
profile_dict_of_obj ((), {}) 0:00:11.481681
profile_list_of_obj ((), {}) 0:00:10.893125
profile_dict_of_slotobj ((), {}) 0:00:06.381897
profile_list_of_slotobj ((), {}) 0:00:05.860749

3

Không có câu hỏi.
Bạn có dữ liệu, không có thuộc tính nào khác (không có phương thức, không có gì). Do đó bạn có một thùng chứa dữ liệu (trong trường hợp này là một từ điển).

Tôi thường thích suy nghĩ về mô hình dữ liệu . Nếu có một số vấn đề hiệu suất lớn, thì tôi có thể từ bỏ một cái gì đó trừu tượng, nhưng chỉ với những lý do rất tốt.
Lập trình là tất cả về việc quản lý sự phức tạp và việc duy trì sự trừu tượng chính xác thường là một trong những cách hữu ích nhất để đạt được kết quả như vậy.

Về lý do một đối tượng chậm hơn, tôi nghĩ rằng phép đo của bạn không chính xác.
Bạn đang thực hiện quá ít bài tập bên trong vòng lặp for, và do đó những gì bạn thấy có thời gian khác nhau cần thiết để khởi tạo một lệnh (đối tượng nội tại) và đối tượng "tùy chỉnh". Mặc dù từ góc độ ngôn ngữ chúng giống nhau, chúng có cách thực hiện khá khác nhau.
Sau đó, thời gian chuyển nhượng sẽ gần như giống nhau cho cả hai, vì cuối cùng các thành viên được duy trì trong một từ điển.


0

Vẫn còn một cách khác để giảm mức sử dụng bộ nhớ nếu cấu trúc dữ liệu không được cho là chứa chu kỳ tham chiếu.

Hãy so sánh hai lớp:

class DataItem:
    __slots__ = ('name', 'age', 'address')
    def __init__(self, name, age, address):
        self.name = name
        self.age = age
        self.address = address

$ pip install recordclass

>>> from recordclass import structclass
>>> DataItem2 = structclass('DataItem', 'name age address')
>>> inst = DataItem('Mike', 10, 'Cherry Street 15')
>>> inst2 = DataItem2('Mike', 10, 'Cherry Street 15')
>>> print(inst2)
>>> print(sys.getsizeof(inst), sys.getsizeof(inst2))
DataItem(name='Mike', age=10, address='Cherry Street 15')
64 40

Có thể vì structclasscác lớp dựa trên không hỗ trợ thu gom rác theo chu kỳ, điều không cần thiết trong những trường hợp như vậy.

Ngoài ra còn có một lợi thế của __slots__lớp dựa trên: bạn có thể thêm các thuộc tính bổ sung:

>>> DataItem3 = structclass('DataItem', 'name age address', usedict=True)
>>> inst3 = DataItem3('Mike', 10, 'Cherry Street 15')
>>> inst3.hobby = ['drawing', 'singing']
>>> print(inst3)
>>> print(sizeof(inst3), 'has dict:',  bool(inst3.__dict__))
DataItem(name='Mike', age=10, address='Cherry Street 15', **{'hobby': ['drawing', 'singing']})
48 has dict: True

0

Dưới đây là bản chạy thử của tôi về kịch bản rất hay của @ Jarrod-Chesney. Để so sánh, tôi cũng chạy nó với python2 với "phạm vi" được thay thế bằng "xrange".

Vì tò mò, tôi cũng đã thêm các thử nghiệm tương tự với OrderedDict (mệnh lệnh) để so sánh.

Python 3.6.9:

Time Taken = 0:00:04.971369,    profile_dict_of_nt,     Size = 944.27
Time Taken = 0:00:05.743104,    profile_list_of_nt,     Size = 1,066.93
Time Taken = 0:00:02.524507,    profile_dict_of_dict,   Size = 1,920.35
Time Taken = 0:00:02.123801,    profile_list_of_dict,   Size = 1,760.9
Time Taken = 0:00:05.374294,    profile_dict_of_obj,    Size = 1,532.12
Time Taken = 0:00:04.517245,    profile_list_of_obj,    Size = 1,441.04
Time Taken = 0:00:04.590298,    profile_dict_of_slot,   Size = 1,030.09
Time Taken = 0:00:04.197425,    profile_list_of_slot,   Size = 870.67

Time Taken = 0:00:08.833653,    profile_ordict_of_ordict, Size = 3,045.52
Time Taken = 0:00:11.539006,    profile_list_of_ordict, Size = 2,722.34
Time Taken = 0:00:06.428105,    profile_ordict_of_obj,  Size = 1,799.29
Time Taken = 0:00:05.559248,    profile_ordict_of_slot, Size = 1,257.75

Python 2.7.15+:

Time Taken = 0:00:05.193900,    profile_dict_of_nt,     Size = 906.0
Time Taken = 0:00:05.860978,    profile_list_of_nt,     Size = 1,177.0
Time Taken = 0:00:02.370905,    profile_dict_of_dict,   Size = 2,228.0
Time Taken = 0:00:02.100117,    profile_list_of_dict,   Size = 2,036.0
Time Taken = 0:00:08.353666,    profile_dict_of_obj,    Size = 2,493.0
Time Taken = 0:00:07.441747,    profile_list_of_obj,    Size = 2,337.0
Time Taken = 0:00:06.118018,    profile_dict_of_slot,   Size = 1,117.0
Time Taken = 0:00:04.654888,    profile_list_of_slot,   Size = 964.0

Time Taken = 0:00:59.576874,    profile_ordict_of_ordict, Size = 7,427.0
Time Taken = 0:10:25.679784,    profile_list_of_ordict, Size = 11,305.0
Time Taken = 0:05:47.289230,    profile_ordict_of_obj,  Size = 11,477.0
Time Taken = 0:00:51.485756,    profile_ordict_of_slot, Size = 11,193.0

Vì vậy, trên cả hai phiên bản chính, kết luận của @ Jarrod-Chesney vẫn có vẻ tốt.

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.