Hi Clark,
On Fri, Jan 14, 2005 at 12:41:32PM -0500, Clark C. Evans wrote:
> Imagine enhancing the stack-trace with additional information about
> what adaptations were made;
>
> Traceback (most recent call last):
> File "xxx", line 1, in foo
> Adapting x to File
> File "yyy", line 384, in bar
> Adapting x to FileName
> etc.
More thoughts should be devoted to this, because it would be very precious.
There should also be a way to know why a given call to adapt() returned an
unexpected object even if it didn't crash. Given the nature of the problem,
it seems not only "nice" but essential to have a good way to debug it.
> How can we express your thoughts so that they fit into a narrative
> describing how adapt() should and should not be used?
I'm attaching a longer, hopefully easier reformulation...
Armin
A view on adaptation
====================
Adaptation is a tool to help exchange data between two pieces of code; a very
powerful tool, even. But it is easy to misunderstand its aim, and unlike other
features of a programming language, misusing adaptation will quickly lead into
intricate debugging nightmares. Here is the point of view on adaptation which
I defend, and which I believe should be kept in mind.
Let's take an example. You want to call a function in the Python standard
library to do something interesting, like pickling (saving) a number of
instances to a file with the ``pickle`` module. You might remember that there
is a function ``pickle.dump(obj, file)``, which saves the object ``obj`` to the
file ``file``, and another function ``pickle.load(file)`` which reads back the
object from ``file``. (Adaptation doesn't help you to figure this out; you
have to be at least a bit familiar with the standard library to know that this
feature exists.)
Let's take the example of ``pickle.load(file)``. Even if you remember about
it, you might still have to look up the documentation if you don't remember
exactly what kind of object ``file`` is supposed to be. Is it an open file
object, or a file name? All you know is that ``file`` is meant to somehow
"be", or "stand for", the file. Now there are at least two commonly used ways
to "stand for" a file: the file path as a string, or the file object directly.
Actually, it might even not be a file at all, but just a string containing the
already-loaded binary data. This gives a third alternative.
The point here is that the person who wrote the ``pickle.load(x)`` function
also knew that the argument was supposed to "stand for" a source of binary data
to read from, and he had to make a choice for one of the three common
representations: file path, file object, or raw data in a string. The "source
of binary data" is what both the author of the function and you would easily
agree on; the formal choice of representation is more arbitrary. This is where
adaptation is supposed to help. With properly setup adaptation, you can pass
to ``pickle.load()`` either a file name or a file object, or possibly anything
else that "reasonably stands for" an input file, and it will just work.
But to understand it more fully, we need to look a bit closer. Imagine
yourself as the author of functions like ``pickle.load()`` and
``pickle.dump()``. You decide if you want to use adaptation or not.
Adaptation should be used in this case, and ONLY in this kind of case: there is
some generally agreed concept on what a particular object -- typically an
argument of function -- should represent, but not on precisely HOW it should
represent it. If your function expects a "place to write the data to", it can
typically be an open file or just a file name; in this case, the function would
be defined like this::
def dump_data_into(target):
file = adapt(target, TargetAsFile)
file.write('hello')
with ``TargetAsFile`` being suitably defined -- i.e. having a correct
``__adapt__()`` special method -- so that the adaptation will accept either a
file or a string, and in the latter case open the named file for writing.
Surely, you think that ``TargetAsFile`` is a strange name for an interface if
you think about adaptation in term of interfaces. Well, for the purpose of
this argument, don't. Forget about interfaces. This special object
``TargetAsFile`` means not one but two things at once: that the input argument
``target`` represents the place into which data should be written; and that the
result ``file`` of the adaptation, as used within function itself, must be more
precisely a file object.
This two-level distinction is important to keep in mind, specially when
adapting built-in objects like strings and files. For example, the adaptation
that would be used in ``pickle.load(source)`` is more difficult to get right,
because there are two common ways that a string object can stand for a source
of data: either as the name of a file, or as raw binary data. It is not
possible to distinguish between these two differents uses of ``str``
automatically. In other words, strings are very versatile and low-level
objects which can have various meanings in various contexts, and sometimes
these meanings even conflict in the same context! More concretely, it is not
possible to use adaptation to write a function ``pickle.load(source)`` which
accepts either a file, a file name, or a raw binary string. You have to make a
choice. For symmetry with the case of ``TargetAsFile``, a ``SourceAsFile``
would probably interpret a string as a file name, and the caller still has to
explicitely turn a raw string into a file-like object -- by wrapping it in a
``StringIO()``.
However, it would be possible to extend our adapters to accept URLs, say,
because it's possible to distinguish between a local file name and an URL.
Similarily, various other object types could unambiguously refer to,
respectively, a "source" or "target" of data.
The essential point is: the criterion to keep in mind for knowing when it is
reasonable or not to add new adaptation paths is whether the object you are
adapting "clearly stands" for the **high-level concept** that you are adapting
to, and **not** for whatever resulting type or interface the adapted object
should have. It **makes no sense** to adapt a string to a file or a file-like
object. *Never define an adapter from the string type to the file type!!* A
string and a file are two low-level concepts that mean different things. It
only makes sense to adapt a string to a "source of data" which is then
represented as a file.
This subtle distinction is essential when adapting built-in types. In large
frameworks, it is perhaps more common to adapt to interfaces or between classes
specific to your framework. These interfaces and classes merge both roles: one
class is a concrete objects in the Python sense -- a type -- and a single
embodied concept. In this case, the difference between a concrete instance and
the concept it stands for is not so important. This is why we can often think
about adaptation as creating an adapter object on top of an instance, to
provide a different interface for the object. If you adapt an instance to an
interface ``I`` you really mean that there is a common concept behind the
instance and ``I``, and you want to change from the representation given by the
instance to the one given by ``I``.
I believe it is useful to keep in mind that adaptation is really about
converting between different concrete representations ("str", "file") of a
common abstract concept ("source of data"). You have at least to realize which
abstract concept you want to adapt representations of, before you define your
own adapters. If you do, then properties like the transitivity of adaptation
(i.e. automatically finding longer adaptation paths A -> B -> C when asked to
adapt from A to C) become desirable, because the intermediate steps are merely
changes in representation for the same abstract concept ("it's the same source
of data all along"). If you don't, then transitivity becomes the Source Of All
Nightmares :-)
_______________________________________________
Python-Dev mailing list
[email protected]
http://mail.python.org/mailman/listinfo/python-dev
Unsubscribe:
http://mail.python.org/mailman/options/python-dev/archive%40mail-archive.com