9 ms·
Python proposal (inspired by Lua)
- necovek 3y agoLists (like *args) are by definition ordered, and dicts (and thus **kwargs) are by definition unordered key-value pairs. This would be very confusing in suddenly giving dicts a half-ordered, half-not semantics in a particular context. Why wouldn't you simply switch to setting everything through kwargs instead? Not to mention that this would have a performance penalty for dicts as they would have to keep an ordered set of integer keys in case they are used with **.
- ledauphin 3y agoin Python, dicts are actually definitionally ordered, since 3.7, so this objection doesn't carry much weight. additionally, you might be interested to know that the implementation of dicts as ordered was found to be more performant than its predecessor. Finally, even if they were unordered, there would be no need for ordering in this proposal, which depends on unique integer keys starting at 0.
- necovek 3y agoThe fact that they are definitionally ordered does not really matter for this discussion: for all intents and purposes of this proposal, they are unordered (insertion order is pretty much arbitrary order when it comes to their use as positional arguments or kwargs). And my intent was a bit more specific: I was thinking of "sorted", so definitely my bad. For the proposed use case, either **kwargs would have to sort a subset of the dict with integer keys to map positional arguments to their, ahem, positions, and then either leave the rest as-is (or sort it altogether), or a dict would have its definition modified to keep this order as items are added. I dislike both, and I definitely hate implicit sorting happening with **.
- ali_m 3y agoDicts already have confusing half-ordered, half-not semantics. As of 3.7 they are guaranteed to be insertion-ordered, but operators like == don't care about order.
- pphysch 3y agoI don't think it's confusing or even on the level of "semantics": looping over dict keys/elements is a common pattern and the 3.7 change makes that a little more predictable and consistent. Two dicts with same elements not being equal would be surprising and confusing.
- ali_m 3y agoIf we're treating them as ordered containers then it really ought to be surprising and confusing that two dicts with the same elements in a different order are considered equal. Other ordered containers such as lists or tuples don't behave this way. I'd use an `OrderedDict` in situations where I have a mapping where the order of elements is significant. `OrderedDict` signals intent much more clearly, and its `__eq__` method cares about order.
- necovek 3y agoYes, but I hope nobody really relies on that ordering in their code, as, as you say, it is pretty inconsistent. Eg. you can't rely on dict_b_keys = iter(dict_b) for key in dict_a: value_a = dict_a[key] value_b = dict_b[next(dict_b_keys)] print(f"{value_a} == {value_b}: ", value_a == value_b) The problem with the proposal is that it will be even stronger: you either require '*' to sort in-place (I assumed that would be even worse and thus unacceptable, so I ignored that case — it seems others believe that to be a more realistic implementation), or require dicts to maintain a half-sorted map of integer keys.
- Hamuko 3y agoDicts are ordered as of Python 3.7. >the insertion-order preservation nature of dict objects has been declared to be an official part of the Python language spec. https://docs.python.org/3/whatsnew/3.7.html https://docs.python.org/3/whatsnew/3.7.html
- SushiHippie 3y agoPython dictionaries are guaranteed to preserve insertion order since python 3.7
- JulianWasTaken 3y agoBesides the mistake about dicts being ordered in Python which was pointed out by others, > Why wouldn't you simply switch to setting everything through kwargs instead? Not all arguments in Python are passable both ways. In def f(a, /): print(a) `a` may be passed only positionally.
- kstrauser 3y agoI'd argue that functions with only position arguments are a place where you'd never want to use this proposal, ever. A tuple or list would be the right data structure to pass in there.
- JulianWasTaken 3y agoOf course it would be irrelevant for a function that only took positional arguments. I was demonstrating the existence of such things in general since OP seemed unaware. For the proposal, you'd of course only use it for functions that took more kinds of arguments.
- necovek 3y agoThanks for pointing it out. Still, it seems highly unlikely someone would decide to use such a function declaration yet want to use the proposed syntax to pass both positional and keyword arguments.
- fbdab103 3y agoJust laughing at the Cunningham's Law response of everyone jumping in to let you know that dictionaries are now insertion ordered.
- throwmeout123 3y agoThe proposal in question is not that good so maybe the order of the dicts is more interesting :P
- necovek 3y agoWhat I meant was "sorted", which is a bit more specific than "ordered", but it serves me right :) The funniest part is that this does not really matter for this discussion: insertion order is pretty much arbitrary when what you need is for key 0 to come first, key 1 to come next...
- joshuamorton 3y ago> Not to mention that this would have a performance penalty for dicts as they would have to keep an ordered set of integer keys in case they are used with *. ...why? I don't love this proposal, but `{1:"a", 2:"b", 0:"c"} == {0:"c", 1:"a", 2:"b"}`, and so presumably either would have the same behavior when passed to `def f(foo, bar baz): ...`. That the runtime would correct the order as part of the splat operation seems implied?
- necovek 3y agoPassing a huge dict and assuming the runtime would magically do the sorting was exactly an implementation I ignored because it sounded preposterous to me. Just imagine passing in a 500k item dict to ** by accident: dicts maintaining sorted state appeared the only sensible implementation to me.
- joshuamorton 3y agoBut again: why do you need to care about the whole sorted state thing? "Sorting" a dictionary keyed by index is linear time. Like the cost of doing that isn't going to be greater than the existing cost of keyword arguments, which can already be arbitrarily ordered! Like, what about this is more "preposterous" than passing a massive dict to kwargs already?
- necovek 3y agoPassing a dict is passing by reference, and if the called method reads 5 elements from that dict, it's tiny constant time. Sorting of a large collection happening implicitly is too expensive for what would generally be a benign mistake.
- joshuamorton 3y agoBut again: you don't actually need to sort anything. Like that's really important you don't need to sort anything! Passing kwargs requires resolving n references, one for each kwarg. Passing args by index requires passing n references. Like in code: def convert_dict_to_args(f, magic_kwargs): out_args = [] out_kwargs = {} for k, v in magic_kwargs: if isinstance(k, int): out_args[k] = v else: out_kwargs[k] = v return f(*out_args, **out_kwargs) There's no sorting necessary! There's potential particular optimizations that might need to happen to make this work with python's opcodes for fast function calls, since out_args isn't constructed as a tuple which is internally optimized in function calls, but afaik you're already not getting those optimizations/fast paths if you're passing by name and/or using kwargs, so this isn't a change. Edit: for context, >>> from timeit import timeit as t >>> t(stmt='f(1,2,3)', setup='def f(a, b, c): pass', number=int(1e8)) 5.765844099999981 >>> t(stmt='f(*(1,2,3))', setup='def f(a, b, c): pass', number=int(1e8)) 5.840746500000023 >>> t(stmt='f(a=1,b=2,c=3)', setup='def f(a, b, c): pass', number=int(1e8)) 7.400104499999998 >>> t(stmt='f(**d)', setup='d=dict(a=1,b=2,c=3)\ndef f(a, b, c): pass', number=int(1e8)) 20.779321399999958 >>> t(stmt='f(**d)', setup='d=dict()\ndef f(a=None, b=None, c=None): pass', number=int(1e8)) 9.583819900000094 So yeah using kwargs at all has a significant performance penalty, so you're already hitting performance penalties for using kwargs, keeping a sorted view isn't going to help sinificantly.
- VectorLock 3y agoI'm guessing someone got inspired by https://adamj.eu/tech/2021/05/11/python-type-hints-args-and-kwargs/ https://adamj.eu/tech/2021/05/11/python-type-hints-args-and-... IMHO mixing args and kwargs together like this - having a dict with mixed types for keys - looks kinda grotty.
- acmiyaguchi 3y agoI agree, it changes the semantics of dictionaries in a way that doesn't feel right. There are several things that are off-putting, but another example in vein with the mixed key types is the implicit ordered dict.
- Spivak 3y agoEvery dict in Python is already ordered so it doesn't change much and Python doesn't blink at something like `int | str` so you can already do something like {0: "hello", "0": "world"} if you hated your readers. The only change would be changing how ** works when used as function arguments which is already bespoke.
- tetha 3y agoHm, where is the large advantage over building a list pos_args and a map kw_args and calling func(*pos_args, **kw_args)? You can also transform between both forms with a few comprehensions with a few isinstance-checks. >>> def foo(a, b, c=42, d=23): ... print(a, b, c, d) ... >>> args = ['a-value', 'b-value'] >>> kwargs = { 'c': 'c-value', 'd': 'd-value' } >>> foo(*args, **kwargs) a-value b-value c-value d-value
- kstrauser 3y agoI think you're right. Or if you have functions you'd want to use this more often than never, I'd strongly prefer to make them kwarg-only (ie `def foo(*, arg1, arg2, ...)`). Then you don't have to care about ordering at all.
- wheelerof4te 3y agoThere is none. But let's get another hack in, why not. /s
- ltbarcly3 3y agoBuilding up kwargs should be something done as a last resort, or in situations where you need to be a pass-through eg decorators, and the current functionality covers that more or less perfectly. I am suspicious that this suggestion is just to enable poor quality code that the person suggesting it would just grow out of over time. You can just write a class to track your kwargs and args if you want to do what is in this suggestion. Then when you want to use it you would have to do `whatever_func(*args.args, **args.kwargs)`, which isn't exactly a huge amount of trouble.
- izoow 3y agoI don't see the point personally. I don't feel like this is something you'd want to do often, and it's not like the current way to do it is that much harder. This seems to just complicate things with hardly any benefit.
- dehrmann 3y agoMaybe I've been writing boring code, but I haven't run across this problem all that often.
- wheelerof4te 3y agoNope, you have been writing the correct and clear code. Boring code is good code in our profession.
- SSchick 3y agoThis doesn't seem like it would solve any real-world problems? Other than maybe hacking around in repl mode? This would require dicts to be somewhat ordered, as explained by others here already. Also I feel like this would violate `explicit is better than implicit` as per https://peps.python.org/pep-0020/ https://peps.python.org/pep-0020/ and would most certainly lead to extremely confusing/brittle code. I'm somewhat surprised this was upvoted so much.
- chippiewill 3y agoHard disagree. The reason why Lua can have this is because of the ergonomics of its table datatype. The table is used for both list-like and dictionary-like usecases and has specific optimisations around iterating over integer keys in a table. Python on the other hand has a clear separation between the two usecases as separate datatypes, so munging them like this would muddy the language.
- wheelerof4te 3y agoVery much agreed. As if Python wasn't muddied enough, people now want to tack on every bit of syntax sugar just to have...another different way to do the same thing. Urrrghh...
- qsort 3y agoYeah, this is dumb as rocks. On top of what you said, Python already has a mechanism to do what the author is claiming! >>> def f(x, y, a, b): ... print(x, y, a, b) ... >>> args = {'x': 98, 'y': 99, 'a': 1, 'b': 2} >>> f(**args) 98 99 1 2
- theptip 3y agoDoesn’t work with functions that take an arbitrary number of positional args. Not sure of any real examples of that function API but I suspect that’s what Ned is going for.
- ttepasse 3y agoThat looks like something which could be implemented with a function decorator. (A little bit ugly, though.)
- aidos 3y agoOk so what about: def fn(a) fn(**{'a': True, 0: False) I get it, python is dynamic so it allows all sorts of things but I’d be pretty cross if I was debugging code like this. Explicit is better and all that. Nobody really needs this, but, as ever, submit a PEP and see if there’s a load of people who have a calling for it.
- wruza 3y agoIn case you’re wondering what Lua does in this case, it has a special syntax iff the sole argument is a “table” (core dict/array mix) or a string: f {1, 2, a=3} is equivalent to f({1, 2, a=3}) A function f(t) receives an argument t in both cases. This makes a good use for DSLs by reducing ()-noise. It doesn’t have **kwargs in pythonic form, although it has varargs syntax. But all parameters are strictly positional and don’t self-pack or self-unpack in any way. So this inspiration is pretty far from what happens there.
- kzrdude 3y agoAnd calling without ()'s is also available if you call with a single string literal, as often seen in require "modulename".
- shrimpx 3y agoIf the function definition is `def f(x, y, a=None, b=None)`, you can already just call `f(*dict(a=1, x=98, y=99, b=2))`. So instead of using `0` and `1` for the positions, like the tweet author suggests, you can just use the parameter names. However, I've always disliked that you could do this in Python -- passing positional args out of order as kwargs. And also that you could pass kwargs as positional args. Python 3 and later 3.8 introduced optional * and `/` in signature syntax to prevent both of these behaviors, which I appreciate, but IMO this should have been the default behavior from the beginning. Now you can prevent both behaviors by defining the function with `def f(x, y, /, *, a=None, b=None)`.
- DangitBobby 3y agoWhat's the benefit of prohibiting passing positional arguments by name?
- shrimpx 3y agoWhen args are positional-only, you can rename them without breaking callers. The names of the kwargs can be considered "external" and the names of the positional args remain private to the function. This gives authors flexibility to rename args, or even convert all the positional args to variadic `*args`, without affecting users. It also gives authors control over argument order. Like you write `def pow(x, y)`, conventionally meaning "x to the power y", and you don't want users to call it like `pow(y=2, x=3)`.
- wheelerof4te 3y agoPython was heading downhill ever since people started introducing typing outside of the actual "typing" module. We could have kept it as a module-only thing and it would have made it entirely optional feature that you could easily introduce or remove depending on Python version you were targeting. Now, you have types all over and new ways to type random things keep popping up. And of course, that motivated people to propose more stuff like this. Just utter meaningless "feature", making this language even more convoluted than it already is ever since Guido sold out to Microsoft. Yes, I get it, it's his language and he can do whatever he wants with it. But a lot of people contributed to Python over the decades. And not a lot of them agree on where it is heading. A sad thing, really. EDIT: Downvote me to oblivion. Someone had to say it.
- throwmeout123 3y agoExcellent. Python is becoming horrible and losing many of the good qualities it had to cater to a crowd of people that would be better served by java and c# Popularity is a curse, quality goes downhill superfast
- qsort 3y agoSince you're asking, I did downvote this comment. My reasoning is that it is factually dubious and unrelated to the topic. The typing features are optional, so much so that CPython by default doesn't do anything with them. >>> def f(x: int): ... return 2 * x ... >>> f(1) 2 >>> f("hello") 'hellohello' It's also a weird claim that typing is unpopular, since it's almost universally considered to be a Good Thing™. The rest of the comment honestly sounds like a rant. Python has had governance independent of Guido for some time now, I don't really get the point.
- wheelerof4te 3y ago> The typing features are optional, so much so that CPython by default doesn't do anything with them. That is true...for now. But let us look at these two PEPs, only: https://peps.python.org/pep-0585/s https://peps.python.org/pep-0585/s and https://peps.python.org/pep-0604/ https://peps.python.org/pep-0604/ PEP 585 added new syntax for built-in collection types that doesn't work with older versions of Python. PEP 604 also added new Union and Optional typing syntax that doesn't work with older versions of Python, either. So, you will effectively break your code if you try adding any of these. In a large enough codebase, there could be dozens of these annoying things. Why do that when you could have offloaded every single typing annotation to it's separate module (conveniently called typing), and add a simple shortcut to an IDE to have types removed/re-introduced automatically. There are even more built-in ways to type your arguments now. So, let's see how long your argument stands.
- smitty1e 3y agoIt seems to me (waves hands) that this could be implemented as a decorator. Why not jack around and find out if this makes sense? Even if good for one use-case, it may not generalize sufficiently to make core changes.
- nedbat 3y agoHey everyone, this was my proposal. Don't take it too seriously. I tossed it off on Mastodon, it's not a PEP or anything. Python isn't going downhill because of me!
- ilyash 3y agoThoughts: Closest implemented thing that I know of that implements the solution to the stated problem: Raku's Capture - https://docs.raku.org/type/Capture https://docs.raku.org/type/Capture Maybe Python can go in that direction. Subjectively, it doesn't feel right to mix positional and named containers in Python.
- TrianguloY 3y agoI'm not sure if people realize that python allows dictionaries to have integer keys. This is syntactically valid python code: def sum(a, b): return a + b d={0:5, 1:9} sum(**d) But it throws a TypeError: sum() keywords must be strings. Also, it's interesting to see a lot of people saying that this is too "black magic" for python, when python is probably one of the languages with the most black magic I've seen. In fact, I'm sure that with maybe just a single call to a helper function you can simulate this new feature thanks to the python black magic...
- TrianguloY 3y agoCan someone explain to me why dictionaries being ordered or not matter here? It does not! The integer key represents the argument you want to assign the variable to. And what if you specify an argument with an integer key but also with the corresponding string key? Guess what, python allows this (for example f(1, *[2], **{"v":3}), and it throws a TypeError: sum() got multiple values for argument 'a'