Recipe20.14.Automatic Initialization of Instance Attributes


Recipe 20.14. Automatic Initialization of Instance Attributes

Credit: Sébastien Keim, Troy Melhase, Peter Cogolo

Problem

You want to set some attributes to constant values, during object initialization, without forcing your subclasses to call your _ _init_ _ method.

Solution

For constant values of immutable types, you can just set them in the class. For example, instead of the natural looking:

class counter(object):     def _ _init_ _(self):         self.count = 0     def increase(self, addend=1):         self.count += addend

you can code:

class counter(object):     count = 0     def increase(self, addend=1):         self.count += addend

This style works because self.count += addend, when self.count belongs to an immutable type, is exactly equivalent to self.count = self.count + addend. The first time this code executes for a particular instance self, self.count is not yet initialized as a per-instance attribute, so the per-class attribute is used, on the right of the equal sign (=); but the per-instance attribute is nevertheless the one assigned to (on the left of the sign). Any further use, once the per-instance attribute has been initialized in this way, gets or sets the per-instance attribute.

This style does not work for values of mutable types, such as lists or dictionaries. Coding this way would then result in all instances of the class sharing the same mutable-type object as their attribute. However, a custom descriptor works fine:

class auto_attr(object):     def _ _init_ _(self, name, factory, *a, **k):         self.data = name, factory, a, k     def _ _get_ _(self, obj, clas=None):         name, factory, a, k = self.data         setattr(obj, name, factory(*a, **k))         return getattr(obj, name)

With class auto_attr at hand, you can now code, for example:

class recorder(object):     count = 0     events = auto_attr('events', list)     def record(self, event):         self.count += 1         self.events.append((self.count, event))

Discussion

The simple and standard approach of defining constant initial values of attributes by setting them as class attributes is just fine, as long as we're talking about constants of immutable types, such as numbers or strings. In such cases, it does no harm for all instances of the class to share the same initial-value object for such attributes, and, when you do such operations as self.count += 1, you intrinsically rebind the specific, per-instance value of the attribute, without affecting the attributes of other instances.

However, when you want an attribute to have an initial value of a mutable type, such as a list or a dictionary, you need a little bit moresuch as the auto_attr custom descriptor type in this recipe. Each instance of auto_attr needs to know to what attribute name it's being bound, so we pass that name as the first argument when we instantiate auto_attr. Then, we have the factory, a callable that will produce the desired initial value when called (often factory will be a type object, such as list or dict); and finally optional positional and keyword arguments to be passed when factory gets called.

The first time you access an attribute named name on a given instance obj, Python finds in obj's class the descriptor (an instance of auto_attr) and calls the descriptor's method _ _get_ _, with obj as an argument. auto_attr's _ _get_ _ calls the factory and sets the result under the right name as an instance attribute, so that any further access to the attribute of that name in the instance gets the actual value.

In other words, the descriptor is designed to hide itself when it's first accessed on each instance, to get out of the way from further accesses to the attribute of the same name on that same instance. For this purpose, it's absolutely crucial that auto_attr is technically a nondata descriptor class, meaning it doesn't define a _ _set_ _ method. As a consequence, an attribute of the same name may be set in the instance: the per-instance attribute overrides (i.e., takes precedence over) the per-class attribute (i.e., the instance of a nondata descriptor class).

You can regard this recipe's approach as "just-in-time generation" of instance attributes, the first time a certain attribute gets accessed on a certain instance. Beyond allowing attribute initialization to occur without an _ _init_ _ method, this approach may therefore be useful as an optimization: consider it when each instance has a potentially large set of attributes, maybe costly to initialize, and most of the attributes may end up never being accessed on each given instance.

It is somewhat unfortunate that this recipe requires you to pass to auto_attr the name of the attribute it's getting bound to; unfortunately, auto_attr has no way to find out for itself. However, if you're willing to add a custom metaclass to the mix, you can fix this little inconvenience, too, as follows:

class smart_attr(object):     name = None     def _ _init_ _(self, factory, *a, **k):         self.creation_data = factory, a, k     def _ _get_ _(self, obj, clas=None):         if self.name is None:             raise RuntimeError, ("class %r uses a smart_attr, so its "                 "metaclass should be MetaSmart, but is %r instead" %                 (clas, type(clas)))         factory, a, k = self.creation_data         setattr(obj, name, factory(*a, **k))         return getattr(obj, name) class MetaSmart(type):     def _ _new_ _(mcl, clasname, bases, clasdict):         # set all names for smart_attr attributes         for k, v in clasdict.iteritems( ):             if isinstance(v, smart_attr):                 v.name = k         # delegate the rest to the supermetaclass         return super(MetaSmart, mcl)._ _new_ _(mcl, clasname, bases, clasdict) # let's let any class use our custom metaclass by inheriting from smart_object class smart_object:     _ _metaclass_ _ = MetaSmart

Using this variant, you could code:

class recorder(smart_object):     count = 0     events = smart_attr(list)     def record(self, event):         self.count += 1         self.events.append((self.count, event))

Once you start considering custom metaclasses, you have more options for this recipe's task, automatic initialization of instance attributes. While a custom descriptor remains the best approach when you do want "just-in-time" generation of initial values, if you prefer to generate all the initial values at the time the instance is being initialized, then you can use a simple placeholder instead of smart_attr, and do more work in the metaclass:

class attr(object):     def _ _init_ _(self, factory, *a, **k):         self.creation_data = factory, a, k import inspect def is_attr(member):     return isinstance(member, attr) class MetaAuto(type):     def _ _call_ _(cls, *a, **k):         obj = super(MetaAuto, cls)._ _call_ _(cls, *a, **k)         # set all values for 'attr' attributes         for n, v in inspect.getmembers(cls, is_attr):             factory, a, k = v.creation_data             setattr(obj, n, factory(*a, **k))         return obj # lets' let any class use our custom metaclass by inheriting from auto_object class auto_object:     _ _metaclass_ _ = MetaAuto

Code using this more concise variant looks just about the same as with the previous one:

class recorder(auto_object):     count = 0     events = attr(list)     def record(self, event):         self.count += 1         self.events.append((self.count, event))

See Also

Recipe 20.13 for another approach that avoids _ _init_ _ for attribute initialization needs; Library Reference and Python in a Nutshell docs on special method _ _init_ _, and built-ins super and setattr.



Python Cookbook
Python Cookbook
ISBN: 0596007973
EAN: 2147483647
Year: 2004
Pages: 420

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net