158 lines
4.7 KiB
Markdown
158 lines
4.7 KiB
Markdown
|
\[ [Index](index.md) | [Exercise 4.3](ex4_3.md) | [Exercise 5.1](ex5_1.md) \]
|
||
|
|
||
|
# Exercise 4.4
|
||
|
|
||
|
*Objectives:*
|
||
|
|
||
|
- Learn about customizing attribute access
|
||
|
- Delegation vs. inheritance
|
||
|
|
||
|
## (a) Slots vs. setattr
|
||
|
|
||
|
In previous exercises, `__slots__` was used to list the instance
|
||
|
attributes on a class. The primary purpose of slots is to optimize
|
||
|
the use of memory. A secondary effect is that it strictly limits the
|
||
|
allowed attributes to those listed. A downside of slots is that it
|
||
|
often interacts strangely with other parts of Python (for example,
|
||
|
classes using slots can't be used with multiple inheritance). For
|
||
|
that reason, you really shouldn't use slots except in special cases.
|
||
|
|
||
|
If you really wanted to limit the set of allowed attributes, an
|
||
|
alternate way to do it would be to define a `__setattr__()` method.
|
||
|
Try this experiment:
|
||
|
|
||
|
```python
|
||
|
>>> class Stock:
|
||
|
def __init__(self, name, shares, price):
|
||
|
self.name = name
|
||
|
self.shares = shares
|
||
|
self.price = price
|
||
|
def __setattr__(self, name, value):
|
||
|
if name not in { 'name', 'shares', 'price' }:
|
||
|
raise AttributeError('No attribute %s' % name)
|
||
|
super().__setattr__(name, value)
|
||
|
|
||
|
>>> s = Stock('GOOG', 100, 490.1)
|
||
|
>>> s.name
|
||
|
'GOOG'
|
||
|
>>> s.shares = 75
|
||
|
>>> s.share = 50
|
||
|
Traceback (most recent call last):
|
||
|
File "<stdin>", line 1, in <module>
|
||
|
File "<stdin>", line 8, in __setattr__
|
||
|
AttributeError: No attribute share
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
In this example, there are no slots, but the `__setattr__()` method still restricts
|
||
|
attributes to those in a predefined set. You'd probably need to
|
||
|
think about how this approach might interact with inheritance (e.g., if subclasses wanted
|
||
|
to add new attributes, they'd probably need to redefine `__setattr__()` to make it work).
|
||
|
|
||
|
## (b) Proxies
|
||
|
|
||
|
A proxy class is a class that wraps around an existing class and provides a similar interface.
|
||
|
Define the following class which makes a read-only layer around an existing object:
|
||
|
|
||
|
```python
|
||
|
>>> class Readonly:
|
||
|
def __init__(self, obj):
|
||
|
self.__dict__['_obj'] = obj
|
||
|
def __setattr__(self, name, value):
|
||
|
raise AttributeError("Can't set attribute")
|
||
|
def __getattr__(self, name):
|
||
|
return getattr(self._obj, name)
|
||
|
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
To use the class, you simply wrap it around an existing instance:
|
||
|
|
||
|
```python
|
||
|
>>> from stock import Stock
|
||
|
>>> s = Stock('GOOG', 100, 490.1)
|
||
|
>>> p = Readonly(s)
|
||
|
>>> p.name
|
||
|
'GOOG'
|
||
|
>>> p.shares
|
||
|
100
|
||
|
>>> p.cost
|
||
|
49010.0
|
||
|
>>> p.shares = 50
|
||
|
Traceback (most recent call last):
|
||
|
File "<stdin>", line 1, in <module>
|
||
|
File "<stdin>", line 8, in __setattr__
|
||
|
AttributeError: Can't set attribute
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
## (c) Delegation as an alternative to inheritance
|
||
|
|
||
|
Delegation is sometimes used as an alternative to inheritance. The idea is almost the
|
||
|
same as the proxy class you defined in part (b). Try defining the following class:
|
||
|
|
||
|
```python
|
||
|
>>> class Spam:
|
||
|
def a(self):
|
||
|
print('Spam.a')
|
||
|
def b(self):
|
||
|
print('Spam.b')
|
||
|
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
Now, make a class that wraps around it and redefines some of the methods:
|
||
|
|
||
|
```python
|
||
|
>>> class MySpam:
|
||
|
def __init__(self):
|
||
|
self._spam = Spam()
|
||
|
def a(self):
|
||
|
print('MySpam.a')
|
||
|
self._spam.a()
|
||
|
def c(self):
|
||
|
print('MySpam.c')
|
||
|
def __getattr__(self, name):
|
||
|
return getattr(self._spam, name)
|
||
|
|
||
|
>>> s = MySpam()
|
||
|
>>> s.a()
|
||
|
MySpam.a
|
||
|
Spam.a
|
||
|
>>> s.b()
|
||
|
Spam.b
|
||
|
>>> s.c()
|
||
|
MySpam.c
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
Carefully notice that the resulting class looks very similar to
|
||
|
inheritance. For example the `a()` method is doing something similar
|
||
|
to the `super()` call. The method `b()` is picked up via the
|
||
|
`__getattr__()` method which delegates to the internally held `Spam`
|
||
|
instance.
|
||
|
|
||
|
**Discussion**
|
||
|
|
||
|
The `__getattr__()` method is commonly defined on classes that act as
|
||
|
wrappers around other objects. However, you have to be aware that the
|
||
|
process of wrapping another object in this manner often introduces
|
||
|
other complexities. For example, the wrapper object might break
|
||
|
type-checking if any other part of the application is using the
|
||
|
`isinstance()` function.
|
||
|
|
||
|
Delegating methods through `__getattr__()` also doesn't work with special
|
||
|
methods such as `__getitem__()`, `__enter__()`, and so forth. If a class
|
||
|
makes extensive use of such methods, you'll have to provide similar functions
|
||
|
in your wrapper class.
|
||
|
|
||
|
|
||
|
\[ [Solution](soln4_4.md) | [Index](index.md) | [Exercise 4.3](ex4_3.md) | [Exercise 5.1](ex5_1.md) \]
|
||
|
|
||
|
----
|
||
|
`>>>` Advanced Python Mastery
|
||
|
`...` A course by [dabeaz](https://www.dabeaz.com)
|
||
|
`...` Copyright 2007-2023
|
||
|
|
||
|
. This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
|