Voting for PEP 308 (was Re: For review: PEP 308 - If-then-else expression)

Stephen Horne intentionally at blank.co.uk
Wed Mar 5 07:55:55 EST 2003


On Tue, 11 Feb 2003 15:31:43 +1100, "Delaney, Timothy C (Timothy)"
<tdelaney at avaya.com> wrote:

>We were unfortunately unsuccessful in convincing people that some things
>are better as a function accepting multiple arguments than as a method of
>one of those two arguments.

That argument actually makes a lot of sense to me.

Actually, I've come to realise recently that the object.function ()
notation is a bit of a kludge in general (whatever language it's
written in).

The reasoning is this...

The object.method notation exists for the following reasons...

1.  Because the method 'belongs' to the class/object for encapsulation
    and data hiding purposes.

2.  Because the polymorphism of the class/object is resolved using
    that one object.

If it wasn't for these issues, the object should just be written in
the call as a normal parameter.

The first of these does not really apply to the same degree in Python,
and that is a good thing. This is, in my view, a 'bug' in the object
oriented ideal.

Classes are not, in my view, the natural scope for encapsulation and
data hiding. Interacting components, including objects, are often
inherently tightly coupled. Encapsulating too much within a class
often comes back to haunt you when your requirements change, and you
need to handle more dependencies between data that you hid in
different classes. This is one reason why we now have books on
refactoring, and tools that automate refactoring - we have to refactor
because we hid our data too well when we wrote the first version.

The natural scope for encapsulation and data hiding is a subsystem,
package or module. There are exceptions, so the Python way of using
__name__ has its uses, but I prefer the Python system of not
advertising the 'private' identifiers in a module.

This is actually closer to the wider engineering view. For example,
the gear-wheels in a clock are designed to fit together and to work
together to achieve a goal. They are not designed or maintained
independently from each other. One gear wheel is not a 'black box'
from the perspective of another - they are tightly coupled components.
Only when you look at particular assemblies, or at the clock as a
whole, do the individual gear wheels become private details. You could
say the same about a gear box in a car - the gear ratios it supports
are tightly coupled to the capabilities of the engine, even though (at
the level of abstraction where 'engine' and 'gear box' are seen as
discrete components) you no longer care about the smaller components
that make up the gear box.

In short, the components that make up a system are almost always
tightly coupled. Fighting against that just makes life harder. The
classes and functions in a particular module are written to work
together and there's no reason why they shouldn't have access to each
others private internals. What might be useful is a way of nesting
small modules within a larger module file (like Ada packages, Modula 2
modules, or perhaps a bit like C++ namespaces but with 'private'
declarations supported - but not like current Python packages, which
solve a different problem). All class members should be private - but
private to the module, not just the class.

Resolving polymorphism is therefore the main reason for having an
object.method syntax - but this isn't as compelling as it seems
either. It is an artifact of the single dispatch mechanism (ie
resolving the polymorphism based on precisely one parameter - 'self'
or 'this'). Using a language which supports multiple dispatch, the
simplest notation would be to fall back on a simple function call with
an explicit specification of which parameters are used to resolve
polymorphism. You could then set up something like...

  system test :
    #  Declare the class heirarchy
    class c_Base :
      pass

    class c_Derived1 (c_Base) :
      pass

    class c_Derived2 (c_Base) :
      pass

    #  Declare the call prototype - calls refer to this rather than
    #  any of the specialised implementations.
    declare name (late c_Base a, late c_Base b, c)

    #  Declare the implementations to call once the polymorphism
    #  is resolved.
    def name (late c_Derived1 a, late c_Derived1 b, c) :
      pass

    def name (late c_Derived1 a, late c_Derived2 b, c) :
      pass

    #  Choose not to have different versions based on b
    #  if a is of type c_Derived1
    def name (late c_Derived2 a, late c_Base b, c) :
      pass

In the above code, the hypothetical 'late' keyword refers to late
binding and the typenames are only intended to be specified for
late-binding parameters.

This is, of course, for illustration of an idea - not a proposal. It
is also less than ideal - like C++, it forces the programmer to
anticipate the need for polymorphism, rather than allowing anything to
be overridden (unless declared 'final' or whatever) on demand.

I've thought about it this approach quite a bit, and (once a range of
knock-on effects are considered) the overall approach seems almost
like a new paradigm - perhaps 'system oriented' rather than 'object
oriented'.

Back to the original point, though - using a method like this, you
could keep the ability to override methods using polymorphism (and do
so more flexibly, in general) yet you don't have to single out one
parameter of each function as being 'special'.

-- 
steve at ninereeds dot fsnet dot co dot uk




More information about the Python-list mailing list