explain this:
The __getattr__ method is invoked by the interpreter when attribute lookup fails.
In simple terms, given the expression my_obj.x, Python checks if the my_obj instance
has an attribute named x; if not, the search goes to the class (my_obj.__class__), and
then up the inheritance graph.2 If the x attribute is not found, then the __getattr__
method defined in the class of my_obj is called with self and the name of the
attribute as a string (e.g., 'x').
Example 12-8 lists our __getattr__ method. Essentially it checks whether the
attribute being sought is one of the letters xyzt and if so, returns the corresponding
vector component.
Example 12-8. Part of vector_v3.py: __getattr__ method added to the Vector class
__match_args__ = ('x', 'y', 'z', 't')
def __getattr__(self, name):
cls = type(self)
try:
pos = cls.__match_args__.index(name)
except ValueError:
pos = -1
if 0 <= pos < len(self._components):
return self._components[pos]
msg = f'{cls.__name__!r} object has no attribute {name!r}'
raise AttributeError(msg)
Set __match_args__ to allow positional pattern matching on the dynamic
attributes supported by __getattr__.3
Get the Vector class for later use.
408 | Chapter 12: Special Methods for SequencesTry to get the position of name in __match_args__.
.index(name) raises ValueError when name is not found; set pos to -1. (I’d
rather use a method like str.find here, but tuple doesn’t implement it.)
If the pos is within range of the available components, return the component.
If we get this far, raise AttributeError with a standard message text.
It’s not hard to implement __getattr__, but in this case it’s not enough. Consider
the bizarre interaction in Example 12-9.
Example 12-9. Inappropriate behavior: assigning to v.x raises no error, but introduces
an inconsistency
>>> v = Vector(range(5))
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])
>>> v.x
0.0
>>> v.x = 10
>>> v.x
10
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])
Access element v[0] as v.x.
Assign new value to v.x. This should raise an exception.
Reading v.x shows the new value, 10.
However, the vector components did not change.
Can you explain what is happening? In particular, why does v.x return 10 the second
time if that value is not in the vector components array? If you don’t know right off
the bat, study the explanation of __getattr__ given right before Example 12-8. It’s a
bit subtle, but a very important foundation to understand a lot of what comes later in
the book.
After you’ve given it some thought, proceed and we’ll explain exactly what happened.
The inconsistency in Example 12-9 was introduced because of the way __getattr__
works: Python only calls that method as a fallback, when the object does not have the
named attribute. However, after we assign v.x = 10, the v object now has an x
attribute, so __getattr__ will no longer be called to retrieve v.x: the interpreter will
Vector Take #3: Dynamic Attribute Access | 409just return the value 10 that is bound to v.x. On the other hand, our implementation
of __getattr__ pays no attention to instance attributes other than self._compo
nents, from where it retrieves the values of the “virtual attributes” listed in
__match_args__.
We need to customize the logic for setting attributes in our Vector class in order to
avoid this inconsistency.
Recall that in the latest Vector2d examples from Chapter 11, trying to assign to the .x
or .y instance attributes raised AttributeError. In Vector, we want the same excep‐
tion with any attempt at assigning to all single-letter lowercase attribute names, just
to avoid confusion. To do that, we’ll implement __setattr__, as listed in
Example 12-10.
Example 12-10. Part of vector_v3.py: __setattr__ method in the Vector class
def __setattr__(self, name, value):
cls = type(self)
if len(name) == 1:
if name in cls.__match_args__:
error = 'readonly attribute {attr_name!r}'
elif name.islower():
error = "can't set attributes 'a' to 'z' in {cls_name!r}"
else:
error = ''
if error:
msg = error.format(cls_name=cls.__name__, attr_name=name)
raise AttributeError(msg)
super().__setattr__(name, value)
Special handling for single-character attribute names.
If name is one of __match_args__, set specific error message.
If name is lowercase, set error message about all single-letter names.
Otherwise, set blank error message.
If there is a nonblank error message, raise AttributeError.
Default case: call __setattr__ on superclass for standard behavior.
410 | Chapter 12: Special Methods for SequencesThe super() function provides a way to access methods of super‐
classes dynamically, a necessity in a dynamic language supporting
multiple inheritance like Python. It’s used to delegate some task
from a method in a subclass to a suitable method in a superclass, as
seen in Example 12-10. There is more about super in “Multiple
Inheritance and Method Resolution Order” on page 494.
While choosing the error message to display with AttributeError, my first check
was the behavior of the built-in complex type, because they are immutable and have a
pair of data attributes, real and imag. Trying to change either of those in a complex
instance raises AttributeError with the message "can't set attribute". On the
other hand, trying to set a read-only attribute protected by a property as we did in “A
Hashable Vector2d” on page 374 produces the message "read-only attribute". I
drew inspiration from both wordings to set the error string in __setitem__, but was
more explicit about the forbidden attributes.
Note that we are not disallowing setting all attributes, only single-letter, lowercase
ones, to avoid confusion with the supported read-only attributes x, y, z, and t.
Knowing that declaring __slots__ at the class level prevents set‐
ting new instance attributes, it’s tempting to use that feature
instead of implementing __setattr__ as we did. However, because
of all the caveats discussed in “Summarizing the Issues with
__slots__” on page 388, using __slots__ just to prevent instance
attribute creation is not recommended. __slots__ should be used
only to save memory, and only if that is a real issue.
Even without supporting writing to the Vector components, here is an important
takeaway from this example: very often when you implement __getattr__, you need
to code __setattr__ as well, to avoid inconsistent behavior in your objects.
If we wanted to allow changing components, we could implement __setitem__ to
enable v[0] = 1.1 and/or __setattr__ to make v.x = 1.1 work. But Vector will
remain immutable because we want to make it hashable in the coming section