Chapter 8. Controlling Attribute Access

Let’s start with one very small and simple new feature. Here is the start of the definition of a simple Point class:

class Point:

    __slots__ = ("x", "y")

    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y


When a class is created without the use of __slots__, behind the scenes Python creates a private dictionary called __dict__ for each instance, and this dictionary holds the instance’s data attributes. This is why we can add or remove attributes from objects. (For example, we added a cache attribute to the get_function() function earlier in this short cut.)

If we only need objects where we access the original attributes and don’t need to add or remove attributes, we can create classes that don’t have a __dict__. This is achieved simply by defining a class attribute called __slots__ whose value is a tuple of attribute names. Each object of such a class will have attributes of the specified names and no __dict__; no attributes can be added or removed from such classes. These objects consume less memory than conventional objects, although this is unlikely to make much difference unless large numbers of objects are created.

Having seen how to limit a class’s attributes, we will now look at an example where attribute values are computed on the fly rather than stored. Here’s the complete implementation of such a class:

class Ord:

    def __getattr__(self, char):
        return ord(char)


With the Ord class available, we can create an instance, ord = Ord(), and then have an alternative to the built-in ord() function that works for any character that is a valid identifier. For example, ord.a returns 97, ord.Z returns 90, and ord.å returns 229. (But ord.! and similar are syntax errors.)

Note that if we typed the Ord class into IDLE it would not work if we then typed ord = Ord(). This is because the instance has the same name as the built-in ord() function that the Ord class uses, so the ord() call would actually become a call to the ord instance and result in a TypeError exception. The problem would not arise if we imported a module containing the Ord class because the interactively created ord object and the built-in ord() function used by the Ord class would be in two separate modules, so one would not displace the other. If we really need to create a class interactively and to reuse the name of a built-in we can do so by ensuring that the class calls the built-in—in this case by importing the builtins module which provides unambiguous access to all the built-in functions, and calling builtins.ord() rather than plain ord().

Here’s another tiny yet complete class. This one allows us to create “constants”. It isn’t difficult to change the values behind the class’s back, but it can at least prevent simple mistakes.

class Const:

    def __setattr__(self, name, value):
        if name in self.__dict__:

            raise ValueError("cannot change a const attribute")
        self.__dict__[name] = value

    def __delattr__(self, name):
        if name in self.__dict__:
            raise ValueError("cannot delete a const attribute")
        raise AttributeError("'{0}' object has no attribute '{1}'"
                             .format(self.__class__.__name__, name))


With this class we can create a constant object, say, const = Const(), and set any attributes we like on it, for example, const.limit = 591. But once an attribute’s value has been set, although it can be read as often as we like, any attempt to change or delete it will result in a ValueError exception being raised. We have not reimplemented __getattr__() because the base class object.__getattr__() method does what we want—returns the given attribute’s value or raises an AttributeError exception if there is no such attribute. In the __delattr__() method we mimic the __getattr__() method’s error message for nonexistent attributes, and to do this we must get the name of the class we are in as well as the name of the nonexistent attribute. The class works because we are using the object’s __dict__ which is what the base class __getattr__(), __setattr__(), and __delattr__() methods use, although here we have used only the base class’s __getattr__() method. All the special methods used for attribute access are listed in Table 2.

Table 2. Attribute Access Special Methods

image

There is another way of getting constants: We can use named tuples. Here are a couple of examples:

Const = collections.namedtuple("_", "min max")(191, 591)
Const.min, Const.max                        # returns: (191, 591)
Offset = collections.namedtuple("_", "id name description")(*range(3))
Offset.id, Offset.name, Offset.description  # returns: (0, 1, 2)


In both cases we have just used a throwaway name for the named tuple because we want just one named tuple instance each time, not a tuple subclass for creating instances of a named tuple. Although Python does not support an enum data type, we can use named tuples as we have done here to get a similar effect.

For our last look at attribute access special methods we will use the book’s Image.py example. This module defines an Image class whose width, height, and background color are fixed when an Image is created (although they are changed if an image is loaded). We provided access to them using read-only properties. For example, we had:

     @property
     def width(self):
         return self.__width


This is easy to code but could become tedious if there are a lot of read-only properties. Here is a different solution that handles all the Image class’s read-only properties in a single method:

     def __getattr__(self, name):
         if name == "colors":
             return set(self.__colors)
         classname = self.__class__.__name__
         if name in frozenset({"background", "width", "height"}):
             return self.__dict__["_{0}__{1}".format(classname, name)]
         raise AttributeError("'{0}' object has no attribute '{1}'"
                              .format(classname, name))


If we attempt to access an object’s attribute and the attribute is not found, Python will call the __getattr__() method (providing it is implemented, and that we have not reimplemented __getattribute__()), with the name of the attribute as a parameter. Implementations of __getattr__() must raise an AttributeError exception if they do not handle the given attribute.

For example, if we have the statement image.colors, Python will look for a colors attribute and having failed to find it, will then call Image.__getattr__(image, "colors"). In this case the __getattr__() method handles a "colors" attribute name and returns a copy of the set of colors that the image is using.

The other attributes are immutable, so they are safe to return directly to the caller. We could have written separate elif statements for each one like this:

elif name == "background":
    return self.__background


But instead we have chosen a more compact approach. Since we know that under the hood all of an object’s nonspecial attributes are held in self.__dict__, we have chosen to access them directly. For private attributes (those whose name begins with two leading underscores), the name is mangled to have the form _className__attributeName, so we must account for this when retrieving the attribute’s value from the object’s private dictionary.

For the name mangling needed to look up private attributes and to provide the standard AttributeError error text, we need to know the name of the class we are in. (It may not be Image because the object might be an instance of an Image subclass.) Every object has a __class__ special attribute, so self.__class__ is always available inside methods and can safely be accessed by __getattr__() without risking unwanted recursion.

Note that there is a subtle difference in that using __getattr__() and self.__class__ provides access to the attribute in the instance’s class (which may be a subclass), but accessing the attribute directly uses the class the attribute is defined in.

Attribute Access Special Methods

One special method that we have not covered is __getattribute__(). Whereas the __getattr__() method is called last when looking for (nonspecial) attributes, the __getattribute__() method is called first for every attribute access. Although it can be useful or even essential in some cases to call __getattribute__(), reimplementing the __getattribute__() method can be tricky. Reimplementations must be very careful not to call themselves recursively—using super().__getattribute__() or object.__getattribute__() is often done in such cases. Also, since __getattribute__() is called for every attribute access, reimplementing it can easily end up degrading performance compared with direct attribute access or properties. None of the classes presented in this book reimplements __getattribute__().

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset