Attributes

Elk attributes have many properties. You can create a powerful class simply by declaring attributes.

An attributes is a property that every member of a class has. For example, every Person might have a name and a date of birth. Attributes can be optional, so some Person objects might have a tax file number, and some might not.

At its simplest, an attribute can be thought of as a named value that can be read and set. However, attributes can also have defaults, type constraints, delegation and much more.

Attribute options

The options passed to ElkAttribute define the properties of the attribute. There are many options but none are required.

Read-write versus read-only

The mode option is used to control whether an attribute is read-only ("ro") or read-write ("rw"). When an attribute is read-only, attempting to set or delete its value raises AttributeError.

Required or not?

By default, all attributes are optional and do not need to be provided when an object is constructed. If you want to make an object required, simply set the required option to True:

class Person(elk.Elk):
    name = elk.ElkAttribute(required=True)

Constructing an object without supplying required attributes (as keyword arguments) will raise AttributeError. Attempting to delete a required attribute will also raise AttributeError:

person = Person()               # raises AttributeError
person = Person(name='Alice')   # ok
del person.name                 # raises AttributeError

Using required alongside default or builder relaxes the need to supply a value to the constructor, but if a value is given it is preferred over the default value.

Default and builder methods

Attributes can have default values, and Elk provides two ways to specify that default.

In the simplest form, simply provide a value for the default option:

class TeeShirt(elk.Elk):
    size = elk.ElkAttribute(default="medium")

If the size attribute is not provided to the constructor, it will be set to "medium":

shirt = TeeShirt()
shirt.size           # "medium"

You can also provide a callable for default. The callable will be called with the object as the single argument and its return value will be the value of the attribute:

import random

class TeeShirt(elk.Elk):
    size = elk.ElkAttribute(
        default=lambda self: random.choice(['small', 'medium', 'large'])
    )

This is a trivial example, but it illustrates the point that the callable will be called for every new object created.

When the default is called during object construction, it may be called before other attributes have been set. if your default is dependent on other parts of the object’s state, you can make the attribute lazy.

As an alternative to directly using a callable, you can supply a builder method for your attribute:

class TeeShirt(elk.Elk):
    size = elk.ElkAttribute(builder='_build_size')

    def _build_size(self):
        return random.choice(['small', 'medium', 'large'])

This has several advantages. First, it moves a chunk of code to its own named method, which improves readability and separation of concerns. Second, because this is a named method, it can be extended or overridden by a subclass.

It is strongly recommended to use a builder instead of a default for anything beyond the most trivial default.

A builder, just like a default, is called with the object as the single argument.

Builders allow subclassing

Because the builder is called by name, builder methods are both inheritable and overridable.

If we subclass our our TeeShirt class, we can override _build_size:

class SmallTeeShirt(TeeShirt):
    def _build_size(self):
        return 'small'

Builders work well with roles

Because builders are called by name, they work well with Roles. For example, a role could provide an attribute but require that the consuming class provide the builder:

class HasSize(elk.ElkRole):
    size = elk.ElkAttribute(builder='_build_size')

class Thing(elk.Elk):
    __with__ = HasSize

    def _build_size(self):
        return 'small'

Laziness

Elk lets you defer attribute population by making an attribute lazy:

class TeeShirt(elk.Elk):
    size = elk.ElkAttribute(builder='_build_size', lazy=True)

When lazy is true, the default is not generated until the attribute is read, rather than at object construction time. There are several reasons why you might choose to do this:

  • If the value depends on other attributes, then the attribute must be lazy because the order in which attribute values are set during object construction is not specified.
  • Making an attribute lazy lets you defer the cost of computing its value until it is needed. If the attribute is never read, you avoid doing the work at all.

It is recommended to make any attribute with a builder or non-trivial default lazy as a matter of course.

Constructor parameters

By default, each attribute can be passed by name to the class’s constructor. On occasion, you may want to use a different name for the constructor parameter. You may also want to make an attribute unsettable via the constructor.

You can do either of these things with the init_arg option:

class TeeShirt(elk.Elk):
    bigness = elk.ElkAttribute(init_arg='size')

Now we have an attributed named "bigness", but we pass size to the constructor.

Even more useful is the ability to disable setting an attribute via the constructor. This is particularly handy for private attributes:

_genetic_code = elk.ElkAttribute(
    lazy=True,
    builder='_build_genetic_code',
    init_arg=None
)

By setting the init_arg to None we make it impossible to set this attribute when creating a new object. Attempting to do so raises TypeError.

Type constraints

Attributes can be restricted to only accept certain types. For example, to restrict an attribute to strings:

first_name = elk.ElkAttribute(type=str)

It is also possible to restrict values to one of a set of types by specifying a tuple:

x = elk.ElkAttribute(type=(float, complex))

Constructing with or assigning a value of the wrong type will raise TypeError.

Delegation

An attribute can define names that will be added to the object and will delegate to the attribute’s value:

color = elk.ElkAttribute(type=Color, handles=['as_hex_string'])

This will add the as_hex_string method to the object containing the attribute, which when called will call as_hex_string on the attribute value, as if obj.color.as_hex_string had been called.

You can delegate to methods, Elk attributes or regular attributes. Attribute assignment and deletion works as normal through delegations (even through multiple levels of delegation).

See Delegation for documentation on how to set up delegation.

Attribute inheritance

A subclass inherits all of its base class(es)’ attributes as-is. However, you can override the inherited attribute.

Shorthand

Typing elk.ElkAttribute all the time can get a bit tedious. The attr alias is provided as a convenience:

class Person(elk.Elk):
    name = elk.attr(required=True)