confusion with decorators

Steven D'Aprano steve+comp.lang.python at pearwood.info
Fri Feb 1 00:16:54 CET 2013


Jason Swails wrote:

> On Thu, Jan 31, 2013 at 12:46 AM, Steven D'Aprano <
> steve+comp.lang.python at pearwood.info> wrote: 

>> Well, that surely isn't going to work, because it always decorates the
>> same function, the global "fcn".
> 
> I don't think this is right.  

It certainly isn't. Sorry for the noise.


[...]
> Again, my example code is over-simplified.  A brief description of my
> class
> is a list of 'patch' (diff) files with various attributes.  If I want
> information from any of those files, I instantiate a Patch instance (and
> cache it for later use if desired) and return any of the information I
> want from that patch (like when it was created, who created it, what files
> will be altered in the patch, etc.).
> 
> But a lot of these patches are stored online, so I wanted a new class (a
> RemotePatchList) to handle lists of patches in an online repository.  I
> can do many of the things with an online patch that I can with one stored
> locally, but not everything, hence my desire to squash the methods I don't
> want to support.


Normally, subclasses should extend functionality, not take it away. A
fundamental principle of OO design is that anywhere you could sensibly
allow an instance, should also be able to use a subclass.

So if you have a Patch class, and a RemotePatch subclass, then everything
that a Patch can do, a RemotePatch can do too, because RemotePatch
instances *are also* instances of Patch.

But the rule doesn't go in reverse: you can't necessarily use a Patch
instance where you were using a RemotePatch. Subclasses are allowed to do
*more*, but they shouldn't do *less*.

On the other hand, if you have a Patch class, and a RemotePatchList class,
inheritance does not seem to be the right relationship here. A
RemotePatchList does not seem to be a kind of Patch, but a kind of list.


> I'd imagine a much more sensible approach is to generate a base class that
> implements all methods common to both and simply raises an exception in
> those methods that aren't.  I agree it doesn't make much sense to inherit
> from an object that has MORE functionality than you want.

If a method is not common to both, it doesn't belong in the base class. The
base should only include common methods.

In fact, I'm usually rather suspicious of base classes that don't ever get
used except as a base for subclassing. I'm not saying it's wrong, but it
could be excessive abstraction. Abstraction is good, but you can have too
much of a good thing. If the base class is not used, consider a flatter
hierarchy:

    class Patch:  ...
    class RemotePatch(Patch):  ...


rather than:

    class PatchBase:  ...
    class Patch(PatchBase):  ...
    class RemotePatch(Patch):  ...

although this is okay:

    class PatchBase:  ...
    class Patch(PatchBase):  ...
    class RemotePatch(PatchBase):  ...


> However, my desire to use decorators was not to disable methods in one
> class vs. another.  The _protector_decorator (a name borrowed from my
> actual code), is designed to wrap a function call inside a try/except, to
> account for specific exceptions I might raise inside.

Ah, your example looked like you were trying to implement some sort of
access control, where some methods were flagged as "protected" to prevent
subclasses from using them. Hence my quip about Java. What you describe
here makes more sense.


> One of my classes 
> deals with local file objects, and the other deals with remote file
> objects
> via urllib.  Naturally, the latter has other exceptions that can be
> raised,
> like HTTPError and the like.  So my desire was to override the decorator
> to handle more types of exceptions, but leave the underlying methods
> intact without duplicating them.

    >>> decorated(3)
    4

One way to do that is to keep a list of exceptions to catch:


class Patch:
    catch_these = [SpamException, HamException]
    def method(self, arg):
        try:
            do_this()
        except self.catch_these:
            do_that()

The subclass can then extend or replace that list:

class RemotePatch(Patch):
    catch_these = Patch.catch_these + [EggsException, CheeseException]




>> import functools
> 
> I need to support Python 2.4, and the docs suggest this is 2.5+.  Too bad,
> too, since functools appears pretty useful.

functools.wraps is pretty simple. You can use this as an equivalent:

# `functools.wraps` was added in Python 2.5.
def wraps(func_to_wrap):
    """Return a decorator that wraps its argument.

    This is a reimplementation of functools.wraps() which copies the name,
    module, docstring and attributes of the base function to the decorated
    function. wraps() is available in the standard library from Python 2.5.

    >>> def undecorated(x):
    ...     '''This is a doc string.'''
    ...     return x+1
    ...
    >>> undecorated.__module__ = 'parrot'
    >>> undecorated.attr = 'something'
    >>> @wraps(undecorated)
    ... def decorated(x):
    ...     return undecorated(x)
    ...
    >>> decorated(3)
    4
    >>> decorated.__doc__
    'This is a doc string.'
    >>> decorated.attr
    'something'
    >>> decorated.__module__
    'parrot'
    >>> decorated.__name__
    'undecorated'

    """
    def decorator(func):
        def f(*args, **kwargs):
            return func(*args, **kwargs)
        f.__doc__ = func_to_wrap.__doc__
        try:
            f.__name__ = func_to_wrap.__name__
        except Exception:
            # Older versions of Python (2.3 and older perhaps?)
            # don't allow assigning to function __name__.
            pass
        f.__module__ = func_to_wrap.__module__
        if hasattr(func_to_wrap, '__dict__'):
            f.__dict__.update(func_to_wrap.__dict__)
        return f
    return decorator


The doctest passes for Python 2.4.



-- 
Steven




More information about the Python-list mailing list