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.
- it mode clearly indicates intent (you don't want a whole new class, just
a new namespace)
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())
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.
- 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
<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
- 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
And remind me, what's `Truevars`?