On Wed, May 5, 2021, 02:11 Steven D'Aprano <steve@pearwood.info> wrote:
My comments follow, interleaved with Matt's.


On Mon, May 03, 2021 at 11:30:51PM +0100, Matt del Valle wrote:

> But you've pretty much perfectly identified the benefits here, I'll just
> elaborate on them a bit.
>
> - the indentation visually separates blocks of conceptually-grouped
> attributes/methods in the actual code (a gain in clarity when code is read)

Indeed, that is something I often miss: a way to conceptually group
named functions, classes and variables which is lighter weight than
separating them into a new file.

But you don't need a new keyword for that. A new keyword would be nice,
but grouping alone may not be sufficient to justify a keyword.


> - the dot notation you use to invoke such methods improves the experience
> for library consumers by giving a small amount conceptually-linked
> autocompletions at each namespace step within a class with a large API,
> rather getting a huge flat list.

We don't need a new keyword for people to separate names with dots.

Although I agree with your position regarding nested APIs, *to a point*,
I should mention that, for what it is worth, it goes against the Zen:

Flat is better than nested.


[...]
> - you can put functions inside a namespace block, which would become
> methods if you had put them in a class block

This is a feature which I have *really* missed.


> - you don't have the same (in some cases extremely unintuitive)
> scoping/variable binding rules that you do within a class block (see the
> link in my doc). It's all just module scope.

On the other hand, I don't think I like this. What I would expect is
that namespaces ought to be a separate scope.

To give an example:

    def spam():
        return "spam spam spam!"

    def eggs():
        return spam()

    namespace Shop:
        def spam():
            return "There's not much call for spam here."
        def eggs():
            return spam()

    print(eggs())
    # should print "spam spam spam!"
    print(Shop.eggs())
    # should print "There's not much call for spam here."


If we have a namespace concept, it should actually be a namespace, not
an weird compiler directive to bind names in the surrounding global
scope.

I agree with this. Great proposal though.



> - it mode clearly indicates intent (you don't want a whole new class, just
> a new namespace)

Indeed.


> When using the namespaces within a method (using self):
>
> - It allows you to namespace out your instance attributes without needing
> to create intermediate objects (an improvement to the memory footprint, and
> less do-nothing classes to clutter up your codebase)
>
> - While the above point of space complexity will not alway be relevant I
> think the more salient point is that creating intermediate objects for
> namespacing is often cognitively more effort than it's worth. And humans
> are lazy creatures by nature. So I feel like having an easy and intuitive
> way of doing it would have a positive effect on people's usage patterns.
> It's one of those things where you likely wouldn't appreciate the benefits
> until you'd actually gotten to play around with it a bit in the wild.


I'm not entirely sure what this means.


> For example, you could rewrite this:
>
> class Legs:
>   def __init__(self, left, right):
>       self.left, self.right = left, right
>
>
> class Biped:
>     def __init__(self):
>         self.legs = Legs(left=LeftLeg(), right=RightLeg())
>
>
> As this:
>
> class Biped:
>     def __init__(self):
>         namespace self.legs:
>             left, right = LeftLeg(), RightLeg()


Oh, I hope that's not what you consider a good use-case! For starters,
the "before" with two classes seems to be a total misuse of classes.
`Legs` is a do-nothing class, and `self.legs` seems to be adding an
unnecessary level of indirection that has no functional or conceptual
benefit.

I hope that the purpose of "namespace" is not to encourage people to
write bad code like the above more easily.


> And sure, the benefit for a single instance of this is small. But across a
> large codebase it adds up. It completely takes away the tradeoff between
> having neatly namespaced code where it makes sense to do so and writing a
> lot of needless intermediate classes.
>
> SimpleNamespace does not help you here as much as you would think because
> it cannot be understood by static code analysis tools when invoked like
> this:
>
> class Biped:
>     def __init__(self):
>         self.legs = SimpleNamespace(left=LeftLeg(), right=RightLeg())

Surely that's just a limitation of the *specific* tools. There is no
reason why they couldn't be upgraded to understand SimpleNamespace.


[...]
> > 2. If __dict__ contains "B.C" and "B", then presumably the interpreter
> > would need to try combinations against the outer __dict__ as well as B. Is
> > the namespace proxy you've mentioned intended to prevent further lookup in
> > the "B" attribute?
> >
>
> The namespace proxy must know its fully-qualified name all the way up to
> its parent scope (this is the bit that would require some magic in the
> python implementation), so it only needs to forward on a single attribute
> lookup to its parent scope. It does not need to perform several
> intermediate lookups on all of its parent namespaces.
>
> So in the case of:
>
> namespace A:
>     namespace B:
>         C = True
>
>
> >>>A.B
> <namespace object <A.B> of <module '__main__' (built-in)>>
>
>
> Note that namespace B 'knows' that its name is 'A.B', not just 'B'

If I have understood you, that means that things will break when you do:

    Z = A
    del A
    Z.B.C  # NameError name 'A.B' is not defined

Objects should not rely on their parents keeping the name they were
originally defined under.



[...]
> Traversing all the way through A.B.C does involve 2 intermediate lookups
> (looking up 'A.B' on the parent scope from namespace A, then looking up
> 'A.B.C' on the parent scope from namespace A.B). But once you have a
> reference to a deeply nested namespace, looking up any value on it is only
> a single lookup step.

That's no different from the situation today:

    obj = spam.eggs.cheese.aardvark.hovercraft
    obj.eels  # only one lookup needed


> > 3. Can namespaces be nested? If so, will their attributed they always
> > resolve to flat set of attributes in the encapsulating class?
> >
>
> Yes, namespaces can be nested arbitrarily, and they will always set their
> attributes in the nearest real scope (module/class/locals). There's an
> example of this early on in the doc:
>
> namespace constants:
>     NAMESPACED_CONSTANT = True
>
>     namespace inner:
>         ANOTHER_CONSTANT = "hi"
>
> Which is like:
>
> vars(sys.modules[__name__])["constants.NAMESPACED_CONSTANT"] =
> Truevars(sys.modules[__name__])["constants.inner.ANOTHER_CONSTANT"] =
> "hi"

Can I just say that referencing `vars(sys.modules[__name__])` *really*
works against the clarity of your examples?

Are there situations where that couldn't be written as

    globals()["constants.NAMESPACED_CONSTANT"]

instead?

And remind me, what's `Truevars`?



--
Steve
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-leave@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/JF6OWQJ7JRH4CGUWU3APCDMSAB37MR67/
Code of Conduct: http://python.org/psf/codeofconduct/