Понимание размера класса, namedtuple и __slots__ в Python 3.7 - PullRequest
1 голос
/ 18 марта 2019

После просмотра выступления Нины Захкаренко по управлению памятью Python на Pycon2016 ( link ) показалось, что метод dunder __slots__ был инструментом для уменьшения размера объекта и ускорения поиска атрибутов.

Я ожидал, что нормальный класс будет самым большим, тогда как подход __slots__ / namedtuple сэкономит место. Однако быстрый эксперимент доказал, что я не прав:

from collections import namedtuple
from sys import getsizeof

class Rectangle:
   '''A class based Rectangle, with a full __dict__'''
   def __init__(self, x, y, width, height):
      self.x = x
      self.y = y
      self.width = width
      self.height = height

class SlotsRectangle:
   '''A class based Rectangle with __slots__ defined for attributes'''
   __slots__ = ('x', 'y', 'width', 'height')

   def __init__(self, x, y, width, height):
      self.x = x
      self.y = y
      self.width = width
      self.height = height

NamedTupleRectangle = namedtuple('Rectangle', ('x', 'y', 'width', 'height'))
NamedTupleRectangle.__doc__ = 'A rectangle as an immutable namedtuple'

print(f'Class: {getsizeof(Rectangle(1,2,3,4))}')
print(f'Slots: {getsizeof(SlotsRectangle(1,2,3,4))}')
print(f'Named Tuple: {getsizeof(NamedTupleRectangle(1,2,3,4))}')

Терминальный выход:

$ python3.7 example.py
Class: 56
Slots: 72
Named Tuple: 80

Что здесь происходит? Из документации * Python Модель данных видно, что для __slots__ используются дескрипторы, которые добавили бы служебную нагрузку к классам, реализующим ее. Однако почему результаты так сильно отклоняются от нормального класса?

Направляя мой внутренний Рэймонд Х .: должен быть более трудный путь!

Ответы [ 3 ]

1 голос
/ 18 марта 2019

Функция sys.getsizeof(), вероятно, не делает то, что вы думаете; он не работает для сложных объектов, таких как пользовательские классы.

Посмотрите на этот ответ , чтобы узнать метод расчета объема памяти объектов; возможно это поможет тебе Я скопировал код этого ответа здесь, но полное объяснение приведено в ответе, который я связал.

import sys
from numbers import Number
from collections import Set, Mapping, deque

try: # Python 2
    zero_depth_bases = (basestring, Number, xrange, bytearray)
    iteritems = 'iteritems'
except NameError: # Python 3
    zero_depth_bases = (str, bytes, Number, range, bytearray)
    iteritems = 'items'

def getsize(obj_0):
    """Recursively iterate to sum size of object & members."""
    _seen_ids = set()
    def inner(obj):
        obj_id = id(obj)
        if obj_id in _seen_ids:
            return 0
        _seen_ids.add(obj_id)
        size = sys.getsizeof(obj)
        if isinstance(obj, zero_depth_bases):
            pass # bypass remaining control flow and return
        elif isinstance(obj, (tuple, list, Set, deque)):
            size += sum(inner(i) for i in obj)
        elif isinstance(obj, Mapping) or hasattr(obj, iteritems):
            size += sum(inner(k) + inner(v) for k, v in getattr(obj, iteritems)())
        # Check for custom object instances - may subclass above too
        if hasattr(obj, '__dict__'):
            size += inner(vars(obj))
        if hasattr(obj, '__slots__'): # can have __slots__ with __dict__
            size += sum(inner(getattr(obj, s)) for s in obj.__slots__ if hasattr(obj, s))
        return size
    return inner(obj_0)
0 голосов
/ 13 мая 2019

Существует более компактный вариант с recordclass библиотека:

from recordclass import dataobject

class Rectangle(dataobject):
   x:int
   y:int
   width:int
   height:int

>>> r = Rectangle(1,2,3,4)
>>> print(sys.getsizeof(r))
48

У него меньше занимаемой памяти, чем у __slots__, потому что он не участвует в циклическом сборе мусора (флаг Py_TPFLAGS_HAVE_GC не устанавливается, поэтому PyGC_Head (24 байта) вообще не нужно).

0 голосов
/ 18 марта 2019

«Направляю мой внутренний Рэймонд Х» + 1

Итак, дело в слотах, вы должны прочитать о слотах .

Другое дело,они влияют на class размер:

print(f'(Class) Class: {getsizeof(Rectangle)}') # 1056
print(f'(Class) Slots: {getsizeof(SlotsRectangle)}') # 888

Круто.Теперь давайте предположим, что мы добавляем поле в класс Rectangle:

rect = Rectangle(1,2,3,4)
rect.extra_field = dict() # wild right?
print(f'(Object) Class: {getsizeof(rect)}') # still 56

Таким образом, вы можете «посчитать» ресурсы, «которые вы используете» (в виде переменных экземпляра), и прямоугольник слотов будет 112и прямоугольник без слотов тоже будет 112 ...

Однако мы знаем, что это не тот случай, так как мы ожидаем, что правильный прямоугольник будет по крайней мере 352, потому что мы добавили dict к нему.

Слоты не позволяют вам сделать это и, таким образом, предоставляют способ ограничения использования ресурсов.

Проверьте этот ответ здесь , похоже,это может работать довольно хорошо для вашего случая использования.Запуск его на прямоугольнике слотов и обычном прямоугольнике дает 152 и 352 соответственно.

Кроме того, если вы действительно пытаетесь оптимизировать свой код и минимизировать использование ресурсов, переходите к ржавчине / c/ c ++ сторона дома.

...