CRAZY: First class namespaces

Amit Patel amitp at Xenon.Stanford.EDU
Sat May 27 20:14:52 EDT 2000


I have to warn you.  This is a bit wacky, and I have no illusion that
any of this will go into Python.  It's just some stuff that Python has
inspired me to think about, mainly because Python exposes namespaces
to the programmer, and programmers do cool things with it.  How far
can that go?  (You're supposed to run away yelling when you hear
someone say "How far can something go?")  This idea touches on
stackless (because stack frames become objects, which are on the heap)
and nested scopes, too, if that'll help you run screaming.  But if
you're ready to read some crazy stuff, here it is:


==

Python and many other OO languages have a nice way of looking up field
names in objects -- they look in an object, then in the class, then
superclass, and so on.  It's a chained lookup system.  Python also
uses a chained lookup for variable names.  It first looks in locals()
and then looks in globals().  If we have nested scopes in P3k, it will
look in the local namespace, then in the parent namespace, then in its
parent, and so on until we reach the global namespace.  Jim Fulton's
ExtensionClass implements a chained lookup system that can go from one
object to its "container" to find values.

I'd like to see all of the chained namespace implementations unified.
Objects, classes, modules, activation records ("stack frames") -- all
are namespaces underneath.

In this proposal, the __parent__ name is treated specially in an
object.  For looking up a name, you first check the namespace and then
if you don't have the name, you ask your parent.  If you have no
__parent__, you give a NameError.  For assigning to a name, you always
assign the name in the current namespace, and you never look at the
parent.

Note that this is just like what we do now, except we call the special
field "__class__" if it's in an object and "__bases__" if it's a
class.  In this system, "ordinary" objects would simply have their
class as the __parent__.  An ordinary class would have its base class
as its __parent__, as long as there's no multiple inheritance [1].
Environmental acquisition (e.g., ExtensionClass) would have its
container has its __parent__.  At this point, an ordinary class or an
acquisition class is no different than a "normal" object, so we may
not need classes.  (Already in Python, classes and objects aren't so
different.)


The lookup semantics (chaining for getattr and not chaining for
setattr) almost match both how fields in objects work and how local
variables work [2].  So we could make namespaces into objects too.  In
the following example:

  # module spam.py

  x = 5

  def foo(y):
     print x
     x = 8
     print x

     def cheese(z):
        print x, y, z
     return spam

  bar = foo(10)
  bar(35)

The global (module) namespace is an object.  (In fact, it's the module
object.)  It's the object <x = 5, foo = function....>.  The function
foo needs to remember its scope, so its .co_namespace points to the
spam module object.

When we *call* foo(10), we need to create a new namespace for the local
variables.  This is an object <y=10, __parent__ = spam-module>.
When we print x, we look for x in this object, and it isn't there.  So
we look for x in the __parent__, which is the spam module object.
It's there, so we return 5 and print it.  The next thing we do is
assign x to 8.  Since there's no chaining on assignment, we create an
entry x=8 in the namespace object, which becomes <y=10, x=8, __parent__ =
spam-module>.  Now we try to print x again.  We look in the
namespace object and find x is 8, so that's what we print.  Next we
define a function cheese, which gets its .co_namespace set to the
current namespace object.  Then we return that function object.

The global module now gets bar=function... added to it.

And we call bar(35).  What happens here?  When we call a function, we
have to create a new namespace object where the parent is set to the
co_namespace.  So that's <z=35, __parent__ = <y=10, x=8, __parent__ =
spam-module>>.  So when we try to print x, y, z, we find 8, 10, 35.


Does that all make sense?

Summary: rules for variable lookup look and smell a lot like rules for
object lookup.  If we combine the two, we can also put in classes,
acquisition classes, and modules.  It's "everything's an object"
pushed a bit farther, into the area of local variables.  I would keep
the syntax that distinguishes classes from objects, but it'd be just
syntax -- nothing deeply different underneath.

There are few more notes I have:


* Methods on objects:

  We have to resolve the issue of "self" being a magic argument.  If
  objects and classes are the same, then either def foo(self, x) or
  self.foo = lambda x: .. isn't consistent.

  We might want to say "method" introduces an unbound method and
  "def" introduces a bound method (i.e., a function that isn't going
  to take 'self').

* A magic "self" variable:

  We could have it so that when you invoke a method, it sets not only
  __parent__ to  the co_namespace, but also sets self to the object.
  That way, you'd define foo() without listing "self", but you'd still
  have a "self" object and you'd access fields with "self.f".

     method foo(x):
         print self.f + x

  I'm not convinced this is the right thing to do.

* Inline objects:

  It'd be nice to have a lightweight way of declaring objects, like
  the <name=value, name=value, ...> syntax I used in the examples
  above.  If objects are everything, you want to be able to make them
  all the time.  It's possible to use {} even though {} is used for
  dictionaries.  If it's {key:value, ..} then it's a dictionary and if
  it's {name=value, ..} then it's an object.  But that may be too
  confusing.

* The "global" keyword:

  We still want some way of getting to the next level of scope.  Since
  objects and namespaces are combined, and the object has a special
  __parent__, the __parent__ is exposed as a local variable.  So we
  could do:

  x = 5
  def foo():
     __parent__.x = 10

  to assign to the global x.  If it's nested more:

  x = 5
  def foo():
     def cheese():
        __parent__.__parent__.x = 5

  Not ideal, but then, I don't deal with globals too much anyway.
  Maybe 'global' can be a magic variable (not keyword) that gets set
  to the last non-empty __parent__.  Then you'd end up doing:

  x = 5
  def foo():
     def cheese():
        global.x = 5

* Cycles and refcounting

  If we do nested scopes, we already get the cycles problem.  I don't
  think making namespaces into objects makes anything worse.

* Modules:

  Modules already exhibit this unification of namespaces and objects.
  From inside the module, you can refer to variables.  From otuside
  the module, you refer to fields.  Unifying namespaces and objects
  wouldn't really do anything to modules.  Instead, it'd make
  all namespaces behave like a module in that they're objects.

* Methods on modules:

  I think you could get methods (including things like __repr__) on
  modules free here.  I haven't thought about it enough, but my
  intuition tells me that since classes have gone away, and you can put
  methods on objects, you should be able to put methods on modules too.
  We just have resolve the bound/unbound method issue.

* The "with" statement from Pascal:

  I suspect you can do "with" pretty easily but I'm not sure about the
  details.  It may require multiple inheritance (aiee) if you want to
  be able to see local variables at the same time you see an object's
  fields.

* Assignment to __parent__:

  So far I've been assuming that __parent__ is something set by Python
  for internal use.  For objects, it's set when you call a class
  constructor.  For classes, it's set when you declare the class using
  the "class" construct.  For namespaces, it's set when you call a
  function.

  But really, the cool stuff is really when the user can assign to
  __parent__!

  For example, __parent__ = self would make it so that you can assign
  to fields in your object without using "self.".  (You'd also lose
  your local *and* global variables, which would discourage anyone
  from doing this.)  The environmental acquisition aspect of
  ExtensionClass is pretty easy now -- you just create a raw object
  and set its __parent__ to its container.  You could "reparent" an
  object on the fly to change its class.  Or you could use
  prototype-based programming techniques, as in Self.

* Efficiency:

  It'll probably be harder to optimize name lookup with something like
  this (especially if you can change __parent__).  However ..  with
  only one underlying implementation for namespaces, objects, modules,
  and classes, any optimization work you perform here will benefit all
  of those language features.

* Related language features:

  Simula's objects were originally activation records / stack frames.
  So making the two similar or even the same is a really old idea.
  Smalltalk makes everything an object, so my guess is that activation
  records too were objects.  Smalltalk blocks are objects too.
  JavaScript tries to do something that unifies the two, but gets many
  aspects totally wrong, by mixing up static scopes and dynamic
  activation records.  (For example, functions get an object with
  local variables when they're *declared*, even though they haven't
  been *called* yet!)  Pascal and JavaScript have a "with" statement
  that lets you view an object's fields as variables.



       -crazily-yr's - Amit


FOOTNOTES

[1]  One snag is multiple inheritance.  I don't think we really
     need multiple inheritance once we have nested scopes, but MI is
     a really controversial issue.  Here's an example of MI:

     class TCPServer:
        ...
     class ForkingMixIn:
        ...
     class ForkingTCPServer(ForkingMixIn, TCPServer):
        pass

     What I'd do instead is write ForkingMixIn this way:

     def ForkingMixIn(AnyServer):
        class ForkingMixIn(AnyServer):
           ...
        return ForkingMixIn

     [Note: I wouldn't want to use this syntax, but this illustrates
     the underlying construct.]

     Now I can write ForkingTCPServer this way:

     ForkingTCPServer = ForkingMixIn(TCPServer)

     The advantage of this over MI (in Python) is that you can call
     the methods from the base class.  Right now, ForkingMixIn can't
     easily define method foo() to call TCPServer's foo() and then do
     one more thing.  With the above setup, ForkingMixIn has a handle
     to the base class, and can call AnyServer.foo(self).

     Alternatively, a namespace could have a __parents__ tuple, and it
     could search each one of them.  However, this would be
     unnecessary for variable scoping (unless your mind is really
     twisted!).  It'd be useful for the "with" statement, but it seems
     confusing.

[2]  One thing I find confusing in Python is that a local name 
     definition applies even before it's been executed:

     x = 5
     def foo():
        print x
        x = 3
        print x

     IMO, this should print 5 and then 3.  The unified namespace 
     proposal would do this.  However, Python currently gives a 
     NameError.
-- 
--
Amit J Patel, Computer Science Department, Stanford University
http://www-cs-students.stanford.edu/~amitp/



More information about the Python-list mailing list