简体   繁体   中英

python unpacking arg list with *args and **kwargs along with other positional and keyword only params

I have this following function defined in a file named kwargs.py. (changing the module to unpacking.py made the function work as expected). See the image at the bottom of the question.

def tag(name, *content, cls=None, **attrs):
    if cls is not None:
        attrs['class'] = cls
    
    if attrs:
        attr_str = ''.join(' %s="%s"' % (key,val) 
                                         for key, val 
                                         in sorted(attrs.items()))
    else:
        attr_str = ''
    
    if content:
        return '\n'.join('<%s%s>%s</%s>' % (name,attr_str,c,name) for c in content)
    else:
        return '<%s%s />' % (name,attr_str)

when executing this

tag('div', 'testing', cls='test', **dict(id=33,alt='this is a test'))

The result i get is

'<div alt="this is a test" class="test" id="33">testing</div>'

But when i execute this

tag(**dict(name='div', content=('testing','and testing'), cls='test', id=33, alt='this is a test'))

i only get

'<div alt="this is a test" class="test" id="33" />'

why is the parameter name getting assigned but not content . (Even if the tuple is not unpacked i was expecting at least the tuple itself be assigned to content[0]).

What am i missing here?

Edit: Python 3.8.3 x86 in Windows 10 在此处输入图像描述

*content is not an argument. You can't assign anything to it.

The docs indeed don't explain it explicitly. It's just a variable that you can use within your function body. What it does is "it scoops up all remaining input arguments".

Normally, these variadic arguments will be last in the list of formal parameters, because they scoop up all remaining input arguments that are passed to the function.

https://docs.python.org/3/tutorial/controlflow.html#arbitrary-argument-lists

This function

def func(*args, **kwargs):
    print("args", args)
    print("kwargs", kwargs)


func(args=[1, 2, 3])

will print

args ()
kwargs {'args': [1, 2, 3]}

EDIT:

Your examples are wrong. You can't get a different output.

For this function

def tag(name, *content, cls=None, **attrs):
    if cls is not None:
        attrs['class'] = cls
    if attrs:
        attr_str = ''.join(' %s="%s"' % (key, val)
                           for key, val
                           in sorted(attrs.items()))
    else:
        attr_str = ''

    if content:
        resp = '\n'.join('<%s%s>%s</%s>' % (name, attr_str, c, name) for c in content)
    else:
        resp = '<%s%s />' % (name, attr_str)

    print("name", name)
    print("*content", content)
    print("cls", cls)
    print("attrs", attrs)
    print("attrs_str", attr_str)
    print("resp", resp)
    print('-'*10)
   
    return resp

If you run this

tag('div', 'testing', cls='test', **dict(id=33, alt='this is a test'))
tag(**dict(name='div', content=('testing','and testing'), cls='test', id=33, alt='this is a test'))

You get

name div
*content ('testing',)
cls test
attrs {'id': 33, 'alt': 'this is a test', 'class': 'test'}
attrs_str  alt="this is a test" class="test" id="33"
resp <div alt="this is a test" class="test" id="33">testing</div>
----------
name div
*content ()
cls test
attrs {'content': ('testing', 'and testing'), 'id': 33, 'alt': 'this is a test', 'class': 'test'}
attrs_str  alt="this is a test" class="test" content="('testing', 'and testing')" id="33"
resp <div alt="this is a test" class="test" content="('testing', 'and testing')" id="33" />
----------

So you shouldn't get '<div alt="this is a test" class="test" id="33" />' for the second case. I get <div alt="this is a test" class="test" content="('testing', 'and testing')" id="33" /> for the exactly same function.

EDIT 2: Probably your namespace is broken as name **kwargs is often used somewhere else in a way you use **attrs here so changing this module name / importing function directly should solve your issue.

First let's make sure we are using the same terminology:

  • A function declares parameters ( name, *content, cls=None, **attrs in your example),
  • A function call receives arguments (eg 'div', 'testing', cls='test', **dict(id=33, alt='this is a test') ).

The arguments are then bound to the parameters, so they can be accessed in the function body.

First let's look at the function signature:

def tag(name, *content, cls=None, **attrs):
    ...

This function defines four parameters:

  • name - a positional-or-keyword parameter,
  • content - a varargs-parameter which captures any number of additional positional arguments,
  • cls - a keyword-only parameter with a default value,
  • attrs - a keywords-parameter which captures any number of additional keyword arguments.

When you call this function in the following way this is what happens:

tag('div', 'testing', cls='test', **dict(id=33, alt='this is a test'))
  • 'div' is bound to name ,
  • 'testing' is captured by content resulting in a 1-tuple,
  • 'test' is bound to cls ,
  • id=33, alt='this is a test' are captured by attrs .

Now the special thing about the *content and **attrs parameters is that they capture any number of excess arguments but they can't be bound to directly. Ie you can't bind content=(1, 2) . Instead if you pass tag('foo', 1, 2) this binding happens automatically. So if you call the function in the following way:

tag(**dict(name='div', content=('testing', 'and testing'), cls='test', id=33, alt='this is a test'))

Then all arguments are provided by keyword and hence all except name and cls are captured by attrs . This is because *content only captures positional arguments.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM