Decorators not worth the effort

Dieter Maurer dieter at handshake.de
Sat Sep 15 02:31:30 EDT 2012


Dwight Hutto wrote at 2012-9-14 23:42 -0400:
> ...
>Reduce redundancy, is argumentative.
>
>To me, a decorator, is no more than a logging function. Correct me if
>I'm wrong.

Well, it depends on how you are using decorators and how complex
your decorators are. If what you are using as decorating function
it really trivial, as trivial as "@<decoratorname>", then you
do not gain much.

But your decorator functions need not be trivial.
An example: in a recent project,
I have implemented a SOAP webservice where most services depend
on a valid session and must return specified fields even when
(as in the case of an error) there is no senseful value.
Instead of putting into each of those function implementations
the check "do I have a valid session?" and at the end
"add required fields not specified", I opted for the following
decorator:

def valid_session(*fields):
! fields = ("errorcode",) + fields
  @decorator
  def valid_session(f, self, sessionkey, *args, **kw):
!   s = get_session(sessionkey)
!   if not s.get("authenticated", False):
!     rd = {"errorcode": u"1000"}
!   else:
!     rd = f(self, sessionkey, *args, **kw)
!   return tuple(rd.get(field, DEFAULTS.get(field, '')) for field in fields)
  return valid_session

The lines starting with "!" represent the logic encapsulated by the
decorator -- the logic, I would have to copy into each function implementation
without it.

I then use it this way:

  @valid_session()
  def logout(self, sessionkey):
    s = get_session(sessionkey)
    s["authenticated"] = False
    return {}

  @valid_session("amountavail")
  def getStock(self, sessionkey, customer, item, amount):
    info = self._get_article(item)
    return {u"amountavail":info["deliverability"] and u"0" or u"1"}

  @valid_session("item", "shortdescription", "pe", "me", "min", "price", "vpe", "stock", "linkpicture", "linkdetail", "linklist", "description", "tax")
  def fetchDetail(self, sessionkey, customer, item):
    return self._get_article(item)
  ...

I hope you can see that at least in this example, the use of the decorator
reduces redundancy and highly improves readability -- because
boilerplate code (check valid session, add default values for unspecified
fields) is not copied over and over again but isolated in a single place.


The example uses a second decorator ("@decorator") --
in the decorator definition itself. This decorator comes from the
"decorator" module, a module facilitating the definition of signature
preserving decorators (important in my context): such a decorator
ensures that the decoration result has the same parameters as the
decorated function. To achieve this, complex Python implementation
details and Python's introspection must be used. And I am very
happy that I do not have to reproduce this logic in my decorator
definitions but just say "@decorator" :-)


Example 3: In another project, I had to implement a webservice
where most of the functions should return "json" serialized data
structures. As I like decorators, I chose a "@json" decorator.
Its definition looks like this:

@decorator
def json(f, self, *args, **kw):
  r = f(self, *args, **kw)
  self.request.response.setHeader(
    'content-type',
    # "application/json" made problems with the firewall,
    #  try "text/json" instead
    #'application/json; charset=utf-8'
    'text/json; charset=utf-8'
    )
  return udumps(r)

It calls the decorated function, then adds the correct "content-type"
header and finally returns the "json" serialized return value.

The webservice function definitions then look like:

    @json
    def f1(self, ....):
       ....

    @json
    def f2(self, ...):
       ....

The function implementions can concentrate on their primary task.
The "json" decorator" tells that the result is (by magic specified
elsewhere) turned into a "json" serialized value.

This example demontrates the improved maintainability (caused by
the redundancy reduction): the "json rpc" specification stipulates
the use of the "application/json" content type. Correspondingly,
I used this content-type header initially. However, many enterprise
firewalls try to protect against viruses by banning "application/*"
responses -- and in those environments, my initial webservice
implementation did not work. Thus, I changed the content type
to "text/json". Thanks to the decorator encapsulation of the
"json result logic", I could make my change at a single place -- not littered
all over the webservice implementation.


And a final example: Sometimes you are interested to cache (expensive)
function results. Caching involves non-trivial logic (determine the cache,
determine the key, check whether the cache contains a value for the key;
if not, call the function, cache the result). The package "plone.memoize"
defines a set of decorators (for different caching policies) which
which caching can be as easy as:

      @memoize
      def f(....):
          ....

The complete caching logic is encapsulated in the tiny "@memoize" prefix.
It tells: calls to this function are cached. The function implementation
can concentrate on its primary task and there is no need to obscure
the implementation by the orthogonal aspect of caching.


I hope I could convince you that while you may not have a serious need
for decorators, there are cases where they can be really useful.

Should I have not yet succeeded, I suggest you read some overview
on aspect oriented programming. I am sure, you will find there
losts of further examples why it is a good idea to separate
general purpose aspects (logging, monitoring, persistency, resource
management, caching, serialization, ...) from the primary task of
a function. Decorators provide syntactic sugur to facilitate this
separation in Python.


--
Dieter



More information about the Python-list mailing list