用普通类、数据类和命名元组创建对象表现不同的原因是什么?

8

我正在学习数据类和命名元组。我发现使用不同的Python特性创建对象时,它们的性能是不同的。

数据类:

In [1]: from dataclasses import dataclass
   ...:
   ...: @dataclass
   ...: class Position:
   ...:     lon: float = 0.0
   ...:     lat: float = 0.0
   ...:

In [2]: %timeit for _ in range(1000): Position(12.5, 345)
326 µs ± 34.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

普通类:

In [1]: class Position:
   ...:
   ...:     def __init__(self, lon=0.0, lat=0.0):
   ...:         self.lon = lon
   ...:         self.lat = lat
   ...:

In [2]: %timeit for _ in range(1000): Position(12.5, 345)
248 µs ± 2.89 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

命名元组:

In [2]: Position = namedtuple("Position", ["lon","lat"], defaults=[0.0,0.0])

In [3]: %timeit for _ in range(1000): Position(12.5, 345)
286 µs ± 13.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
  • Python 版本:3.7.3
  • 操作系统:MacOS Mojave

所有实现具有相同的对象属性和默认值。

  1. 为什么这个趋势是时间(dataclass)> 时间(namedtuple)> 时间(普通类)?
  2. 每种实现都采取了什么措施来占用各自的时间?
  3. 哪种实现在什么场景下具有最佳性能?

这里,时间表示创建对象所需的时间。

1个回答

3

在Python中,一切都是字典。对于数据类来说,在该字典中会有更多的条目,因此将它们放入其中需要更多的时间。

这个变化是如何发生的? @Arne的评论指出我在这里缺少了一些东西。我做了一个样例代码:

from dataclasses import dataclass
import time

@dataclass
class Position:
    lon: float = 0.0
    lat: float = 0.0


start_time = time.time()
for i in range(100000):
    p = Position(lon=1.0, lat=1.0)
elapsed = time.time() - start_time
print(f"dataclass {elapsed}")
print(dir(p))


class Position2:
    lon: float = 0.0
    lat: float = 0.0

    def __init__(self, lon, lat):
        self.lon = lon
        self.lat = lat


start_time = time.time()
for i in range(100000):
    p = Position2(lon=1.0, lat=1.0)
elapsed = time.time() - start_time
print(f"just class {elapsed}")
print(dir(p))

start_time = time.time()
for i in range(100000):
    p = {"lon": 1.0, "lat": 1.0}
elapsed = time.time() - start_time
print(f"dict {elapsed}")

包含结果:

/usr/bin/python3.8 ...../test.py
dataclass 0.16358232498168945
['__annotations__', '__class__', '__dataclass_fields__', '__dataclass_params__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'lat', 'lon']
just class 0.1495649814605713
['__annotations__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'lat', 'lon']
dict 0.028212785720825195

Process finished with exit code 0

Dict 的例子供参考。

查看了 dataclass,这个函数:

(489) def _init_fn(fields, frozen, has_post_init, self_name, globals):

负责创建构造函数。正如Arne指出的,post_init代码是可选的,并且不会生成。我有另一个想法,那就是在字段周围进行一些工作,但:

In [5]: p = Position(lat = 1.1, lon=2.2)                                                                                                                                                                           

In [7]: p.lat.__class__                                                                                                                                                                                            
Out[7]: float

因此,这里没有额外的封装或代码。从所有这些中,我看到的唯一额外的东西就是更多的方法。


谢谢。我会仔细阅读的。我已经编辑了问题。 - bigbounty
@bigbounty 如果我们谈论性能,我也会添加一个简单的字典。 - Michał Zaborowski
1
如果您没有自己编写__post_init__,则不会生成对它的调用。 - Arne
@Arne,你是对的 - 我已经更新了回复。希望现在没问题 :) - Michał Zaborowski
重新运行带有"slots=True"的数据类进行测试会很有趣。 - wakey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接