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


126

메모리 사용량 및 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

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


10
그런 큰 시퀀스를 생성 할 때는 범위 대신 xrange를 사용해야합니다. 물론, 몇 초의 실행 시간을 다루기 때문에 큰 차이는 없지만 그래도 좋은 습관입니다.
Xiong Chiamiov

2
하지 않는 한 그것은 python3 경우
바니

답변:


157

사용해 보셨습니까 __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__.


2
대단합니다-감사합니다! 나는 내 컴퓨터에서 똑같이 시도했습니다- 슬롯 이있는 객체 가 가장 효율적인 접근 방식입니다 (약 7 초를 얻었습니다).
tkokoszka

6
슬롯이있는 객체를위한 클래스 팩토리 인 docs.python.org/library/collections.html#collections.namedtuple 이라는 이름의 튜플도 있습니다 . 확실히 깔끔하고 더 최적화되었을 수 있습니다.
Jochen Ritzel

명명 된 튜플도 테스트하고 결과로 답변을 업데이트했습니다.
codeape

1
코드를 몇 번 실행했는데 결과가 다르다는 사실에 놀랐습니다-slots = 3sec obj = 11sec dict = 12sec namedtuple = 16sec. 저는 Win7 64 비트에서 CPython 2.6.6을 사용하고 있습니다
Jonathan

뒤통수 때리는 웃긴를 강조하기 위해 - namedtuple는 GOT의 최악의 대신 결과 최고
조나단

15

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

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


이것을 테스트 했습니까? o.__dict__["attr"]추가 오버 헤드가있는 하나이며 추가 바이트 코드 작업을 수행합니다. obj.attr이 더 빠릅니다. (물론 속성 액세스는 구독 액세스보다 느리지 않을 것입니다. 매우 최적화 된 중요 코드 경로입니다.)
Glenn Maynard

2
물론 당신이 실제로 있다면 않는 오 .__ DICT __ [ "ATTR"]가 느려집니다 - 나는 단지 그것을이 그런 식으로 정확히 구현하지 않는 것이, 그와 동등이라고 말을 의미했다. 내 말에서 명확하지 않은 것 같습니다. 또한 메모리 할당, 생성자 호출 시간 등과 같은 다른 요소도 언급했습니다.
Vinay Sajip 2009-08-26

11 년이 지난 python3의 최신 버전에서도 여전히 그렇습니까?
matanster

9

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

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

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


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가 가장 빠르지 만 가장 많은 메모리를 사용합니다.

이건 질문으로 바꿔야 해 확인하기 위해 내 컴퓨터에서도 실행했습니다 (psutil이 설치되지 않았으므로 해당 부분을 제거했습니다). 어쨌든 이것은 나에게 당혹스럽고 원래의 질문에 완전히 대답하지 않았 음을 의미합니다. 다른 모든 답변은 "namedtuple is great"및 "use slots " 와 같 으며 매번 새로운 dict 객체가 그들보다 빠릅니다. 딕셔너리는 정말 잘 최적화 된 것 같아요?
Multihunter

1
문자열을 반환하는 makeL 함수의 결과 인 것 같습니다. 대신 빈 목록을 반환하면 결과는 python2의 hughdbrown과 대략 일치합니다. 명명 된 튜플이 항상 SlotObj보다 느리다는 점을 제외하면 :(
Multihunter

작은 문제가있을 수 있습니다. 문자열이 파이썬에 캐시되기 때문에 makeL이 각 '@timeit'라운드에서 다른 속도로 실행될 수 있습니다.하지만 제가 틀렸을 수도 있습니다.
Barney

이 값으로 대체하기 때문에 @BarnabasSzabolcs 때마다 새로운 캐릭터를 만들어야합니다 내가 % "이것은 샘플 % s 문자열을입니다"
재로드 체스니

예, 그것은 루프 내에서 사실이지만 두 번째 테스트에서 i는 다시 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()

결과 :

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

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

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

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


0

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

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

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

0

다음은 @ Jarrod-Chesney의 아주 멋진 스크립트의 테스트 실행입니다. 비교를 위해 "범위"가 "xrange"로 대체 된 python2에 대해서도 실행합니다.

호기심으로 비교를 위해 OrderedDict (ordict)와 유사한 테스트를 추가했습니다.

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

따라서 두 가지 주요 버전에서 @ Jarrod-Chesney의 결론은 여전히 ​​좋아 보입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.