Oh, that's quite different than mapping patterns in PEP 634. :-(

On Thu, Oct 22, 2020 at 8:28 PM Steven D'Aprano <steve@pearwood.info> wrote:
I would like to propose dict (or mapping) unpacking assignment. This is
inspired in part by the Python-Ideas thread "f-strings as assignment
targets", dict unpacking in function calls, and iterable unpacking
assignment.

Comments welcome.


Background
----------

Iterable unpacking assignment:

    values = (1, 2, 3)
    a, b, c = *values

is a very successful and powerful technique in Python. Likewise dict
unpacking in function calls and dict displays:

    kwargs = {'a': 1, 'b': 2}
    func(**kwargs)
    d = {**kwargs, 'c': 3}


There have been various requests for allowing dict unpacking on the
right-hand side of an assignment [citation required] but in my opinion
none of them have had a good justification.

Motivated by the idea of scanning text strings with a scanf-style
function, I propose the following behaviour for dict unpacking
assignment:

    items = {'eggs': 2, 'cheese': 3, 'spam': 1}
    spam, eggs, cheese = **items
    assert spam == 1 and eggs == 2 and cheese == 3


Syntax
------

    target_list [, **target] = **expression

`target_list` is a comma-separated list of targets. Targets may be:


- simple names, e.g. `spam` and `eggs`

- dotted names, e.g. `spam.eggs`

- numbered subscripts, e.g. `spam[1]`


but is not required to support arbitrary complex targets such as:

    spam(*args).eggs(2*x + y)[1].cheese  # not supported

Likewise only int literals are supported for subscripts. (These
restrictions may be lifted.)

This is similar to the limited range of fields acceptabled by the string
format mini-language. The same restrictions apply to `**target`.

Each target must be unique.

`expression` must evaluate to a dict or other mapping.

Assignment proceeds by matching up targets from the left to keys on the
right:

1. Every target must be matched exactly by a key. If there is a target
without a corresponding key, that is an error.

2. Any key which does not match up to a target is an error, unless a
`**target` is given.

3. If `**target` is given, it will collect any excess key:value pairs
remaining into a dict.

4. If the targets and keys match up, then the bindings are applied from
left to right, binding the target to the value associated with that key.

Examples:


    # Targets are not unique
    a, b, a = **items
    => SyntaxError

    # Too many targets
    a, b, c = **{'a': 1, 'b': 2}
    => raises a runtime exception

    # Too few targets
    a = **{'a': 1, 'b': 2}
    => raises a runtime exception

    a, **extras = **{'a': 1, 'b': 2}
    assert a == 1
    assert extras == {'b': 2}

    # Equal targets and keys
    a, b, **extras = **{'a': 1, 'b': 2}
    assert a == 1
    assert b == 2
    assert extras == {}

    # Dotted names
    from types import SimpleNamespace
    obj = SimpleNamespace()
    obj.spam = **{'obj.spam': 1}
    assert obj.spam == 1

    # Subscripts
    arr = [None]*5
    arr[1], arr[3] = **{'arr[3]': 33, 'arr[1]': 11}
    assert arr == [None, 11, None, 33, None]


Assignments to dotted names or subscripts may fail, in which case the
assignment may only partially succeed:


    spam = 'something'
    eggs = None
    spam, eggs.attr = {'spam': 1, 'eggs.attr': 2}
    # raises AttributeError: 'NoneType' object has no attribute 'attr'
    # but `spam` may have already been bound to 1


(I think that this is undesirable but unavoidable.)


Motivating use-cases
--------------------

The motivation comes from the discussion for scanf-like functionality.
The addition of dict unpacking assignment would allow something like
this:


    pattern = "I'll have {main} and {extra} with {colour} coffee."
    string = "I'll have spam and eggs with black coffee."
    main, extra, colour = **scanf(pattern, string)

    assert main == 'spam'
    assert extra == 'eggs'
    assert colour == 'black'


But the possibilities are not restricted to string scanning. This will
allow functions that return multiple values to choose between returning
them by position or by name:


    height, width = get_dimensions(window)  # returns a tuple
    height, width = **get_dimensions(window)  # returns a mapping

Developers can choose whichever model best suits their API.

Another use-case is dealing with kwargs inside functions and methods:


    def method(self, **kwargs):
        spam, eggs, **kw = **kwargs
        process(spam, eggs)
        super().method(**kw)


--
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/FJYAE6P5263G3MQMVQ5IUCXUNAUJYQAT/
Code of Conduct: http://python.org/psf/codeofconduct/


--
--Guido van Rossum (python.org/~guido)
Pronouns: he/him (why is my pronoun here?)