Initial commit
This commit is contained in:
157
Exercises/ex4_4.md
Normal file
157
Exercises/ex4_4.md
Normal file
@@ -0,0 +1,157 @@
|
||||
\[ [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/)
|
||||
Reference in New Issue
Block a user