Wrapping a list and delegating

We'll look at how we might wrap one of Python's built-in container classes. Wrapping an existing class means that some methods will have to be delegated to the underlying container.

As there are a large number of methods in any of the built-in collections, wrapping a collection may require a fair amount of code. When it comes to creating persistent classes, wrapping has advantages over extending. That's the subject of Chapter 10, Serializing and Saving - JSON, YAML, Pickle, CSV, and XML. In some cases, we'll want to expose the internal collection to save writing a large number of sequence methods that delegate to an internal list.

A common restriction that applies to statistics data classes is that they need to be insert only. We'll be disabling a number of method functions. This is the kind of dramatic change in the class features that suggests using a wrapper class instead of an extension.

We can design a class that supports only append and __getitem__, for example. It would wrap a list class. The following code can be used to accumulate data from simulations:

class StatsList3:
def __init__(self) -> None:
self._list: List[float] = list()
self.sum0 = 0 # len(self), sometimes called "N"
self.sum1 = 0. # sum(self)
self.sum2 = 0. # sum(x**2 for x in self)

def append(self, value: float) -> None:
self._list.append(value)
self.sum0 += 1
self.sum1 += value
self.sum2 += value * value

def __getitem__(self, index: int) -> float:
return self._list.__getitem__(index)

@property
def mean(self) -> float:
return self.sum1 / self.sum0

@property
def stdev(self) -> float:
return math.sqrt(
self.sum0*self.sum2 - self.sum1*self.sum1
) / self.sum0

This class has an internal _list object that is the underlying list. We've provided an explicit type hint to show the object is expected to be List[float]. The list is initially empty. As we've only defined append() as a way to update the list, we can maintain the various sums easily. We need to be careful to delegate the work to the superclass to be sure that the list is actually updated before our subclass processes the argument value.

We can directly delegate __getitem__() to the internal list object without examining the arguments or the results.

We can use this class as follows:

>>> sl3 = StatsList3() 
>>> for data in 2, 4, 4, 4, 5, 5, 7, 9: 
...     sl3.append(data) 
... 
>>> sl3.mean 
5.0 
>>> sl3.stdev    
2.0 

We created an empty list and appended items to the list. As we maintain the sums as items are appended, we can compute the mean and standard deviation extremely quickly.

We did not provide a definition of __iter__(). Instances of this class will, however, be iterable in spite of this omission.

Because we've defined __getitem__(), several things now work. Not only can we get items, but it also turns out that there will be a default implementation that allows us to iterate through the sequence of values.

Here's an example:

>>> sl3[0] 
2 
>>> for x in sl3: 
...     print(x) 
... 
2 
4 
4 
4 
5 
5 
7 
9 

The preceding output shows us that a minimal wrapper around a collection is often enough to satisfy many use cases.

Note that we didn't, for example, make the list sizeable. If we attempt to get the size, it will raise an exception, as shown in the following snippet:

>>> len(sl3) 
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module> 
TypeError: object of type 'StatsList3' has no len() 

We might want to add a __len__() method that delegates the real work to the internal _list object. We might also want to set __hash__ to None, which would be prudent as this is a mutable object.

We might want to define __contains__() and delegate this feature to the internal _list too. This will create a minimalist container that offers the low-level feature set of a container.

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

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