Introducing the members¶
As we have seen in the introduction, members are used in the class definition of an atom object to define the fields that will exist on each instance of that class. As such members are central to atom.
The following sections will shed some lights on the different members that come with atom and also how they work which will come handy when we will discuss how you can customize the behaviors of members later in this guide.
From a technical point of view, members are descriptors like property and they can do different things when you try to access or set the attribute.
Let’s first look at what happen when you access an attribute:
class Custom(Atom): value = Int() obj = Custom() obj.value obj.value
Since we did not pass a value for
value when instantiating our class (we
did not do
obj=Custom(value=1)), when we first access
value it does not
have any value. As a consequence the framework will fetch the default value.
As we have seen in the introduction, the default value can be specified in
several ways, either as argument to the member, or using
set_default or even
by using a specially named method (more on that in
Customimizing members: specially named methods).
Once the framework has fetched the default value it will validate it. In particular here, we are going to check that we did get an integer for example. The details of the validation will obviously depend on the member.
If the value is valid, next a post-validation method will be called that can some do further processing. By default this is a no-op and we will see in Customimizing members: specially named methods how this can be customized.
With this process complete, the state of our object has changed since we created the value stored in that instance. This corresponds to a create that will be sent to the observers if any is registered.
The observer called, the value can now be stored (so that we don’t go through this again) and is now ready to be returned to the sure, the get step is complete. However before doing that we will actually perform a post-gettatr step. Once again this is a no-op by default but can be customized.
On further accesses, since the value exists, we will go directly retrieve the value and perform the post-getattr, and no notification will be generated.
Setting a value follows a very similar pattern. First the value is of course validated (and post-validated). It is then actually stored (set).
Next as for the get and validate operation, a post-setattr step is run. As for the other post by default this won’t do anything.
Finally is any observer is attached, the observers are notified.
Now that the behavior of members is a bit less enigmatic let’s introduce the members that comes with atom.
Members for simple values¶
Atom provides the following members for basic scalars types:
Value: a member that can receives any value, no validation is performed
Int: an integer value. One can choose if it is allowed to cast the assigned values (float to int), the default is true.
Float: a floating point value. One can choose if it is allowed to cast the assigned values (int to float, …), the default is true.
Str: bytes and unicode strings. One can choose if it is allowed to cast the assigned values (str to bytes, …), the default is false.
Enum: a value that can only take a finite set of values. Note that this is unrelated to the enum module.
Containers and type validation¶
Atom also provides members for three basic containers tuple, list and
Dict. In each case, you can specify the type
of the values (key and value for dict), using members, as follows:
class MyAtom(Atom): t = Tuple(Int()) l = List(Float()) d = Dict(Str(), Int())
Alternatively, you can pass simple Python types. In this cases they will be
wrapped in an
Instance member that will be introduced in the next section.
class MyAtom(Atom): t = Tuple(int) l = List(float) d = Dict(str, int)
Note that you cannot (by default) enforce a specific number of items in a tuple.
In order to enforce type validation of container, atom has to use custom subclass. As a consequence, when assigning to a member, the original container is copied. This copy on assignment behavior can cause some surprises if you modifiy the original container after assigning it.
One additional important point, atom does not track the content of the
container is not tracked. As a consequence, in place modification of the
container do not trigger any notifications. One workaround can be to copy the
container, modify it and re-assign it. Another option for lists is to use a
ContainerList member, which uses a special list subclass sending
notifications when the list is modified.
Enforcing custom types¶
Sticking to simple types can quickly be limitating and this is why atom provides member to enforce that the value is simply of a certain type or a subclass:
Typed: the value must of the specified type or a subtypes. Only one type can be specified.
Instance: the value must pass
isinstance(value, types)). Using
Instanceonce can specify a tuple of types.
Subclass: the value must be a class and a subclass of the specified type.
In some cases, the type is not accessible when the member is instantiated
(because it will be created later in the same file for example), atom also
ForwardSubclass. Those three
members rather than taking a type or a tuple of type as argument, accept a
callable taking no argument and returning the type(s) to use for validation.
class Leaf(Atom): node = ForwardTyped(lambda : Node) class Node(Atom): parent = ForwardTyped(lambda : Node) leaves = List(Typed(Leaf))
In some cases, the same information may be convienently represented either by
a custom class or something simpler, like a tuple. One example of such a use
case is a color: a color can be easily represented by the four components
(red, green, blue, alpha) but in a library may be represented by a custom
class. Atom provides the
Coerced member to allow to enforce a particular
type while also allowing seamless conversion from alternative representations.
The conversion can occur in two ways as illustrated below:
- by calling the specified types on the provided value
- by calling an alternative coercer function provided to the member
class Color(object): def __init__(self, components): self.red, self.green. self.blue, self.alpha = components def dict_to_color(color_dict): components =  for c in ('red', 'green', 'blue', 'alpha') components.append(color_dict[c]) return Color(components) class MyAtom(Atom): color = Coerced(Color) color2 = Coerced(Color, coercer=dict_to_color)
Memory less members¶
Atom also provides two members that do not remember the value they are provided, but that can be used to fire notifications:
Event: this is a member to which each time a value is assigned to, a notification is fired. Additionally one can specify the type of value that are accepted. An alternative way to fire the notification is to call the object you get when accessing the member.
Signal: this member is similar to Qt signal. One cannot be assigned to it, however one can call it on instances, and when called the notifier will be called with the arguments and keyword arguments passed to the signal. Note that this is at odds with the general behavior of observers described in Notifications and observers.
The example below illustrates how those members work:
class MyAtom(Atom): s = Signal() e = Event() @observe('s', 'e') def print_value(self, change): print(change) obj = MyAtom() obj.e = 2 obj.e(1) obj.s(2) obj.emit(1)
This last member is a bit special. It does not do anything by itself but can be used to copy the behaviors of another member. In addition, any observer attached to the delegator will also be attached to the delegate member.