Skip to main content

yet another namedtuple alternative

Project description

Yet another namedtuple alternative for Python

compose.Struct is something like an alternative to namedtuple, attrs and now dataclasses in Python 3.7.

to create a new struct, you simply:

class Foo(compose.Struct):
    bar = ...
    baz = 'spam'

This generates a class like this:

class Foo:
    __slots__ = 'bar', 'baz'

    def __init__(self, bar, baz='spam'):
        self.bar = bar
        self.baz = baz

You can, naturally, implement any other methods you wish.

You can also use type annotation syntax for positional arguments:

class Foo(compose.Struct):
    bar: int
    baz: str = 'spam'

If the name = ... syntax is used in combination with type annotation syntax for positional arguments, all positional arguments with annotations will come before positional arguments without. However, this should be considered an implementation detail. best practice is to not mix the two styles. Use typing.Any if you are using type annotations and don’t want one of the arguments to care about type.

How’s this different from attrs and dataclasses? A few ways. Aside from the use of ellipsis to create positional parameters, another difference that can be seen here is that everything is based on __slots__, which means your attribute lookup will be faster and your instances more compact in memory. attrs allows you to use slots, but struct only uses slots. This means that attributes cannot be dynamically created. If a class needs private attributes, you may create additional slots with the usual method of defining __slots__ inside the class body.

Another important distinction is compose.Struct doesn’t define a bunch of random dunder methods. You get your __init__, __repr__, and to_dict and that’s it [1]. It is the opinion of the author that sticking all attributes in a tuple and comparing them usually is not what you want when defining a new type. However, it is still easy to get more dunder methods, as you will see in the following section.

Interfaces

Perhaps the most significant difference between our structs and alternatives is that we emphasize composition over inheritance. A struct isn’t even able to inherit in the normal way! It’s an outrage! What about interfaces!? What about polymorphism!? Well, what compose provides is a simple way to generate pass-through methods to attributes.

from compose import Struct, Provider

class ListWrapper(Struct):
    data = Provider('__getitem__', '__iter__')
    metadata = None

So this will generate pass-through methods for __getitem__ and __iter__ to the data attribute. Certain python keywords and operators can be used as shorthand for adding dunder methods as well.

@struct
class ListWrapper:
    data = Provider('[]', 'for')
    metadata = None

Here, [] is shorthand for item access and implements __getitem__, __setitem__ and __delitem__. for implements the __iter__ method. A full list of these abbreviations can be found below in the Pre-Defined Interfaces section.

Going even deeper, interfaces can be specified as classes. Wrapper methods will be created for any method attached to a class which is given as an argument to Provider. The following code is more or less equivalent to subclassing collections.UserList, but no inheritance is used.

from collections import abc

class ListWrapper(Struct):
    data = Provider(abc.MutableSequence)
    metadata = None

An instances of this class tested with isinstance(instance, abc.MutableSequence) will return True because wrapper methods have been generated on self.data for all the methods in abc.MutableSequence. Note that ``abc.MutableSequence`` does not actually provide all of the methods a real list does. If you want ALL of them, you can use ``Provides(list)``.

You cannot implicitly make pass-through methods for __setattr__ and __getattribute__ by passing in a class that implements them, since they have some rather strange behaviors. You can, however, pass them explicitly to Provider to force the issue. In the case of __setattr__, This invokes special behavior. See __setattr__ hacks for details.

All methods defined with a provider can be overridden in the body of the class as desired. Methods can also be overridden by other providers. It’s first-come, first-serve in that case. The Provider you want to define the methods has to be placed above any other interfaces that implement the same method.

Mix-in Classes vs. Inheritance

There is no inheritance with Structs. Because of metaclass magic, a class that inherits from Struct is not its child. It is always a child of object. Provider is a way to implement pass-through methods easily. Mix-in classes bind methods from other classes directly to your class. It doesn’t go through the class hierarchy and rebind everything, only methods defined directly on the mix-in class. Inheriting from normal python classes may have unpredictable results.

compose provides one mix-in class: Immutable, which is implemented like this:

class Mutablility(Exception):
    pass


class Immutable:
    def __setattr__(self, attr, value):
        raise Mutablility(
            "can't set {0}.{1}. type {0} is immutable.".format(
                self.__class__.__name__,
                attr,
                value
            ))

It can be used like this:

from compose import Struct, Immutable


class Foo(Struct, Immutable):
    bar = ...
    baz = ...

When an instance of Foo is created, it will not be possible to set attributes afterwards in the normal way. (Though it is technically possible if you set it with object.__setattr__(instance, 'attr', value)). Attempting to do foo.bar = 7 will raise a Mutability error.

If you need a struct to look like a child of another class, I suggest using the abc module to define abstract classes. This allows classes to look like children for the purposes of type-checking, but without actually using inheritance.

Order

This is the order of priority for where methods come from:

  • Struct generates a unique __init__ method for each class it creates. This cannot be overriden. Alternative constructors should be implemented as class methods.

  • methods defined in the body of the struct get next dibs.

  • any attributes defined on your mix-ins will be defined on the class if they don’t already exist.

  • Only then are Provider attributes allowed to add any methods which haven’t yet been defined.

*args and **kwargs

Though it is not especially recommended, it is possible to implement *args and **kwargs for your constructor.

>>> from compose import Struct, args, kwargs
>>> class Foo(Struct):
...     items = args
...     mapping = kwargs
...
>>> f = Foo('bar', 'baz', spam='eggs')
>>> f
Foo(*items=('bar', 'baz'), **mapping={'spam': 'eggs'})

This breaks the principle that the object’s repr can be used to instantiate an identical instance, but it does at least give the option and still makes the internal structure of the class transparent. With Provider parameters, simply pass in compose.args or compose.kwargs as arguments the constructor.

>>> class MySequence(Struct):
...     data = Provider('__getitem__', '__iter__', args)
...
>>> s = MySequence('foo', 'bar', 'baz')
>>> s
MySequence(*data=('foo', 'bar', 'baz'))
>>> for i in s:
...     print(i)
...
foo
bar
baz

Caveats

This library uses code generation at class-creation time. The intent is to optimize performance of instances at the cost of slowing class creation. If you’re dynamically creating huge numbers of classes, using compose.Struct might be a bad idea. FYI, namedtuple does the same. I haven’t looked at the source for attrs too much, but I did see some strings with sourcecode there as well.

Pre-Defined Interfaces

This is the code that implements the expansion of interface abbreviations for dunder methods. Any key in the interfaces dictionary may be used to implement the corresponding dunder methods on an attribute with the Provides() constructor.

interfaces = {
    '+': 'add radd',
    '-': 'sub rsub',
    '*': 'mul rmul',
    '@': 'matmul rmatmul',
    '/': 'truediv rtruediv',
    '//': 'floordiv rfloordiv',
    '%': 'mod rmod',
    '**': 'pow rpow',
    '<<': 'lshift rlshift',
    '>>': 'rshift rrshift',
    '&': 'and rand',
    '^': 'xor rxor',
    '|': 'or ror',
    '~': 'invert',
    '==': 'eq',
    '!=': 'ne',
    '>': 'gt',
    '<': 'lt',
    '>=': 'ge',
    '<=': 'le',
    '()': 'call',
    '[]': 'getitem setitem delitem',
    '.': 'get set delete set_name',
    'in': 'contains',
    'for': 'iter',
    'with': 'enter exit',
    'del': 'del',
    'await': 'await'
}
interfaces = {k: ['__%s__' % n for n in v.split()]
              for k, v in interfaces.items()}

__setattr__ hacks

If you choose to create an attribute wrapper for __setattr__, the default will look like this so you won’t hit a recursion error while accessing pre-defined attributes:

def __setattr__(self, attribute, value):
    try:
        object.__setattr__(self, attribute, value)
    except AttributeError:
        setattr(self.wrapped_attribute, attribute, value)

If you want to override __setattr__ with a more, eh, “exotic” method, the attributes defined in the class body will be set properly when the instance is initialized, but will use your method at all other times, including in other methods, which may break your stuff.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

compose_struct-0.8.0.tar.gz (11.0 kB view hashes)

Uploaded Source

Built Distribution

compose_struct-0.8.0-py3-none-any.whl (16.1 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page