A better self

Bengt Richter bokr at oz.net
Sat Jul 20 01:45:24 EDT 2002


On Tue, 9 Jul 2002 15:10:59 -0700, "David LeBlanc" <whisper at oz.net> wrote:

>I notice that in every discussion of self, there's always a '.' in there.
>So, why not make a leading '.' mean self? AFAIK, it's otherwise currently
>illegal. The only drawback that I can think of is that it's less visible as
>a first argument for class defs.
>
>class foo():
>	def __init__(., me, me2, me3)
>		.a = me
>		.b = me2
>		.c = me3
>	def fee(.)
>		print .a, .b, .c
>	def bar(.)
>		.fee()
>
>What I like about it is that it's resonant with the use of '.' to denote
>members/components of modules (i.e: sys.path) and suggests that the
>identifier is a member of the local "capsule" (or class scope if you
>prefer). I see it as answering the needs of both camps: it's easy to typy,
>unobtrusive and yet supports the "belt and suspenders" crowd who are not
>comfortable with an implied self.
>
>Before you all react (programmers can be SO conservative), please think
>about it!
>
Ok, I thought about it some ;-)
I think I may even have proposed or seconded virtually the same self-less-dot in the past,
so I'm not going to criticize that too much, but I think there is more to think about ;-)

ISTM that self in self.x is really a named name-space selector identifying a
list of "directories" to search in orderly fashion for the appended 'x',
or a default place to bind 'x' if not already bound.

A bare 'x' implies a search through another namespace, also a list of
"directories", depending on context. (By quoting I mean that some may
not literally be directories).

If these lists of directories could be exposed simply for purposes of declaration
and access, we might be able to do some interesting things.

Suppose 'namepath' were magically defined in all execution contexts to be
the list of directories to search for a bare name in that context (i.e.,
pretty much what dir() sees now). Hence namepath[0] would always be the local
namespace first accessed for a bare name, unless other factors apply.

In a method, 'self' would normally be found in namepath[0], and self.x would
be found in self.namepath[0] if it was an instance attribute, and self.namepath[1]
if it was in self.__class__.__dict__, etc.

Now if namepath were dynamically modifiable, you could get bare name access
to instance attributes as in this example:

class FOO:
    def __init__(self, v): self.v = v # old style init
    def myMethod(self):
        x = 123           # default local
        namepath.insert(0, self.namepath[0])
        y = 456           # bare means self.y here!!
        print v, x, y     # found in namepath[0], namepath[1], and namepath[0] resp.
        namepath.pop(0)   # back to default
        print self.v, x, self.y # duplicate previous output

An alternative (additional) way to spell the above namepath operations might be:

    def myMethod(self):
        x = 123   # default local
        with self.namepath[0]:  # [1]
            y = 456
            print v, x, y
        print self.v, x, self.y # [2] duplicate previous output

[1] meaning namepath.insert(0, self.namepath[0]) until dedent.
    A list of directories might also be allowed for "with ... :"
[2] dedent accomplishes namepath.pop(0) if it's a with-block # back to default
Note that namepath always appears locally as if namepath == namepath[0]['namepath']

This still leaves "global x" to be handled generally in terms of namepath.
IOW, a kind of private namepath.insert(0, namepath[-1]) for 'x' but not
all names, and no need to search beyond the one directory. The general problem
is to control where a bare 'x' will be re/bound for assignment. Perhaps a placement
function, e.g.,

    place('x', namepath[-1])  # effect of "global x"

Thus the equivalent of the default for any bare name would be

    place('name', namepath[0])

Given the above, we are perhaps in a position to write

    place(('x','y'), namepath[1])

and expect to use it with nested scopes, e.g.,

    def foo():
        def init_xy():
            place(('x','y'), namepath[1]) # force binding in immediately enclosing scope
            f = file('blah') # local f goes to namepath[0] # just a weird example
            x, y = map(int, f.readline().split()) # x and y bindings are created in enclosing scope
        def bar(dx,dy):
            with namepath[1]: # could/should use place
                x += dx
                y += dy

If you wanted to specify name-specific lookup search paths it could be like place:

    lookup(('x','y'), namepath[1:], {'x': 'no NameError for x'}) # y will raise NameError if not found.


I am implying that default assignment goes to namepath[0] unless place() has defined
other places for the name in question, and that lookup by default does not just go from
local to global, but through lexically enclosing scopes in between (as found in the
default namepath list). Additionally, namepath can be modified dynamically, which will
control search for names, unless overridden for specific names by lookup().

Also, for a given statment, rebinding should occur in the same namepath directory where
a lookup succeeds for the same name, so that x += 1 will not create a local based on an 'x'
in an enclosing scope. However x = 1 would bind in namepath[0] unless overridden by
a place('x', somewhere). This would allow bar() above to leave out the "with namepath[1]:"
statement. However, place() might  be better optimizable, since it would not change default
lookup for dx and dy.

For convenience, namepath(n) could present the relevant dictionary as a virtual "self" of
a virtual object whose attributes are stored in that directory. I.e.,
    namepath(1).x = 123 would be equivalent to
    namepath[1]['x'] = 123
which would bind x in the immediately enclosing scope.

This is hot OTTOMH, offered in the hope of stimulating useful and/or enjoyable discussion.
ISTM these issues are bound (non-Python sense ;-) to play an evolving role in Python given
nested scopes etc. Obviously a naive implementation of the above would have a lot of overhead.
OTOH, place() and lookup() might provide optimization opportunities.

Hm, a further thought: If place() had an optional keyword argument named 'prop',
we could write

    place('p', myModule.namepath[0], prop=0) # ensure that p is not already a property at myModule.p
    p = property(get_p, set_p)               # myModule.p is property object itself here
    place('p', myModule.namepath[0], prop=1) # myModule.p triggers property methods after this

or if we know p is ordinary or nonexistent, just

    myModule.p = property(get_p, set_p)      # myModule.p is property object itself here
    place('p', myModule.namepath[0], prop=1) # myModule.p triggers property methods after this

and expect writing myModule.p to trigger the get_p or set_p functions. If you wanted to
rebind p in that directory, you would have to do another place('p', ... prop=0). One way
to do this, and keep ordinary lookups fast, might be to associate a flag byte with names
in dictionaries, so that it's a one-instruction matter to decide whether special processing
such as for properties is required. A C switch on this byte could also handle special-casing
for slots, I would imagine. The place() function would change bits in the flag byte.

With this methodology for properties, the magic actions could be triggered on any access
to a name via any specified directory, not just class attribute directories accessed via instances.
Sharp tools are dangerous, of course ;-)

I'm not sure whether tying property magic to name lookup is a better idea than tying the magic
to the looked-up object and its state independent of access path, but that is another discussion.

Regards,
Bengt Richter



More information about the Python-list mailing list