Create a gist now

Instantly share code, notes, and snippets.

from timeit import timeit
from collections import namedtuple
from operator import itemgetter
RUNS = 1000000
Foo = namedtuple('Foo', 'bar baz')
foo = Foo(5, 10)
assert foo.bar == 5
assert foo.baz == 10
def foo_bar_times_baz(foo):
return foo.bar * foo.baz
assert foo_bar_times_baz(foo) == 50
class Foo(namedtuple('Foo', 'bar baz')):
def bar_times_baz(self):
return self.bar * self.baz
foo = Foo(5, 10)
assert foo.bar_times_baz() == 50
Foo = namedtuple('Foo', 'bar baz')
Foo.bar_times_baz = lambda self: self.bar * self.baz
foo = Foo(5, 10)
assert foo.bar_times_baz() == 50
class NamedTuple(tuple):
__slots__ = ()
_fields = None # Subclass must provide this
def __new__(_cls, *args, **kwargs):
if len(args) > len(_cls._fields):
raise TypeError("__new__ takes {} positional arguments but {} were given".format(len(_cls._fields) + 1, len(args) + 1))
missing_args = tuple(fld for fld in _cls._fields[len(args):] if fld not in kwargs)
if len(missing_args):
raise TypeError("__new__ missing {} required positional arguments".format(len(missing_args)))
extra_args = tuple(kwargs.pop(fld) for fld in _cls._fields[len(args):] if fld in kwargs)
if len(kwargs) > 0:
raise TypeError("__new__ got an unexpected keyword argument '{}'".format(list(kwargs.keys())[0]))
return tuple.__new__(_cls, tuple(args + extra_args))
def _make(self, iterable, new=tuple.__new__, len=len):
'Make a new Bar object from a sequence or iterable'
cls = self.__class__
result = new(cls, iterable)
if len(result) != len(cls._fields):
raise TypeError('Expected {} arguments, got {}'.format(len(self._fields), len(result)))
return result
def __repr__(self):
'Return a nicely formatted representation string'
fmt = '(' + ', '.join('%s=%%r' % x for x in self._fields) + ')'
return self.__class__.__name__ + fmt % self
def _asdict(self):
'Return a new OrderedDict which maps field names to their values'
return OrderedDict(zip(self._fields, self))
__dict__ = property(_asdict)
def _replace(_self, **kwds):
'Return a new Bar object replacing specified fields with new values'
result = _self._make(map(kwds.pop, _self._fields, _self))
if kwds:
raise ValueError('Got unexpected field names: %r' % list(kwds))
return result
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return tuple(self)
def __getstate__(self):
'Exclude the OrderedDict from pickling'
return None
def __getattr__(self, field):
try:
idx = self._fields.index(field)
except ValueError:
raise AttributeError("'{}' NamedTuple has no attribute '{}'".format(self.__class__.__name__, field))
return self[idx]
class FooSubClass(NamedTuple):
_fields = ('bar', 'baz')
def bar_times_baz(self):
return self.bar * self.baz
foo_sub_class = FooSubClass(5, 10)
assert foo_sub_class.bar_times_baz() == 50
assert foo_sub_class == (5, 10)
# This isn't great because we can add attributes
foo_sub_class.blah = 29
class FooSubClass(NamedTuple):
__slots__ = ()
_fields = ('bar', 'baz')
def bar_times_baz(self):
return self.bar * self.baz
foo_sub_class = FooSubClass(5, 10)
assert foo_sub_class.bar_times_baz() == 50
# Check we can't assign other attributes
try:
foo_sub_class.blah = 29
raise Exception("Should fail with attribute error.")
except AttributeError:
pass
assert foo_sub_class == (5, 10)
direct_idx_time = timeit('foo[0]', setup='from __main__ import foo', number=RUNS)
direct_attr_time = timeit('foo.bar', setup='from __main__ import foo', number=RUNS)
sub_class_idx_time = timeit('foo[0]', setup='from __main__ import foo_sub_class as foo', number=RUNS)
sub_class_attr_time = timeit('foo.bar', setup='from __main__ import foo_sub_class as foo', number=RUNS)
print("namedtuple (idx time) ", direct_idx_time)
print("namedtuple (attr time)", direct_attr_time)
print("subclass (idx time) ", sub_class_idx_time)
print("sublcass (attr time)", sub_class_attr_time)
def optimize(cls):
for idx, fld in enumerate(cls._fields):
setattr(cls, fld, property(itemgetter(idx), doc='Alias for field number {}'.format(idx)))
setattr(cls, "blah", 37)
return cls
@optimize
class FooSubClassOptimized(NamedTuple):
__slots__ = ()
_fields = ('bar', 'baz')
blahX = 42
def bar_times_baz(self):
return self.bar * self.baz
foo_sub_class_optimized = FooSubClassOptimized(5, 10)
assert foo_sub_class_optimized.bar_times_baz() == 50
try:
foo_sub_class.blah = 29
raise Exception("Should fail with attribute error.")
except AttributeError:
pass
assert foo_sub_class_optimized == (5, 10)
sub_class_optimized_idx_time = timeit('foo[0]', setup='from __main__ import foo_sub_class_optimized as foo', number=RUNS)
sub_class_optimized_attr_time = timeit('foo.bar', setup='from __main__ import foo_sub_class_optimized as foo', number=RUNS)
print("subclass-opt (idx time) ", sub_class_optimized_idx_time)
print("subclass-opt (attr time)", sub_class_optimized_attr_time)
def namedfields(*fields):
def inner(cls):
if not issubclass(cls, tuple):
raise TypeError("namefields decorated classes must be subclass of tuple")
attrs = {
'__slots__': (),
}
methods = ['__new__', '_make', '__repr__', '_asdict',
'__dict__', '_replace', '__getnewargs__',
'__getstate__']
attrs.update({attr: getattr(NamedTuple, attr) for attr in methods})
attrs['_fields'] = fields
attrs.update({fld: property(itemgetter(idx), doc='Alias for field number {}'.format(idx))
for idx, fld in enumerate(fields)})
attrs.update({key: val for key, val in cls.__dict__.items()
if key not in ('__weakref__', '__dict__')})
return type(cls.__name__, cls.__bases__, attrs)
return inner
@namedfields('bar', 'baz')
class FooDecorate(tuple):
def bar_times_baz(self):
return self.bar * self.baz
foo_decorate = FooDecorate(5, 10)
assert foo_decorate.bar_times_baz() == 50
decorate_idx_time = timeit('foo[0]', setup='from __main__ import foo_decorate as foo', number=RUNS)
decorate_attr_time = timeit('foo.bar', setup='from __main__ import foo_decorate as foo', number=RUNS)
print("decorator (idx time) ", decorate_idx_time)
print("decorator (attr time)", decorate_attr_time)
Foo = namedfields('bar', 'baz')(type('Foo', (tuple, ), {}))
foo = Foo(5, 10)
assert foo == (5, 10)
print(timeit('foo.bar', setup='from __main__ import foo', number=RUNS))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment