Noam Raphael said:
However, I'm not sure if this solves my practical problem - testing
whether all abstract methods were implemented. I think that usually, you
can't write a test which checks whether an abstract method did what it
should have, since different implementations do different things. I
Still, there must be some _protocol_ about what those abstract methods
are all about... otherwise you _are_ just talking about an *interface*,
the way I use the terms (I didn't invent that usage, I just find it very
useful in practice). An interface is "just syntax" -- method names and
signatures; a protocol adds semantics and pragmatics (the distinction
between the last two may only be meaningful to somebody with a
background in linguistics, and it's not important here anyway).
If some method is a hook which may do anything including nothing then
it's most often more useful not to make it abstract (or absent) in the
base class, but rather to define it there as a no-op. This way, you
reduce the boilerplate in all subclasses: they only override the methods
they NEED to override. Besides no-op, this could also apply to other
semantics that were "a very likely possibility" -- but it's unusual for
any given semantics to be "very likely", except "first, do no harm";-)
When you design a protocol, _make it testable_. Say that you start with
the idea of the protocol having two methods:
def frambozzle(self):
''' must make the instance frambozzled '''
def unframbozzle(self):
''' must make the instance not-frambozzled '''
This protocol is not complete. Add a third method:
def isframbozzled(self):
''' return a true value iff the instance is frambozzled '''
NOW you have a testable protocol! Sorry for the toy-ness of the
example, but I don't want to post for hundreds of lines. Beyond the
direct advantages of being able to write much better tests, experience
teaches that a testable protocol tends to have extra resilience and
usefulness for perfectly reasonable uses that the protocol designer
might not have anticipated. E.g., with the testable protocol one can
write
def toggle_frambozzling(some_instance):
if some_instance.isframbozzled():
some_instance.unframbozzle()
else:
some_instance.frambozzle()
Note that we're only using semantics here, aka abstract semantics, not
pragmatics (just to illuminate the semi-relevant distinction I was using
above). Abstract semantics are those you can fully capture by axioms
related to methods that are part of the protocol itself, without for
example needing to refer to the external world, or other objects. You
get pragmatics when such references are there, e.g., informal specs of
the kind "the shipping costs of a frambozzled instance are expected to
not exceed that of an unframbozzled instance, and normally to be below;
on the other hand, frambozzled instances need only be marshalable on a
single platform and version of Python and of this framework, while
unframbozzled instances must ensure marshaling cross-platform on any
version of Python >= 2.3.4 and version of this framework >= 0.8.2". In
practice, only projects with relatively high formality tend to write
things down to this level, of course, but generally there is some less
formal pragmatic understanding of what it's all about.
For example, in a recent thread about hashing &c I pointed out that
adding a method such as "def __hash__(self): return 23" can ensure
semantically correct hashing of any class; OTOH, it DOES badly violate
pragmatics (and produces horrid performance!-), because hash values are
MEANT to "differentiate" among different instances (they don't _hafta_,
but they're expected to do SOME level of reasonable effort!-).
don't even know how you can test whether an abstract method was
implemented - should you run it and see if it raises a
NotImplementedError?
Makes sense to me, yes. assertRaises seems good;-).
If you're going to use your abstract base class for ANY purpose, you'd
better have SOME idea of the signature with which you can call those
methods which its subclasses must implement! Otherwise, it's all for
naught: you get zero polymorphism, period. Say that subclass A has
def whoa(self): ...
while subclass B has
def whoa(self, andmore): ...
then you can NEVER call whoa without knowing which subclass you have an
instance of -- no polymorphism! It's fine if a subclass wants to have a
WIDER signature (add OPTIONAL args to those specified in the base class,
even a *any if you wish), but there must be SOME commonality. Well
then, call according to that commonality, just like any USE of you
abstract base class will do.
This points out that, if you wish to do checks at 'class' statement time
which determine whether a class is instantiable, the signature of the
methods should be part of those checks. Fortunately, module inspect
makes it reasonably easy to get and check methods' signatures.
And even if you find a way
to test whether a method was implemented, I still think that the
duplication of code isn't very nice - you have both in your class
definition and in your test suite a section which says only "method
so-and-so should be implemented."
Then it may be best to ONLY have the test suite -- don't put the method
in the ABC at all. If you WANT the method in the ABC, for documentation
purposes, well then, that's not duplication of code, it's documentation,
which IS fine (just like it's quite OK to have some of the same info in
a Tutorial document, in a Reference one, AND in a class's docstring!).
If you don't want to have the duplication your unit tests become easier:
you just getattr from the class (don't even have to bother instantiating
it, ain't it great!), and check the result with inspect.
I think that making abstract methods a different object really makes
sense - they are just something else. Functions (and methods) define
what the computer should do. Abstract methods define what the
*programmer* should do.
Unit tests define what the programmer should do, to a large extent, much
better than other approaches do (until it comes to the integration of
components, but then, abstract methods ain't much use there either;
tests of other ilks may be preferable;-).
ITRW (in the real world) you MAY find yourself in situations where you
know unit-tests WILL be skimped on. Then you're in crisis mode from the
word go, and taking what little precautions you can (including custom
metaclasses that in practice won't be bypassed and can enforce a tiny
sliver of sanity in what will inevitably remain a crazy environment) may
be among the band-aids you can try. Besides, part of your investment in
building that infrastructure does get repaid in getting some errors
diagnosed earlier (though not as thoroughly and early as unit tests
might, but, we're starting from the sad RW assumption that _some_ deuced
coders won't do them, and we're just trying to reduce "totally dead" to
"mostly dead"...;-).
Alex