[IMP] fields: add `__slots__` on all fields

Introduce slots on all field classes for common attributes; slots take much
less memory than a `__dict__`.  The other attributes are stored in a dictionary
`_attrs`; all fields with an empty value for `_attrs` (common case) share the
same empty dictionary.  This saves quite some memory (around 4.5Mb per
registry), given the number of field instances created for a registry.

Another mechanism is used for the default values of attributes, since slots
cannot be assigned on classes.
This commit is contained in:
Raphael Collet 2015-03-11 12:37:10 +01:00
parent 915af86a91
commit e7928e1265
1 changed files with 207 additions and 80 deletions

View File

@ -79,11 +79,14 @@ class MetaField(type):
MetaField.by_type[cls.type] = cls MetaField.by_type[cls.type] = cls
# compute class attributes to avoid calling dir() on fields # compute class attributes to avoid calling dir() on fields
cls.default_attrs = {}
cls.column_attrs = [] cls.column_attrs = []
cls.related_attrs = [] cls.related_attrs = []
cls.description_attrs = [] cls.description_attrs = []
for attr in dir(cls): for attr in dir(cls):
if attr.startswith('_column_'): if attr.startswith('_default_'):
cls.default_attrs[attr[9:]] = getattr(cls, attr)
elif attr.startswith('_column_'):
cls.column_attrs.append((attr[8:], attr)) cls.column_attrs.append((attr[8:], attr))
elif attr.startswith('_related_'): elif attr.startswith('_related_'):
cls.related_attrs.append((attr[9:], attr)) cls.related_attrs.append((attr[9:], attr))
@ -254,69 +257,132 @@ class Field(object):
""" """
__metaclass__ = MetaField __metaclass__ = MetaField
_attrs = None # dictionary of field attributes; it contains: type = None # type of the field (string)
relational = False # whether the field is a relational one
__slots__ = [
'_attrs', # dictionary of field attributes; it contains:
# - all attributes after __init__() # - all attributes after __init__()
# - free attributes only after set_class_name() # - free attributes only after set_class_name()
automatic = False # whether the field is automatically created ("magic" field) 'automatic', # whether the field is automatically created ("magic" field)
inherited = False # whether the field is inherited (_inherits) 'inherited', # whether the field is inherited (_inherits)
column = None # the column corresponding to the field 'column', # the column corresponding to the field
setup_done = False # whether the field has been set up 'setup_done', # whether the field has been set up
name = None # name of the field 'name', # name of the field
type = None # type of the field (string) 'model_name', # name of the model of this field
relational = False # whether the field is a relational one 'comodel_name', # name of the model of values (if relational)
model_name = None # name of the model of this field
comodel_name = None # name of the model of values (if relational)
store = True # whether the field is stored in database 'store', # whether the field is stored in database
index = False # whether the field is indexed in database 'index', # whether the field is indexed in database
manual = False # whether the field is a custom field 'manual', # whether the field is a custom field
copy = True # whether the field is copied over by BaseModel.copy() 'copy', # whether the field is copied over by BaseModel.copy()
depends = () # collection of field dependencies 'depends', # collection of field dependencies
recursive = False # whether self depends on itself 'recursive', # whether self depends on itself
compute = None # compute(recs) computes field on recs 'compute', # compute(recs) computes field on recs
compute_sudo = False # whether field should be recomputed as admin 'compute_sudo', # whether field should be recomputed as admin
inverse = None # inverse(recs) inverses field on recs 'inverse', # inverse(recs) inverses field on recs
search = None # search(recs, operator, value) searches on self 'search', # search(recs, operator, value) searches on self
related = None # sequence of field names, for related fields 'related', # sequence of field names, for related fields
related_sudo = True # whether related fields should be read as admin 'related_sudo', # whether related fields should be read as admin
company_dependent = False # whether `self` is company-dependent (property field) 'company_dependent', # whether `self` is company-dependent (property field)
default = None # default(recs) returns the default value 'default', # default(recs) returns the default value
string = None # field label 'string', # field label
help = None # field tooltip 'help', # field tooltip
readonly = False 'readonly', # whether the field is readonly
required = False 'required', # whether the field is required
states = None 'states', # set readonly and required depending on state
groups = False # csv list of group xml ids 'groups', # csv list of group xml ids
change_default = None # whether the field may trigger a "user-onchange" 'change_default', # whether the field may trigger a "user-onchange"
deprecated = None # whether the field is ... deprecated 'deprecated', # whether the field is deprecated
inverse_fields = () # collection of inverse fields (objects) 'inverse_fields', # collection of inverse fields (objects)
computed_fields = () # fields computed with the same method as self 'computed_fields', # fields computed with the same method as self
_triggers = () # invalidation and recomputation triggers 'related_field', # corresponding related field
'_triggers', # invalidation and recomputation triggers
]
# default values for slots
_default_automatic = False
_default_inherited = False
_default_column = None
_default_setup_done = False
_default_name = None
_default_model_name = None
_default_comodel_name = None
_default_store = True
_default_index = False
_default_manual = False
_default_copy = True
_default_depends = ()
_default_recursive = False
_default_compute = None
_default_compute_sudo = False
_default_inverse = None
_default_search = None
_default_related = None
_default_related_sudo = True
_default_company_dependent = False
_default_default = None
_default_string = None
_default_help = None
_default_readonly = False
_default_required = False
_default_states = None
_default_groups = False
_default_change_default = None
_default_deprecated = None
_default_inverse_fields = ()
_default_computed_fields = ()
_default_related_field = None
_default__triggers = ()
def __init__(self, string=None, **kwargs): def __init__(self, string=None, **kwargs):
kwargs['string'] = string kwargs['string'] = string
attrs = {key: val for key, val in kwargs.iteritems() if val is not None} attrs = {key: val for key, val in kwargs.iteritems() if val is not None}
self._attrs = attrs or EMPTY_DICT self._attrs = attrs or EMPTY_DICT
def _set_attr(self, name, value): def __getattr__(self, name):
""" Set the given field attribute, and add it to `_attrs` if necessary. """ """ Access non-slot field attribute. """
object.__setattr__(self, name, value) if name == '_attrs':
if not hasattr(type(self), name): raise AttributeError(name)
try:
return self._attrs[name]
except KeyError:
raise AttributeError(name)
def __setattr__(self, name, value):
""" Set slot or non-slot field attribute. """
try:
object.__setattr__(self, name, value)
except AttributeError:
if self._attrs: if self._attrs:
self._attrs[name] = value self._attrs[name] = value
else: else:
self._attrs = {name: value} # replace EMPTY_DICT self._attrs = {name: value} # replace EMPTY_DICT
def __delattr__(self, name):
""" Remove non-slot field attribute. """
try:
del self._attrs[name]
except KeyError:
raise AttributeError(name)
def new(self, **kwargs): def new(self, **kwargs):
""" Return a field of the same type as `self`, with its own parameters. """ """ Return a field of the same type as `self`, with its own parameters. """
return type(self)(**kwargs) return type(self)(**kwargs)
def set_class_name(self, cls, name): def set_class_name(self, cls, name):
""" Assign the model class and field name of `self`. """ """ Assign the model class and field name of `self`. """
for attr, value in self.default_attrs.iteritems():
setattr(self, attr, value)
self.model_name = cls._name self.model_name = cls._name
self.name = name self.name = name
@ -346,7 +412,7 @@ class Field(object):
self._attrs = EMPTY_DICT self._attrs = EMPTY_DICT
for attr, value in attrs.iteritems(): for attr, value in attrs.iteritems():
self._set_attr(attr, value) setattr(self, attr, value)
if not self.string and not self.related: if not self.string and not self.related:
# related fields get their string from their parent field # related fields get their string from their parent field
@ -486,7 +552,7 @@ class Field(object):
for attr, value in field._attrs.iteritems(): for attr, value in field._attrs.iteritems():
if attr not in self._attrs: if attr not in self._attrs:
self._set_attr(attr, value) setattr(self, attr, value)
# special case for states: copy it only for inherited fields # special case for states: copy it only for inherited fields
if not self.states and self.inherited: if not self.states and self.inherited:
@ -963,6 +1029,8 @@ class Field(object):
class Boolean(Field): class Boolean(Field):
type = 'boolean' type = 'boolean'
__slots__ = []
def convert_to_cache(self, value, record, validate=True): def convert_to_cache(self, value, record, validate=True):
return bool(value) return bool(value)
@ -974,10 +1042,13 @@ class Boolean(Field):
class Integer(Field): class Integer(Field):
type = 'integer' type = 'integer'
group_operator = None # operator for aggregating values
__slots__ = [
'group_operator', # operator for aggregating values
]
_default_group_operator = None
_related_group_operator = property(attrgetter('group_operator')) _related_group_operator = property(attrgetter('group_operator'))
_column_group_operator = property(attrgetter('group_operator')) _column_group_operator = property(attrgetter('group_operator'))
def convert_to_cache(self, value, record, validate=True): def convert_to_cache(self, value, record, validate=True):
@ -1005,8 +1076,14 @@ class Float(Field):
cursor and returning a pair (total, decimal) cursor and returning a pair (total, decimal)
""" """
type = 'float' type = 'float'
_digits = None # digits argument passed to class initializer
group_operator = None # operator for aggregating values __slots__ = [
'_digits', # digits argument passed to class initializer
'group_operator', # operator for aggregating values
]
_default__digits = None
_default_group_operator = None
def __init__(self, string=None, digits=None, **kwargs): def __init__(self, string=None, digits=None, **kwargs):
super(Float, self).__init__(string=string, _digits=digits, **kwargs) super(Float, self).__init__(string=string, _digits=digits, **kwargs)
@ -1045,8 +1122,11 @@ class Float(Field):
class _String(Field): class _String(Field):
""" Abstract class for string fields. """ """ Abstract class for string fields. """
translate = False __slots__ = [
'translate', # whether the field is translated
]
_default_translate = False
_column_translate = property(attrgetter('translate')) _column_translate = property(attrgetter('translate'))
_related_translate = property(attrgetter('translate')) _related_translate = property(attrgetter('translate'))
_description_translate = property(attrgetter('translate')) _description_translate = property(attrgetter('translate'))
@ -1060,17 +1140,21 @@ class Char(_String):
:param bool translate: whether the values of this field can be translated :param bool translate: whether the values of this field can be translated
""" """
type = 'char' type = 'char'
size = None
__slots__ = [
'size', # maximum size of values (deprecated)
]
_default_size = None
_column_size = property(attrgetter('size'))
_related_size = property(attrgetter('size'))
_description_size = property(attrgetter('size'))
def _setup_regular(self, env): def _setup_regular(self, env):
super(Char, self)._setup_regular(env) super(Char, self)._setup_regular(env)
assert isinstance(self.size, (NoneType, int)), \ assert isinstance(self.size, (NoneType, int)), \
"Char field %s with non-integer size %r" % (self, self.size) "Char field %s with non-integer size %r" % (self, self.size)
_column_size = property(attrgetter('size'))
_related_size = property(attrgetter('size'))
_description_size = property(attrgetter('size'))
def convert_to_cache(self, value, record, validate=True): def convert_to_cache(self, value, record, validate=True):
if value is None or value is False: if value is None or value is False:
return False return False
@ -1083,6 +1167,7 @@ class Text(_String):
:param translate: whether the value of this field can be translated :param translate: whether the value of this field can be translated
""" """
type = 'text' type = 'text'
__slots__ = []
def convert_to_cache(self, value, record, validate=True): def convert_to_cache(self, value, record, validate=True):
if value is None or value is False: if value is None or value is False:
@ -1091,13 +1176,18 @@ class Text(_String):
class Html(_String): class Html(_String):
type = 'html' type = 'html'
sanitize = True # whether value must be sanitized
strip_style = False # whether to strip style attributes
__slots__ = [
'sanitize', # whether value must be sanitized
'strip_style', # whether to strip style attributes
]
_default_sanitize = True
_column_sanitize = property(attrgetter('sanitize')) _column_sanitize = property(attrgetter('sanitize'))
_related_sanitize = property(attrgetter('sanitize')) _related_sanitize = property(attrgetter('sanitize'))
_description_sanitize = property(attrgetter('sanitize')) _description_sanitize = property(attrgetter('sanitize'))
_default_strip_style = False
_column_strip_style = property(attrgetter('strip_style')) _column_strip_style = property(attrgetter('strip_style'))
_related_strip_style = property(attrgetter('strip_style')) _related_strip_style = property(attrgetter('strip_style'))
_description_strip_style = property(attrgetter('strip_style')) _description_strip_style = property(attrgetter('strip_style'))
@ -1112,6 +1202,7 @@ class Html(_String):
class Date(Field): class Date(Field):
type = 'date' type = 'date'
__slots__ = []
@staticmethod @staticmethod
def today(*args): def today(*args):
@ -1172,6 +1263,7 @@ class Date(Field):
class Datetime(Field): class Datetime(Field):
type = 'datetime' type = 'datetime'
__slots__ = []
@staticmethod @staticmethod
def now(*args): def now(*args):
@ -1246,6 +1338,7 @@ class Datetime(Field):
class Binary(Field): class Binary(Field):
type = 'binary' type = 'binary'
__slots__ = []
class Selection(Field): class Selection(Field):
@ -1261,8 +1354,12 @@ class Selection(Field):
<field-incremental-definition>`. <field-incremental-definition>`.
""" """
type = 'selection' type = 'selection'
selection = None # [(value, string), ...], function or method name
selection_add = None # [(value, string), ...] __slots__ = [
'selection', # [(value, string), ...], function or method name
]
_default_selection = None
def __init__(self, selection=None, string=None, **kwargs): def __init__(self, selection=None, string=None, **kwargs):
if callable(selection): if callable(selection):
@ -1353,20 +1450,20 @@ class Selection(Field):
class Reference(Selection): class Reference(Selection):
type = 'reference' type = 'reference'
size = None
def __init__(self, selection=None, string=None, **kwargs): __slots__ = [
super(Reference, self).__init__(selection=selection, string=string, **kwargs) 'size', # maximum size of values (deprecated)
]
_default_size = None
_related_size = property(attrgetter('size'))
_column_size = property(attrgetter('size'))
def _setup_regular(self, env): def _setup_regular(self, env):
super(Reference, self)._setup_regular(env) super(Reference, self)._setup_regular(env)
assert isinstance(self.size, (NoneType, int)), \ assert isinstance(self.size, (NoneType, int)), \
"Reference field %s with non-integer size %r" % (self, self.size) "Reference field %s with non-integer size %r" % (self, self.size)
_related_size = property(attrgetter('size'))
_column_size = property(attrgetter('size'))
def convert_to_cache(self, value, record, validate=True): def convert_to_cache(self, value, record, validate=True):
if isinstance(value, BaseModel): if isinstance(value, BaseModel):
if ((not validate or value._name in self.get_values(record.env)) if ((not validate or value._name in self.get_values(record.env))
@ -1392,8 +1489,14 @@ class Reference(Selection):
class _Relational(Field): class _Relational(Field):
""" Abstract class for relational fields. """ """ Abstract class for relational fields. """
relational = True relational = True
domain = None # domain for searching values
context = None # context for searching values __slots__ = [
'domain', # domain for searching values
'context', # context for searching values
]
_default_domain = []
_default_context = {}
def _setup_regular(self, env): def _setup_regular(self, env):
super(_Relational, self)._setup_regular(env) super(_Relational, self)._setup_regular(env)
@ -1461,9 +1564,16 @@ class Many2one(_Relational):
fields or field extensions. fields or field extensions.
""" """
type = 'many2one' type = 'many2one'
ondelete = 'set null' # what to do when value is deleted
auto_join = False # whether joins are generated upon search __slots__ = [
delegate = False # whether self implements delegation 'ondelete', # what to do when value is deleted
'auto_join', # whether joins are generated upon search
'delegate', # whether self implements delegation
]
_default_ondelete = 'set null'
_default_auto_join = False
_default_delegate = False
def __init__(self, comodel_name=None, string=None, **kwargs): def __init__(self, comodel_name=None, string=None, **kwargs):
super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs) super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs)
@ -1543,6 +1653,7 @@ class UnionUpdate(SpecialValue):
class _RelationalMulti(_Relational): class _RelationalMulti(_Relational):
""" Abstract class for relational fields *2many. """ """ Abstract class for relational fields *2many. """
__slots__ = []
def _update(self, records, value): def _update(self, records, value):
""" Update the cached value of `self` for `records` with `value`. """ """ Update the cached value of `self` for `records` with `value`. """
@ -1666,10 +1777,17 @@ class One2many(_RelationalMulti):
the case of related fields or field extensions. the case of related fields or field extensions.
""" """
type = 'one2many' type = 'one2many'
inverse_name = None # name of the inverse field
auto_join = False # whether joins are generated upon search __slots__ = [
limit = None # optional limit to use upon read 'inverse_name', # name of the inverse field
copy = False # o2m are not copied by default 'auto_join', # whether joins are generated upon search
'limit', # optional limit to use upon read
]
_default_inverse_name = None
_default_auto_join = False
_default_limit = None
_default_copy = False # o2m are not copied by default
def __init__(self, comodel_name=None, inverse_name=None, string=None, **kwargs): def __init__(self, comodel_name=None, inverse_name=None, string=None, **kwargs):
super(One2many, self).__init__( super(One2many, self).__init__(
@ -1731,10 +1849,18 @@ class Many2many(_RelationalMulti):
""" """
type = 'many2many' type = 'many2many'
relation = None # name of table
column1 = None # column of table referring to model __slots__ = [
column2 = None # column of table referring to comodel 'relation', # name of table
limit = None # optional limit to use upon read 'column1', # column of table referring to model
'column2', # column of table referring to comodel
'limit', # optional limit to use upon read
]
_default_relation = None
_default_column1 = None
_default_column2 = None
_default_limit = None
def __init__(self, comodel_name=None, relation=None, column1=None, column2=None, def __init__(self, comodel_name=None, relation=None, column1=None, column2=None,
string=None, **kwargs): string=None, **kwargs):
@ -1777,6 +1903,7 @@ class Many2many(_RelationalMulti):
class Serialized(Field): class Serialized(Field):
""" Minimal support for existing sparse and serialized fields. """ """ Minimal support for existing sparse and serialized fields. """
type = 'serialized' type = 'serialized'
__slots__ = []
def convert_to_cache(self, value, record, validate=True): def convert_to_cache(self, value, record, validate=True):
return value or {} return value or {}
@ -1785,11 +1912,11 @@ class Serialized(Field):
class Id(Field): class Id(Field):
""" Special case for field 'id'. """ """ Special case for field 'id'. """
type = 'integer' type = 'integer'
__slots__ = []
string = 'ID' _default_string = 'ID'
store = True _default_store = True
#: Can't write this! _default_readonly = True
readonly = True
def to_column(self): def to_column(self):
self.column = fields.integer(self.string) self.column = fields.integer(self.string)