models: Memoize identical instances automatically

This combined with the previous changes has brought the memory use for a 14k
track test-set down from about 75MB to 17MB or so. Note that this does however,
mean that copy is now lying to us as it does not such thing whenever it can
avoid it.
This commit is contained in:
Thomas Adamcik 2015-04-08 01:14:56 +02:00
parent dd270ab87b
commit b7375323e9
3 changed files with 35 additions and 6 deletions

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals
import copy import copy
import json import json
import weakref
# TODO: split into base models, serialization and fields? # TODO: split into base models, serialization and fields?
@ -24,7 +25,7 @@ class Field(object):
""" """
def __init__(self, default=None, type=None, choices=None): def __init__(self, default=None, type=None, choices=None):
self._name = None # Set by FieldOwner self._name = None # Set by ImmutableObjectMeta
self._choices = choices self._choices = choices
self._default = default self._default = default
self._type = type self._type = type
@ -130,7 +131,7 @@ class Collection(Field):
return self._default.__class__(value) or None return self._default.__class__(value) or None
class FieldOwner(type): class ImmutableObjectMeta(type):
"""Helper to automatically assign field names to descriptors.""" """Helper to automatically assign field names to descriptors."""
@ -142,8 +143,20 @@ class FieldOwner(type):
value._name = key value._name = key
attrs['_fields'] = fields attrs['_fields'] = fields
attrs['_instances'] = weakref.WeakValueDictionary()
attrs['__slots__'] = ['_' + field for field in fields] attrs['__slots__'] = ['_' + field for field in fields]
return super(FieldOwner, cls).__new__(cls, name, bases, attrs)
for base in bases:
if '__weakref__' in getattr(base, '__slots__', []):
break
else:
attrs['__slots__'].append('__weakref__')
return super(ImmutableObjectMeta, cls).__new__(cls, name, bases, attrs)
def __call__(cls, *args, **kwargs): # noqa: N805
instance = super(ImmutableObjectMeta, cls).__call__(*args, **kwargs)
return cls._instances.setdefault(weakref.ref(instance), instance)
class ImmutableObject(object): class ImmutableObject(object):
@ -157,7 +170,7 @@ class ImmutableObject(object):
:type kwargs: any :type kwargs: any
""" """
__metaclass__ = FieldOwner __metaclass__ = ImmutableObjectMeta
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
for key, value in kwargs.items(): for key, value in kwargs.items():
@ -232,7 +245,7 @@ class ImmutableObject(object):
raise TypeError( raise TypeError(
'copy() got an unexpected keyword argument "%s"' % key) 'copy() got an unexpected keyword argument "%s"' % key)
super(ImmutableObject, other).__setattr__(key, value) super(ImmutableObject, other).__setattr__(key, value)
return other return self._instances.setdefault(weakref.ref(other), other)
def serialize(self): def serialize(self):
data = {} data = {}

View File

@ -9,7 +9,7 @@ def create_instance(field):
"""Create an instance of a dummy class for testing fields.""" """Create an instance of a dummy class for testing fields."""
class Dummy(object): class Dummy(object):
__metaclass__ = FieldOwner __metaclass__ = ImmutableObjectMeta
attr = field attr = field
return Dummy() return Dummy()

View File

@ -8,6 +8,22 @@ from mopidy.models import (
TlTrack, Track, model_json_decoder) TlTrack, Track, model_json_decoder)
class CachingTest(unittest.TestCase):
def test_same_instance(self):
self.assertIs(Track(), Track())
def test_same_instance_with_values(self):
self.assertIs(Track(uri='test'), Track(uri='test'))
def test_different_instance_with_different_values(self):
self.assertIsNot(Track(uri='test1'), Track(uri='test2'))
def test_different_instance_with_copy(self):
t = Track(uri='test1')
self.assertIsNot(t, t.copy(uri='test2'))
class GenericCopyTest(unittest.TestCase): class GenericCopyTest(unittest.TestCase):
def compare(self, orig, other): def compare(self, orig, other):