繁体   English   中英

Python 中的(命名)元组字典和速度/RAM 性能

[英]Dictionary of (named) tuples in Python and speed/RAM performance

我正在创建一个包含一百万个元组项的字典d ,理想情况下我想通过以下方式访问它们:

d[1634].id       # or  d[1634]['id']
d[1634].name     # or  d[1634]['name']
d[1634].isvalid  # or  d[1634]['isvalid']

而不是d[1634][0]d[1634][1]d[1634][2]不太明确。

根据我的测试:

import os, psutil, time, collections, typing
Tri = collections.namedtuple('Tri', 'id,name,isvalid')
Tri2 = typing.NamedTuple("Tri2", [('id', int), ('name', str), ('isvalid', bool)])
t0 = time.time()
# uncomment only one of these 4 next lines:
d = {i: (i+1, 'hello', True) for i in range(1000000)}                                 # tuple
# d = {i: {'id': i+1, 'name': 'hello', 'isvalid': True} for i in range(1000000)}      # dict
# d = {i: Tri(id=i+1, name='hello', isvalid=True) for i in range(1000000)}            # namedtuple
# d = {i: Tri2(id=i+1, name='hello', isvalid=True) for i in range(1000000)}            # NamedTuple
print('%.3f s  %.1f MB' % (time.time()-t0, psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))

"""
tuple:       0.257 s  193.3 MB
dict:        0.329 s  363.6 MB
namedtuple:  1.253 s  193.3 MB  (collections)
NamedTuple:  1.250 s  193.5 MB  (typing)
"""
  • tuple相比,使用dict会使 RAM 使用量翻倍
  • tuple相比,使用namedtupleNamedTuple花费的时间乘以 5!

问题:Python 3 中是否有类似元组的数据结构,它允许使用x.idx.name等访问数据,并且 RAM 和 CPU 效率高吗?


笔记:

  • 在我的实际用例中, tuple类似于(uint64, uint64, bool)类型的 C 结构。

  • 我也尝试过:

    • slots (为了避免内部对象的__dict__ ,请参阅__slots__ 的用法?

    • dataclass

       @dataclasses.dataclass class Tri3: id: int...
    • ctypes.Structure .结构:

       class Tri7(ctypes.Structure): _fields_ = [("id", ctypes.c_int), ...]

    但它并没有更好(所有这些都约为 1.2 秒),就性能而言,没有什么比真正的tuple更接近

  • 以下是其他选项: Python 中的类 C 结构

Cython 的cdef 类可能是您想要的:与纯 Python 类相比,它们使用的 memory 更少,即使在访问成员时会付出更多开销(因为字段存储为 C 值而不是 Python 对象)。

例如:

%%cython
cdef class CTuple:
    cdef public unsigned long long int id
    cdef public str name
    cdef public bint isvalid
    
    def __init__(self, id, name, isvalid):
        self.id = id
        self.name = name
        self.isvalid = isvalid

可以根据需要使用:

ob=CTuple(1,"mmm",3)
ob.id, ob.name, ob.isvalid # prints (2, "mmm", 3)

计时/内存消耗:

首先,我机器上的基线:

0.258 s  252.4 MB  # tuples
0.343 s  417.5 MB  # dict
1.181 s  264.0 MB  # namedtuple collections

使用CTuple我们得到:

0.306 s  191.0 MB

这几乎一样快,并且需要相当少的 memory。

如果 C 类型的成员在编译时不清楚,可以使用简单的 python 对象:

%%cython
cdef class PTuple:
    cdef public object id
    cdef public object name
    cdef public object isvalid
    
    def __init__(self, id, name, isvalid):
        self.id = id
        self.name = name
        self.isvalid = isvalid

时间安排有点令人惊讶:

0.648 s  249.8 MB

我没想到它会比CTuple慢这么多,但至少它是命名元组的两倍。


这种方法的一个缺点是它需要编译。 然而,Cython 提供了cython.inline ,可用于编译即时创建的 Cython 代码。

我已经发布了可以通过cynamedtuple pip install cynamedtuple的 cynamedtuple ,它基于下面的原型:

import cython

# for generation of cython code:
tab = "    "
def create_members_definition(name_to_ctype):
    members = []
    for my_name, my_ctype in name_to_ctype.items():
        members.append(tab+"cdef public "+my_ctype+" "+my_name)
    return members

def create_signature(names):
    return tab + "def __init__(self,"+", ".join(names)+"):"

def create_initialization(names):
    inits = [tab+tab+"self."+x+" = "+x for x in names]
    return inits

def create_cdef_class_code(classname, names):
    code_lines = ["cdef class " + classname + ":"]
    code_lines.extend(create_members_definition(names))
    code_lines.append(create_signature(names.keys()))
    code_lines.extend(create_initialization(names.keys()))
    return "\n".join(code_lines)+"\n"

# utilize cython.inline to generate and load pyx-module:
def create_cnamedtuple_class(classname, names):
    code = create_cdef_class_code(classname, names)
    code = code + "GenericClass = " + classname +"\n"
    ret = cython.inline(code)
    return ret["GenericClass"]

可以如下使用,从上面动态定义CTuple

CTuple = create_cnamedtuple_class("CTuple", 
                                 {"id":"unsigned long long int", 
                                  "name":"str",
                                  "isvalid":"bint"})

ob = CTuple(1,"mmm",3)
... 

另一种选择是使用 jit 编译和 Numba 的 jit -classes ,它们提供了这种可能性。 然而,它们似乎要慢得多:

from numba import jitclass, types

spec = [
    ('id', types.uint64), 
    ('name', types.string),
    ('isvalid',  types.uint8),
]

@jitclass(spec)
class NBTuple(object):
    def __init__(self, id, name, isvalid):
        self.id = id
        self.name = name
        self.isvalid = isvalid

结果是:

20.622 s  394.0 MB

所以 numba jitted 类不是(还?)一个好的选择。

您可以尝试反转它(存储为数组结构)并将值访问为x['id'][1634] 换句话说, x是一个包含三个键的字典,每个键的值是一个列表。 这将节省空间。

或者您可以使用 pandas 数据帧。 数据框以矩阵形式存储,其中行具有数字 ID,列具有标签(诸如“名称”等字符串)。 对于 dataframe dfdf.iloc[i]指向 $i^th$ 行,您可以通过df.iloc[i].namedf.iloc[i]['name']访问该行中的名称

记录类库的帮助下,还有另一种快速而紧凑的方法:

pip3 install recordclass

import recordclass
TriDO = recordclass.make_dataclass("TriDO", 
           [('id', int), ('name', str), ('isvalid', bool)],
           fast_new=True)

以下是性能计数器的值(linux、64 位、python3.9、recordclass >= 0.15):

tuple:

t0 = time.time()
d = {i: (i+1, 'hello', True) for i in range(1000000)}
print('%.3f s  %.1f MB' % (time.time()-t0,
      psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
0.194 s  215.8 MB

dict:

t0 = time.time()
d = {i: {'id':i+1, 'name':'hello', 'isvalid':True} for i in range(1000000)}
print('%.3f s  %.1f MB' % (time.time()-t0,
      psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
0.379 s  426.6 MB

namedtuple:

t0 = time.time()
d = {i: Tri(i+1, 'hello', True) for i in range(1000000)}
print('%.3f s  %.1f MB' % (time.time()-t0,
      psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
1.151 s  272.8 MB

'命名元组:'

t0 = time.time()
d = {i: Tri2(i+1, 'hello', True) for i in range(1000000)}
print('%.3f s  %.1f MB' % (time.time()-t0,
      psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
0.989 s  273.2 MB

dataobject:

t0 = time.time()
d = {i: TriDO(i+1, 'hello', True) for i in range(1000000)}
print('%.3f s  %.1f MB' % (time.time()-t0,
      psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2))
0.239 s  241.8 MB

这里更准确的时间:

%timeit d = {i:(i+1, 'hello', True) for i in range(1000000)} # tuple
162 ms ± 756 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit d = {i:{'id':i+1, 'name':'hello', 'isvalid':True} for i in range(1000000)} # dict
250 ms ± 2.77 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit d = {i:Tri(i+1,'hello',True) for i in range(1000000)} # namedtuple
318 ms ± 422 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit d = {i:Tri2(i+1,'hello',True) for i in range(1000000)} # NamedTuple
330 ms ± 5.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit d = {i:TriDO(i+1,'hello',True) for i in range(1000000)} # dataobject
196 ms ± 1.65 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

您可以保留元组并命名您的索引(下面的第二个选项)

from enum import IntEnum

# int variable
ID, NAME, IS_VALID = 0, 1, 2

# IntEnum
class Index(IntEnum):
    ID = 0
    NAME = 1
    IS_VALID = 2
   

# Create tuples
d = {i: (i+1, 'hello', True) for i in range(int(1e6))}  

t0 = time.time()

# check data access performance
# uncomment only one of these 3 next lines:
# for i in range(len(d)): _ = d[i][0], d[i][1], d[i][2]
# for i in range(len(d)): _ = d[i][ID], d[i][NAME], d[i][IS_VALID]
for i in range(len(d)): _ = d[i][Index.ID], d[i][Index.NAME], d[i][Index.IS_VALID]

print('%.3f s' % (time.time()-t0))

"""
int           0.307 s
int variable  0.312 s
IntEnum       0.749 s
"""

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM