[python] 사전 대 객체-어느 것이 더 효율적이며 그 이유는 무엇입니까?

메모리 사용량 및 CPU 소비 측면에서 Python에서 더 효율적인 것은 무엇입니까?

배경 :
엄청난 양의 데이터를 Python에로드해야합니다. 필드 컨테이너 인 개체를 만들었습니다. 4M 인스턴스를 만들고 사전에 저장하는 데 약 10 분이 소요되고 메모리는 최대 6GB였습니다. 사전이 준비된 후 액세스하는 것은 눈 깜짝 할 사이입니다.

예 :
성능을 확인하기 위해 동일한 작업을 수행하는 두 개의 간단한 프로그램을 작성했습니다. 하나는 객체를 사용하고 다른 하나는 사전을 사용하는 것입니다.

개체 (실행 시간 ~ 18 초) :

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

사전 (실행 시간 ~ 12 초) :

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

질문 :
내가 뭘 잘못하고 있거나 사전이 객체보다 빠르나요? 실제로 사전이 더 잘 작동한다면 누군가 이유를 설명 할 수 있습니까?



답변

사용해 보셨습니까 __slots__?

로부터 문서 :

기본적으로 이전 및 새 스타일 클래스의 인스턴스에는 속성 저장을위한 사전이 있습니다. 이것은 인스턴스 변수가 거의없는 객체를위한 공간을 낭비합니다. 많은 수의 인스턴스를 생성 할 때 공간 소비가 급증 할 수 있습니다.

__slots__새 스타일 클래스 정의에서 정의 하여 기본값을 재정의 할 수 있습니다 . __slots__선언 각 변수에 대한 값을 유지하도록 각각의 인스턴스 인스턴스 변수 보유 충분한 공간의 시퀀스 걸린다. __dict__각 인스턴스에 대해 생성되지 않으므로 공간이 절약 됩니다.

그러면 시간과 메모리가 절약됩니까?

내 컴퓨터의 세 가지 접근 방식 비교 :

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 (2.6에서 지원) :

import collections

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

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

벤치 마크 실행 (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__)

명명 된 튜플 테스트를 포함하여 CPython 2.6.2 사용 :

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

그래서 예 (정말 놀라운 것은 아닙니다), 사용 __slots__은 성능 최적화입니다. 명명 된 튜플을 사용하면 __slots__.


답변

객체의 속성 액세스는 배후에서 사전 액세스를 사용하므로 속성 액세스를 사용하면 추가 오버 헤드가 추가됩니다. 또한 객체의 경우 추가 메모리 할당 및 코드 실행 (예 : __init__메서드)으로 인해 추가 오버 헤드가 발생 합니다.

경우 코드에서 o입니다 Obj인스턴스 o.attr에 해당 o.__dict__['attr']추가 오버 헤드의 소량.


답변

namedtuple 사용을 고려해 보셨습니까 ? ( python 2.4 / 2.5 링크 )

튜플의 성능과 클래스의 편리함을 제공하는 구조화 된 데이터를 나타내는 새로운 표준 방식입니다.

사전과 비교할 때 유일한 단점은 (튜플과 같은) 생성 후 속성을 변경할 수있는 기능을 제공하지 않는다는 것입니다.


답변

다음은 파이썬 3.6.1에 대한 @hughdbrown 답변의 사본입니다. 저는 카운트를 5 배 더 크게 만들고 각 실행이 끝날 때마다 파이썬 프로세스의 메모리 풋 프린트를 테스트하는 코드를 추가했습니다.

반대 투표자들이 그것을하기 전에, 물체의 크기를 계산하는이 방법은 정확하지 않다는 것을 알려드립니다.

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

그리고 이것은 내 결과입니다

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

내 결론은 :

  1. 슬롯은 최고의 메모리 풋 프린트를 가지며 속도면에서 합리적입니다.
  2. dicts가 가장 빠르지 만 가장 많은 메모리를 사용합니다.

답변

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

결과 :

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


답변

의문의 여지가 없습니다.
다른 속성이없는 데이터가 있습니다 (메서드도, 아무것도 없음). 따라서 데이터 컨테이너 (이 경우 사전)가 있습니다.

나는 일반적으로 데이터 모델링 측면에서 생각하는 것을 선호합니다 . 큰 성능 문제가 있으면 추상화에서 무언가를 포기할 수 있지만 아주 좋은 이유가 있어야합니다.
프로그래밍은 복잡성을 관리하고 올바른 추상화를 유지하는 것입니다. 것은 이러한 결과를 달성하는 가장 유용한 방법 중 하나입니다.

[정보 이유 객체가 느립니다, 당신의 측정이 정확하지라고 생각합니다.
for 루프 내에서 할당을 너무 적게 수행하므로 dict (내재 개체)와 “사용자 지정”개체를 인스턴스화하는 데 필요한 시간이 다릅니다. 언어 관점에서 볼 때 동일하지만 구현이 상당히 다릅니다.
그 후에는 최종 구성원이 사전 내에 유지되므로 할당 시간은 둘 다에 대해 거의 동일해야합니다.


답변

데이터 구조에 참조주기가 포함되어 있지 않은 경우 메모리 사용량을 줄이는 또 다른 방법이 있습니다.

두 클래스를 비교해 보겠습니다.

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

structclass기반 클래스는 이러한 경우 필요하지 않은 순환 가비지 수집을 지원하지 않기 때문에 가능해졌습니다 .

__slots__기반 클래스에 비해 한 가지 장점 이 있습니다. 추가 속성을 추가 할 수 있습니다.

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