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:
parent
dd270ab87b
commit
b7375323e9
@ -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 = {}
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user