[Python-3000] interfaces

George Sakkis gsakkis at rutgers.edu
Sun Nov 19 18:44:15 CET 2006


On 11/19/06, Antoine Pitrou <solipsis at pitrou.net> wrote:

> I had the problem recently when I wrote a decorator which took an
> optional number as an argument. The decorator declaration goes like
> this:
>
> def deferred(timeout=None):
>     def decorate(func):
>         blah blah...
>     return decorate
>
> It can be used like this:
>
> @deferred()
> def test_case():
>     ...
>
> The problem was if you forgot the parentheses altogether:
>
> @deferred
> def test_case():
>     ...
>
> In that case, no exception was raised, but the test was silently
> ignored. Also, "@deferred" doesn't strike at first sight like something
> is missing (parameterless decorators do exist).
>
> So to know if "deferred" was correctly used, I had to devise a test to
> know if "timeout" is a number (without mandating that it is a e.g. float
> rather than an int). I ended up with this:
>     try:
>         timeout is None or timeout + 0
>     except TypeError:
>         raise TypeError("'timeout' argument must be a number or None")

I understand this is not the point you're trying to make, but in such
cases I usually prefer to make @decorator be equivalent to
@decorator() by something like:

def deferred(timeout=None):
    if callable(timeout):
        f = timeout; timeout = None
    else:
        f = None
    def decorate(func):
        blah blah...
    return decorate if f is None else decorate(f)

Of course this doesn't work if it's valid for the timeout parameter to
be a callable, but usually, like in your example, it's not.

> Instead of testing "timeout" against an arbitrary arithmetic operation,
> it would be more robust to test it against an interface (say "Number").
> I think that's the kind of use cases which makes people oppose the idea
> of removing callable(). It feels more robust to test against an
> interface than against a specific trait of that interface.

The counter-argument is that instead of picking an arbitrary trait of
the interface to check, make sure your unit tests fail if the passed
value is not appropriate. The real problem you should look into is why
"in that case, no exception was raised, but the test was silently
ignored". I don't know how 'timeout' is used in your example, but from
a similar bad experience, I'd guess all you are doing with it is
compare it with a number ("if timeout < 3") and comparisons between
instances of different types don't raise an exception.

That's perhaps one of the top gotchas in Python today; thankfully it
will change in 3.0. Until then, I can't see how one can avoid an
explicit check, either by testing with isinstance() or trying to call
an arbitrary method of the expected protocol. Having said that both
are necessary evils, I'd go with the isinstance check as the lesser
evil. Checking for an arbitrary method is not only an ugly kludge; it
is both unnecessary (if the tested method is not used in the actual
code) and error-prone (e.g. for instances of a type that just happens
to define __add__ so that x + 0 doesn't raise an exception, although x
is an unappropriate argument in the specific context).

George


More information about the Python-3000 mailing list