Python Cookbook 2Nd Edition Jun 1002005 [Electronic resources] نسخه متنی

This is a Digital Library

With over 100,000 free electronic resource in Persian, Arabic and English

Python Cookbook 2Nd Edition Jun 1002005 [Electronic resources] - نسخه متنی

David Ascher, Alex Martelli, Anna Ravenscroft

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید


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.

/ 394