辞書対オブジェクト-どちらがより効率的で、なぜですか?


126

メモリ使用量とCPU消費量の点でPythonでより効率的なものは何ですか-辞書またはオブジェクト?

背景: 大量のデータをPythonにロードする必要があります。単なるフィールドコンテナーであるオブジェクトを作成しました。4Mのインスタンスを作成して辞書に入れるには、約10分と最大6GBのメモリが必要でした。辞書が準備できたら、それにアクセスするのは一瞬です。

例: パフォーマンスを確認するために、同じことをする2つの単純なプログラムを作成しました。1つはオブジェクトを使用し、もう1つは辞書を使用しています。

オブジェクト(実行時間〜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
このような大きなシーケンスを生成するときは、rangeではなくxrangeを実際に使用する必要があります。もちろん、実行時間の秒を扱っているので、それほど大きな違いはありませんが、それでも、それは良い習慣です。
Xiong Chiamiov、2009

2
それがpython3でない限り
バーニー

回答:


157

使ってみました__slots__か?

ドキュメントから:

デフォルトでは、古いスタイルと新しいスタイルの両方のクラスのインスタンスに、属性を格納するための辞書があります。これにより、インスタンス変数がほとんどないオブジェクトのスペースが無駄になります。大量のインスタンスを作成すると、スペースの消費が急激になる可能性があります。

__slots__新しいスタイルのクラス定義で定義することにより、デフォルトをオーバーライドできます。__slots__宣言は、各変数の値を保持するために、各インスタンスにインスタンス変数と埋蔵量だけで十分なスペースのシーケンスを取り。は__dict__インスタンスごとに作成されないため、スペースが節約されます。

それで、時間とメモリを節約できますか?

私のコンピューターでの3つのアプローチの比較:

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 2009

6
名前付きタプルdocs.python.org/library/collections.html#collections.namedtupleもあり、スロットを持つオブジェクトのクラスファクトリです。それは間違いなくよりきちんとしており、おそらくさらに最適化されています。
Jochen Ritzel、2009

名前付きタプルもテストし、結果を使用して回答を更新しました。
codeape、2009

1
私はあなたのコードを数回実行し、私の結果が異なることに驚いた-スロット= 3秒obj = 11秒dict = 12秒namedtuple = 16秒。Win7 64ビットでCPython 2.6.6を使用しています
ジョナサン

パンチラインを強調するために-namedtuple最高の結果ではなく最悪の結果を得ました
ジョナサン

15

オブジェクトの属性アクセスでは、舞台裏で辞書アクセスを使用します。そのため、属性アクセスを使用すると、オーバーヘッドが追加されます。さらに、オブジェクトの場合、たとえば追加のメモリ割り当てや(たとえば__init__メソッドの)コード実行のために、追加のオーバーヘッドが発生します。

コードで、oObjインスタンスの場合は、少量の追加オーバーヘッドo.attrと同等o.__dict__['attr']です。


これをテストしましたか? o.__dict__["attr"]余分なオーバーヘッドがあり、余分なバイトコードオペレーションをとります。obj.attrの方が高速です。(もちろん、属性アクセスはサブスクリプションアクセスより遅くなることはありません-これは非常に最適化された重要なコードパスです。)
Glenn Maynard

2
もちろん、あなたが実際にあれば O .__ dictの__ [「のattr」]それは遅くなります-私は、それがそのように正確に実装されたではないことを、と同等であったと言うことを意味しました。私の言い回しからははっきりしないと思います。私はまた、メモリ割り当て、コンストラクタコール時間等の他の要因述べ
ビナイSajipを

11年後の最近のバージョンのpython3でもこれは同じですか?
matanster


5

これはpython 3.6.1の@hughdbrown回答のコピーです。カウントを5倍にし、各実行の最後にpythonプロセスのメモリフットプリントをテストするコードを追加しました。

反対投票者が気付く前に、オブジェクトのサイズをカウントするこの方法は正確ではないことに注意してください。

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をインストールしていなかったので、その部分を削除しました)。とにかく、これは私にとって不可解であり、元の質問が完全に答えられていないことを意味します。他のすべての答えは「名前付きタプルは素晴らしい」と「スロットを使用する」のようなものであり、明らかに新しいdictオブジェクトは毎回それらよりも速いですか?辞書は本当に最適化されていると思いますか?
Multihunter 2017年

1
これは、makeL関数が文字列を返す結果のようです。空のリストを返すと、代わりに、結果はpython2のhughdbrownのものとほぼ一致します。名前付きタプルを除いて、常にSlotObjより遅い(:
Multihunter

小さな問題がある可能性があります。makeLは、「@ timeit」の各ラウンドで異なる速度で実行される可能性があります。これは、文字列がpythonにキャッシュされるためです-しかし、おそらく私は間違っています。
Barney、

@BarnabasSzabolcsは、値「これはサンプル文字列%sです」%を代入する必要があるため、毎回新しい文字列を作成する必要があります%i
Jarrod Chesney

はい、それはループ内では真実ですが、2番目のテストでは、再び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

質問はありません。
他の属性を持たない(メソッドも何もない)データがあります。したがって、データコンテナ(この場合は辞書)があります。

私は通常、データモデリングの観点から考えることを好みます。大きなパフォーマンスの問題がある場合、抽象化で何かをあきらめることができますが、それには非常に正当な理由があります。
プログラミングはすべて複雑さを管理することであり、正しい抽象を維持することは、多くの場合、そのような結果を達成するための最も有用な方法の1つです。

オブジェクトが遅い理由については、あなたの測定は正しくないと思います。
forループ内で実行する割り当てが少なすぎるため、dict(組み込みオブジェクト)と「カスタム」オブジェクトをインスタンス化するために必要な時間は異なります。言語の観点からは同じですが、実装がかなり異なります。
その後、メンバーは辞書内で維持されるため、割り当て時間はどちらもほぼ同じになります。


0

データ構造に参照サイクルが含まれていない場合に、メモリ使用量を削減する別の方法があります。

2つのクラスを比較してみましょう。

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__ベースのクラスに比べて1つの利点があります。追加の属性を追加できることです。

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