[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.