Uniform Function Call Syntax (UFCS)

Chris Angelico rosuav at gmail.com
Sun Jun 8 12:48:13 EDT 2014


On Mon, Jun 9, 2014 at 1:39 AM, jongiddy <jongiddy at gmail.com> wrote:
> e.g. I could define:
>
> def squared(x):
>     return x * x
>
> i = 3
> i.squared() => 9
>
> j = AClassThatImplements__mul__()
> j.squared() => whatever j * j returns
>
> but also:
> class AnotherClass:
>     def __mul__(self, other):
>         ...
>     def squared(self):
>         return specialised_method_for_calculating_squares()
>
> k = AnotherClass()
> k.squared() => calls method, not function
>
> In this case, there is a problem with letting hasattr('squared') return True for these first two instances.  See Ian's post for a description of the problem.

class Circle:
    def squared(self):
        raise NotImplementedError("Proven impossible in 1882")

The trouble is that logically Circle does have a 'squared' attribute,
while 3 doesn't; and yet Python guarantees this:

foo.squared()
# is equivalent [1] to
func = foo.squared
func()

Which means that for (3).squared() to be 9, it has to be possible to
evaluate (3).squared, which means that hasattr (which is defined by
attempting to get the attribute and seeing if an exception is thrown)
has to return True.

Except that it's even more complicated than that, because hasattr
wasn't defined in your module, so it has a different set of globals.
In fact, this would mean that hasattr would become quite useless.
(Hmm, PEP 463 might become a prerequisite of your proposal...) It also
means that attribute lookup becomes extremely surprising any time the
globals change; currently, "x.y" means exactly the same thing for any
given object x and attribute y, no matter where you do it.

The only way I can think of for all this to make sense is actually
doing it the other way around. Instead of having x.y() fall back on
y(x), have y(x) attempt x.y() first. To pull this off, you'd need a
special bouncer around every global or builtin... which may be tricky.

class MagicDict(dict):
    def __getitem__(self, item):
        # If this throws, let the exception propagate
        obj = super().__getitem__(item)
        if not callable(obj): return obj
        def bouncer(*a, **kw):
            if len(a)==1 and not kw:
            try: return getattr(a[0], item)()
            except AttributeError: pass
            return obj(*a, **kw)
        return bouncer
import __main__
# Except that this bit doesn't work.
__main__.__dict__ = MagicDict(__main__.__dict__)

It's theoretically possible, along these lines, I think. Whether it's
actually any good or not is another question, though!

ChrisA

[1] Modulo performance. CPython, AFAIK, does this exactly as written,
but other Pythons may and do optimize the actual "foo.squared()" form
to reduce heap usage. But in terms of visible effects, equivalent.



More information about the Python-list mailing list