On Nov 30, 2019, at 08:02, Ricky Teachey <ricky@teachey.org> wrote:
It sounds like many would agree that renaming some things could have many benefits, but the cost seems to be higher than I imagined.
What if an easier way could be created to manage renaming that avoided many of these problems?
Perhaps aliases of objects could be something worth baking into the language itself. I don't mean having multiple names pointing to the same object as we can do today, but something new entirely.
Are you suggesting we allow this so users/projects can add their own aliases into the stdlib, or so that the stdlib can grow a whole mess of aliases?
THE FOLLOWING IS PROBABLY A CRACKPOT IDEA, hear it out of you are in the mood to humor me on this Saturday morning.
What if the __dict__ for modules and classes could be something like a ChainMap, with multiple internal lookup mappings. The first level (by which I mean, the first mapping consulted) would be the same as exists today, but the second would contain a weak value dictionary with aliases pointing to objects so that when you do json.load_str, it returns json.loads object just like it would if you had added the alias in the way you would today. But the dict is weak value so that if an object is garbage collected, its alias entries are removed.
So if you monkeypatch json.loads, its last reference might go away, in which case json.load_str becomes a dead weakref and can no longer be used? What’s the benefit of that? Wouldn’t it be better to make load_str alias loads by name, so if an existing module monkeypatches loads, it automatically changes load_str as well?
So the second mapping would be a so called "alias dictionary".
How could this be done to overcome some of the same objections raised?
First, regarding discoverability:
dir() would work identically to as it does today, but you could give an argument to also provide aliases, something like:
>>> dir(json, alias=True)
['JSONDecodeError', 'JSONDecoder', 'JSONEncoder', '__all__', '__author__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_default_decoder', '_default_encoder', 'codecs', 'decoder', 'detect_encoding', 'dump', 'dumps [dump_str]', 'encoder', 'load', 'loads [load_str]', 'scanner']
Note that when you tell dir() that alias=True, additional aliases for objects are added to the output in the form of a string-ified list.
But not the same as actually stringifying a list, which gives you the reprs of its members.
Also, this means tools now have to understand how to parse this language; you can’t just do `for name in dir(spam): something(name)`, you have to do something like:
for name in dir(spam, alias=True):
if match := re.match(r'(\w+) \[(.*?)\])', name):
something(match.groups(1))
for alias in groups(2).split(', '):
something(alias)
else:
something(name)
Of course you could write a helper function to do the heavy lifting, but it’s still going to be nontrivial.
And all existing tools that rely on dir (e.g., to autocomplete in a REPL or IDE, or to build a proxy object that exposes the same names) aren’t going to see aliases until they’re changed to do this.
Also change the default object repr so that it says something like:
>>> json.loads
<function loads (alias: load_str) at 0x.... >
How does it even know that? Does it have to look at its module and name, then look at the module’s alias dict?
Most importantly, it seems like it would be preferred for * importing everything from a module to import only the first level dictionary and not all the aliases.
Doesn’t that defeat the purpose of aliases? If you don’t have them at the REPL when you * import, you’re going to learn the “unintuitive” name that is available, instead of learning the “intuitive” one.
Also, won’t it be at least annoying, if not confusing, if the help, dir, and repr mention the alias but you can’t use it?
Additionally, you can still add or monkeypatch new objects into a namespace (they would go into the first level dictionary, therefore overriding any aliases in the second level). Beyond that, since I'm talking about something new baked into the language, the code writing tools would be updated to know about aliases and provide useful auto complete functionality that makes it clear this is an alias.
How are you imagining that working? When you autocomplete in the standard REPL or ipython or bpython or PyCharm or PyDev, what will they do to indicate that it’s an alias?
For convenient addition of aliases and discovering them for a specific member:
There are likely all sorts of ways you could do this. Perhaps a dunder alias combined with an alias decorator could be added (probably in functools? not top level)?
You could add an alias like:
@functools.alias("load_str")
def loads(...): ...
assert loads.__alias__ == ["load_str"]
Is this dunder a @property that looks at the module’s alias namespace? Or is it actually a copy of the same information? If so, how are they kept in sync? (Or is that just a “consenting adults” thing—if you only add aliases with the decorator the two are in sync, but if you don’t, you have to do it right manually or you’ll confuse people.)
class MyClass:
@functools.alias("prof")
def process_file(self, ...): ...
assert MyClass.process_file.__alias__ == ["prof"]
So classes also have an alias dict? And presumably their dir also changes?
And unbound methods obviously have aliases (because they’re just functions), but what about bound methods (and classmethods and custom method-y decorators)? For example, if `mine = MyClass()`, does `mine.process_file.__alias__` also work? How? Do method objects have a @property for the dunder that delegates to the underlying `__func__`? If not, I don’t think this would be very useful.
You’d also need to rewrite what __getatrribute__ does to look at aliases on the class and base classes (and on the object, or is that not allowed?), which would break most existing custom __getattribute__ methods.