[Python-Dev] proposal for interfaces
Esteban U.C.. Castro
esteban@ccpgames.com
Sat, 28 Sep 2002 08:26:38 -0000
Hi, I have just joined python-dev and I saw your very interesting
proposal=20
for implementing intefaces.
> I have an idea for an interface mechnism for Python, and I'd like to
see if
> anyone likes it before writing an actual PEP. [...]
I like it a lot! Anyway, if it can be implemented in python as is, what
is=20
the point of the PEP? Making the 'interface' root class and/or
InterfaceError=20
builtins, maybe?
I have some comments which I thought I would bounce. I'll organize these
attending to the activities they relate to. Don't hesitate to tell me if
I'm=20
sayig something stupid. :)
Define an interface
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
In your example, it seems that
class Foo(interface):
def default_foo(self, a, b):
"Docstring for foo method."
print "Defaults can be handy."
[I added the a, b arguments to illustrate one point below]
Does the following:=20
- Defines the requirement that all objects that implement Foo have a
foo=20
function
- Defines that foo should accept arguments of the form (a, b) maybe?
- Sets a doc string for the foo method.
- Sets a default implementation for the method.
=20
Some questions on this:
- Can an interface only define method names (and maybe argument
formats)?=20
I think it would be handy to let it expose attributes.
- Is method names (and maybe format of arguments) the only thing you
can=20
'promise' in the interface? In other words, is that the only type of=20
guarantee that code that works against the interface can get? I think
a __check__(self, obj) special method in interfaces would be a simple
way to boost their flexibility.
- For the uses you have given to the prefix_methodname notation so far,
I don't think it's really needed. Isn't the following sufficient?
=20
class Foo(interface):
=20
def foo(self, a, b):
"foo docstring"
# nothing here; no default definition
=20
def bar(self, a, b):
pass # no docstring, _empty definition_
=20
This has the side effect that a method with no default definition and no
doc string is a SyntaxError. Is this too bad?=20
- It would maybe be hard to figure out what such a method is supposed to
do, so you _should_ provide a docstring.=20
- If you're in a hurry, an empty docstring will do the trick. While in=20
'quick and dirty mode' you probably won't be using interfaces a lot,=20
anyway.
Defaults look indeed useful, but the really crucial aspect to lay down=20
in an interface definition is what does it guarantee on the objects that
implement it. If amalgamating this with default defs would otherwise=20
obscure it (there's another issue I'm addressing below), I think
defaults=20
belong more properly to a class that implements the interface, not to
the=20
interface definition itself.
I guess knowing what other uses you have in mind for the
prefix_methodname=20
notation could be useful to decide whether it's warranted.
Check whether an object implements an interface
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
>From your examples, I get it that an object implements an interface iff
it
is an instance of a class that implements that interface.=20
So I guess any checking of the requirements expressed by the interface
is=20
done at the time when you bind() a class to an interface.
This paradigm perfectly fits static and strongly typed languages, but it
falls a bit short of the flexibility of python IMO. You can do very
funny=20
things to classes and objects at runtime, which can break any
assumptions=20
based on object class.
In your example:
def foo_proc(foo_arg):
foo_proxy =3D Foo(foo_arg)
...
x =3D foo_proxy.foo(a, b)
[added a, b again]
imagine foo_proc may only really cares that foo_arg is an object that
has=20
a foo() method that takes (a, b) arguments (this is all Foo guarantees).
* Will the Foo() call check this, or will it just check that some class
in foo_arg's bases is bound to the Foo interface?=20
In the second case, if someone has been fiddling with foo_arg or some
of its base classes, foo_arg.foo() may no longer exist or it may have
a different signature.
=20
* Why should Foo() _always_ fail for objects that _do_ meet the
requirements=20
expressed by the interface Foo but have _not_ declared that they
implement=20
the interface? If the point is to avoid false positives, interfaces=20
with this concern may still make the class check:
class Foo(interface):
def __call__(self, obj):
error =3D __check__(obj)
if error:
raise InterfaceError, error
else:
return self.proxy(obj)
=20
def __check__(self, obj):
if not hasattr(obj, "foo"):
return "No method foo found"
...
return interface.check_class(self, obj)
Making such check optional allows implicit (not declared) interface=20
satisfaction for those who want it. This should extend the applicability
of=20
interfaces.
And this brings up another problem with defaults: they would increase
false=20
positives. What if an interface wants to provide defaults for all its
methods?=20
Will then any object match it? This would force additional checking.=20
Even thought this doesn't look like a big issue to me, I think it's
cleanest=20
to leave validation for interfaces and implementation for classes.
Declare that an object implements an interface or part of it
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
Foo.bind(SomeClass)
Problems:
* I agree the declaration had better be included in the class
definition, at=20
least as an option.=20
* Declarative better than procedural, for this purpose.
* Only classes (not instances) can declare that they implement an
interface.=20
* Indexing notation to 'resolve' methods a bit counterintuitive.
What about:
# interfaces=20
class Foo(interface):
def foo(self, a, b):=20
...
def clash1(self, x):
...
def clash2(self):
... =20
=20
class Bar(interface):
def bar(self, x, y):
...
def clash1(self, a, s, d, f):
...
def clash2(self):=20
# actually equivalent to Foo.clash2
# we should really factor this out in one interface
# but imagine we can't do so for some reason...
... =20
=20
# implementations
class SomeClass:
# promise that all instances of SomeClass will implement Foo and
Bar
__implements__ =3D (Foo, Bar)=20
# automatically assumed to implement Foo.foo() =20
def foo(self, a, b):=20
...
=20
# There is not automatic name clash resolution. InterfaceError
unless=20
# we resolve these explicitly
=20
def fclash1(self, x):
...
fclash1.__implements__ =3D Foo.clash1 # maybe we should require
this=20
# to be a
tuple too?
=20
def bclash1(self, x):
...
bclash1.__implements__ =3D Bar.clash1
=20
def clash2(self):
...
clash2.__implements__ =3D (Foo.clash2, Bar.clash2) # or maybe this
is=20
=09
# not really needed?
=09
# seems to reflect bad=20
=09
# design anyway
# 'Remove' an interface from a subclass without actually removing it
from=20
# the base (or just cut the search with negative result):
=20
class Child(SomeClass):
__implements__ =3D (-Foo,) # will be found before the 'Foo' in =
the
base=20
# class
# an object that is *not* an instance of a class that implements Foo
wants=20
# to play the Foo
obj =3D Child()
obj.foo =3D lambda a, b: ...
obj.clash1 =3D lambda x: ...
obj.for_the_fun_of_it =3D lambda: ...
obj.__implements__ =3D (Foo,) # will be found before the '-Foo' in =
the
# class
obj.for_the_fun_of_it.__implements__ =3D Foo.clash2 # object dict will
be=20
#
searched first
Restrict
=3D=3D=3D=3D=3D=3D=3D=3D
This is, to make sure that an object is only accessed in the ways=20
defined in the interface (via the proxy).=20
This should be optional too, but your syntax does this nicely; you
can call Foo() as an assertion of sorts and ignore the result.
Note that the __implements__ method resolution magic would require=20
that you get a proxy, though.
What do you think?
Esteban.