Saturday 26 December 2015

Weekly Python #11 - Duck Typing

Duck Typing

If it walks like a duck, and quacks like a duck ....

If you are new to Python you may well have heard of Duck Typing but you might not be too sure of what it means.  In this article we will explore Duck Typing with one of the most powerful concepts in Python : Iterators, a very powerful example of duck typing.

An Iterator is any object which implements functionality to allow the user to move through a collection of data one item at a time. Iterator behaviours are both simple to use and implement. Many parts of the python standard library implement iterators : strings, lists, dictionaries, tuples, generators, and even files.

Because every iterator shares the same behaviour, then your application can write one function which will work with every single iterator - no matter what type it is, and your application can implement it's own iterators, and the same functions which work on standard library iterators, will work the same way on your iterator too.

That is the theory, lets see something in practice :

>>> def dup( iterator ):
...     "A Generator which will duplicate each item in the given iterator"
...     for item in iterator:
...         yield item
...         yield item
The function above will take any iterator, and will return a generator which yields each item in the original duplicated. This function relies on Duck Typing: it needs whatever is passed in as the iterator argument to be implemented as an Iterator: It will work on any standard iterator (Note that Iterables (such as Lists, strings, tuples) are also Iterators, where as generator expressions, files are only Iterators).
>>> # A list of numbers
>>> [i for i in dup([0,1,2,3,4,5,6,7,8,9])]
[0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9]
>>>
>>> # A String
>>> [i for i in dup("abcdef")]
['a', 'a', 'b', 'b', 'c', 'c', 'd', 'd', 'e', 'e', 'f', 'f']
>>>
>>> # A Tuple
>>> [i for i in dup( (0,1,2,3,4,5,6) )]
[0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6]
>>>
>>> # A Generator expression
>>> [i for i in dup( (i for i in range(0,7) )]
[0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6]
This function would also work if you passed in an open file object, and would generate a duplicate for every line in the file, and with a dictionary it will generate a duplicated sequence of keys.

With a language such as C or C++, you will struggle to implement such a generic function, which would operate on strings, vectors, arrays and files, since C is a statically typed language which means you need to declare ahead the arguments that your function, and strings, vectors, arrays are all different types.

With Duck typing, as we have seen it isn't the type that matters, it is the behaviour (in this case data which behaves like an Iterable). In general you don't even worry about testing that the arguments are the correct/expected type (let the lower levels worry about raising exceptions and cascading them upwards); if your code is using hasattr or isinstance to check the type or behaviour exists before you use it, then you aren't utilizing the full power of Duck-Typing.

 Custom Errors

You might consider that if you need to raise a custom error rather than use the exception generated by the  lower level code, then the right thing to do is to check that the attributes have the right behaviour before you use them :

>>> def dup( iterator ):
...     "A Generator which will duplicate each item in the given iterator"
...      if not hasattr(iterator,"__iter__"):
...           raise TypeError("You can only duplicate items from an iterator")
...
...      for item in iterator:
...          yield item
...          yield item

Although this will work, it is not the best use of the language, and not very Pythonic (i.e. best practice for Python code). A Better solution is to use the power of Duck Typing, and exception handling.
>>> def dup( iterator ):
...     "A Generator which will duplicate each item in the given iterator"
...      try:
...          for item in iterator:
...              yield item
...              yield item
...      except TypeError:
...          raise TypeError("You can only duplicate items from an iterator")
Not only is this version best practice (if you really insist on a custom Exception message), but it will be quicker as it wont need to execute the hasattr test each time the function is called.

using isinstance or hasattr

There is one case where using isinstance or hasattr is completely valid thing to do (and even recommended) and that is when your code is taking data as an argument into a class __init__ which needs to be an expected type, but your code doesn't use that data until much later (as it is always better to report errors as soon as possible).

No comments:

Post a Comment