Fast, small-footprint Python data object template
# -*- coding: utf-8 -*- | |
# Copyright (c) 2010, 2015 Matthew Zipay <mattz@ninthtest.net>. | |
# All rights reserved. | |
# Licensed under the MIT License <http://opensource.org/licenses/MIT>. | |
class PseudoStruct: | |
"""Base class for structure-like data objects. | |
Define the ``__slots__`` class member in a subclass of | |
``PseudoStruct`` to create a fast, small-footprint data object. | |
(Alternatively, use the :meth:`define` class method.) | |
PseudoStruct objects have get/set speed comparable to attribute | |
access on a simple class object, but with minimal memory footprint | |
(marginally larger than a list or tuple containing the field values, | |
but much smaller than a simple class object having the same fields | |
as attributes). | |
PseudoStruct was inspired by the concept of the same name described | |
in `The class File Format | |
<http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html>`_. | |
""" | |
# subclasses must define the fields they support | |
__slots__ = [] | |
@classmethod | |
def define(cls, class_name, field_names): | |
r"""Dynamically create a PseudoStruct type. | |
:param str class_name: the class name for the new type | |
:param field_names: a sequence of names for the mutable | |
attributes of objects of the new type | |
.. warning:: | |
Dynamically-created PseudoStruct types cannot be pickled! | |
>>> Part = PseudoStruct.define("Part", ["id", "name"]) | |
>>> type(Part) is type | |
True | |
>>> issubclass(Part, PseudoStruct) | |
True | |
>>> Part.__name__ | |
'Part' | |
>>> Part.__slots__ | |
['id', 'name'] | |
The ``define`` method can also create a subclass of a subclass: | |
>>> Part = PseudoStruct.define("Part", ["id", "name"]) | |
>>> Item = Part.define("Item", ["description"]) | |
>>> issubclass(Item, Part) | |
True | |
>>> Item.__name__ | |
'Item' | |
>>> Item.__slots__ | |
['description'] | |
""" | |
return type(class_name, (cls,), {"__slots__": field_names}) | |
def __init__(self, **fields): | |
r"""Initialize by passing the field name/value pairs as keyword | |
arguments. | |
:param dict fields: name/value pairs to initialize this object's | |
attributes | |
:raise AttributeError: if any name in *fields* is not defined as | |
a slot | |
>>> class Attribute(PseudoStruct): | |
... __slots__ = ["name", "value"] | |
... | |
>>> attr = Attribute(name="color", value="orange") | |
>>> attr.name | |
'color' | |
>>> attr.value | |
'orange' | |
""" | |
for (name, value) in fields.items(): | |
setattr(self, name, value) | |
def __getattr__(self, name): | |
r"""Lazily initialize the value for the *name* slot. | |
:param str name: the name of an attribute of this object | |
:raise AttributeError: if *name* is not a valid attribute name | |
for this object | |
:return: ``None`` | |
This method will be called **at most once**, and only if a value | |
has not yet been assigned to the *name* attribute. In this case, | |
``None`` is set and returned as the default value: | |
>>> class Attribute(PseudoStruct): | |
... __slots__ = ["name", "value"] | |
... | |
>>> attr = Attribute() | |
>>> attr.name is None | |
True | |
""" | |
setattr(self, name, None) | |
def __iter__(self): | |
r"""Return an iterator over all ``(name, value)`` pairs for this | |
object's slots, in the order in which the slots were defined. | |
>>> Part = PseudoStruct.define("Part", ["id", "name"]) | |
>>> Item = Part.define("Item", ["desc"]) | |
>>> item = Item(desc="3-sided polygon", id=1, name="triangle") | |
>>> list(iter(item)) | |
[('id', 1), ('name', 'triangle'), ('desc', '3-sided polygon')] | |
""" | |
if (self.__class__ is PseudoStruct): | |
return | |
# be sure to include my own __slots__ | |
classes = [self.__class__] | |
# classes that define __slots__ may only use single inheritance | |
parent = self.__class__.__bases__[0] | |
while (parent is not PseudoStruct): | |
classes.append(parent) | |
parent = parent.__bases__[0] | |
yielded_slots = set() | |
for class_ in reversed(classes): | |
for name in class_.__slots__: | |
# don't yield same (name, value) more than once - this can | |
# happen because it is legal for a subclass to repeat a | |
# slot name | |
if (name not in yielded_slots): | |
yielded_slots.add(name) | |
yield (name, getattr(self, name)) | |
def __repr__(self): | |
r"""Return a string representation of this PseudoStruct. | |
>>> class Part(PseudoStruct): | |
... __slots__ = ["id", "name"] | |
... | |
>>> class Item(Part): | |
... __slots__ = ["desc"] | |
... | |
>>> item = Item(desc="3-sided polygon", id=1, name="triangle") | |
>>> repr(item) | |
"Item {id: 1, name: 'triangle', desc: '3-sided polygon'}" | |
""" | |
return "%s {%s}" % (self.__class__.__name__, | |
", ".join("%s: %r" % pair for pair in self)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment