\[ [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  

![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
