Since atom is designed to allow to define compact objects, the best way to illustrate how it works is to study a class definition making use of it. This example will serve to introduce key concepts that will be explained in more details in the following sections.
from atom.api import Atom, Value, Int, List, set_default, observe class CompactObject(Atom): """Compact object generating notifications. """ untyped_value = Value() int_value = Int(10) list_value = List().tag(pref=True) def _post_setattr_int_value(self, old, new): self.untyped_value = (old, new) def _observe_int_value(self, change): print(change) @observe('list_value') def notify_change(self, change): print(change) class NewCompactObject(Atom): """Subclass with different default values. """ list_value = default_value([1, 2]) def _default_int_value(self): return 1
First note that contrary to a number of projects, atom does not export any objects in the top level atom package. To access the publicly available names you should import from atom.api.
from atom.api import Atom, Value, Int, List, default_value, observe
Here we import several things:
Atom: This is the base class to use for all objects relying on Atom. It provides the some basic methods that will be described later on or in the API documentation. (This class inherits from a more basic class CAtom)
List: Those are members. One can think of them as advanced properties (ie they are descriptors). They define the attributes that are available on the instances of the class. They also provide type validation.
observe: This is a decorator. As we will see later, it can be used to call the decorated method when a member value ‘change’.
set_default: Members can have a default value and this object is used to alter it when subclassing an Atom object.
Now, that the imports are hopefully clear (or at least clearer), let’s move to the beginning of the first class definition.
class CompactObject(Atom): """Compact object generating notifications. """ untyped_value = Value() int_value = Int(10) list_value = List().tag(pref=True)
Here we define a class and add to it three members. Those three members will be the attributes, that can be manipulated on the class instances. In particular, the following will crash while it would work for a usual python object:
obj = CompactObject() obj.non_defined = 0
This may be surprising, since on usual Python objects one can define new attributes on instances. This limitation is the price to pay for the compacity of Atom objects.
This limitation should rarely be an issue and if it is one can get dynamic attributes back by adding the following line to the class definition:
__slots__ = ('__dict__',)
Ok, so each member will be one instance attribute. Now, let’s look at them in
more details. Our first member is a simple
Value. This member actaully does
not perform any type validation and can be used when the attributes can really
store anything. Our second member is an
Int. This member will validate that
the assigned value is actually an integer and the default value is 10 instead
of 0. Finally, we have
List which obviously can only be a list. In addition,
we tagged the member. Tags are actually metadata attached to the descriptors.
They have no built-in use in atom but they can be used to filter on an instance
members when filtering them. Refer to the
metadata.py example for an illustration.
All the available members are described in details in Introducing the members
Coming back to the class definition, we now reached the methods definitions.
def _post_setattr_int_value(self, old, new): self.untyped_value = (old, new) def _observe_int_value(self, change): print(change) @observe('list_value') def notify_change(self, change): print(change)
Here we define three methods. None of these are meant to be called directly by the user-code but will be called by the framework at appropriate times.
_post_setattr_int_value: This function will be called right after setting the value of
int_value, as its name indicates. It will get both the value of the member before the setting operation (old) and the value that was just set (set).
_observe_int_value: This function will be called each the value of
int_valuechanges (not necessarily through a setattr operation). It is passed of dictionary containing a bunch of information about the actual modification. We will describe the content of this dictionary in details in Notifications and observers.
notify_changes: Because this function is decorated with the observe decorator, it will be called each time
list_value. Note however, that changes to the container or its content, e.g. through
appendwill not be caught.
Prefixed methods (_post_setattr, _observe, …) are discussed in more details in Customimizing members: specially named methods.
Here, we have only seen observer definition from within a class. It IS possible to define observers on instances and this will be discussed in Notifications and observers.
Now we can look at the second class definition and discuss default values a bit more.
class NewCompactObject(CompactObject): """Subclass with different default values. """ list_value = set_default([1, 2]) def _default_int_value(self): return 1
In this subclass, we simply alter the default values of two of the members. We do that in two ways:
set_defaultwhich indicates to the framework that it should create a copy of the member existing of the base class and change the default value.
using a specially named method starting with
_default_followed by the member name.
To clarify what this does, we look at what happens after we create instances of each of our classes.
obj1 = CompactObject() print(obj1.int_value) print(obj1.list_value) obj2 = NewCompactObject() print(obj2.int_value) print(obj2.list_value)
The output of this block will be:
10: which match the specified default value in the class definition
: which corresponds to the absence of a specific default value for a list.
1: which corresponds to the value returned by the method used to compute the default value.
[1, 2]which corresponds to the default value we specified using
First note, that even though we did not define
__init__ methods, we
can pass any of the members of the class as a keyword argument, in which case
the argument will be used to set the value of the corresponding member.
obj1 = CompactObject(untyped_value='e')
Atom objects can be frozen using
freeze() at any time of their
lifetime to forbid further modifications.
This brief introduction should have given some basics concerning Atom working. The next three sections will cover in more details three points introduced here: the members, notifications and in particular observers specific to an instance, and finally the specially named methods used to alter default member behaviors.
Starting with atom 0.8.0 atom classes can also infer their members from type annotations see Using type annotations