Ред на оценка на позиционни и ключови аргументи

Помислете за този измислен* пример:

def count(one, two, three):
    print one
    print two
    print three

Три ще бъде числото, което ще преброите, и числото на броенето ще бъде три.

>>> x = [1, 2, 3]
>>> count(*map(int, x), three=x.pop())
1
2
3

Четири няма да броиш,

>>> x = [1, 2, 3, 4]
>>> count(*map(int, x), three=x.pop())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: count() got multiple values for keyword argument 'three'

нито брои две, освен че след това продължиш към три.

>>> x = [1, 2]
>>> count(*map(int, x), three=x.pop())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: count() takes exactly 3 arguments (2 given)

Пет е точно.

>>> x = [1, 2, 3, 4, 5]
>>> count(*map(int, x), three=x.pop())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: count() takes exactly 3 arguments (5 given)

След като прочетох този въпрос, всъщност бих си помислил, че x = [1, 2] е единственият, който работи, т.к.

  • първо, map(int, x) ще бъде оценено, one зададено на 1 и two зададено на 2
  • тогава x все още [1, 2], x.pop() ще бъде оценено и three също ще бъде зададено на 2.

Очаквах за x = [1, 2, 3] да получа грешката, която всъщност видях за x = [1, 2, 3, 4].

какво става тук Защо аргументите изглежда не се оценяват отляво надясно? Оценяват ли се първо аргументите на ключовите думи?


*всъщност истинският ми код съответства на x = [1, 2, 3], което работи, но не бях сигурен, че е безопасно и след като прочетох другия въпрос, реших, че всъщност не би трябвало да работи.

Използвам Python 2.7, ако това има значение.


person mkrieger1    schedule 20.10.2015    source източник
comment
Изваждате елемент, така че оставате с два аргумента в първия пример   -  person Padraic Cunningham    schedule 20.10.2015
comment
Но защо pop преди map?   -  person mkrieger1    schedule 20.10.2015
comment
Поради причини. Честно казано, тук няма очевидно очаквано поведение. Ако някое от тях проработи, бих го считал за пълен късмет и не бих разчитал на него. Пишете ясно и избягвайте мутация, където е възможно, в противен случай е много трудно да разсъждавате какво трябва да се случи.   -  person Chad S.    schedule 20.10.2015
comment
Изявлението и примерът след него под подробностите за изпълнение на CPython изглеждат подходящи тук: Език на Python Справка » Изрази » Извиквания: въпреки че синтаксисът *expression може да се появи след някои аргументи на ключовата дума, той се обработва преди аргументите на ключовата дума   -  person Lukas Graf    schedule 20.10.2015


Отговори (1)


Python 2.7

Ако разгледаме източника на CPython, свързан със създаването на AST(ast_for_call ) за извикване на функция редът на оценката на аргумента се оказва:

return Call(func, args, keywords, vararg, kwarg, func->lineno,
                func->col_offset, c->c_arena);

т.е. args --› ключови думи --› vararg --› kwarg

И така, във вашия случай аргументът на ключовата дума се оценява първо и след това се оценява базираният на звезда израз (vararg).

Байт код:

>>> dis.dis(lambda: func(1, 2, *('k', 'j', 'l'), z=1, y =2, three=x.pop(), **{kwarg:1}))
  1           0 LOAD_GLOBAL              0 (func)
              3 LOAD_CONST               1 (1)         # arg
              6 LOAD_CONST               2 (2)         # arg
              9 LOAD_CONST               3 ('z')       # keyword
             12 LOAD_CONST               1 (1)
             15 LOAD_CONST               4 ('y')       # keyword
             18 LOAD_CONST               2 (2)
             21 LOAD_CONST               5 ('three')   # keyword
             24 LOAD_GLOBAL              1 (x)
             27 LOAD_ATTR                2 (pop)
             30 CALL_FUNCTION            0
             33 LOAD_CONST               9 (('k', 'j', 'l')) #vararg
             36 BUILD_MAP                1
             39 LOAD_CONST               1 (1)
             42 LOAD_GLOBAL              3 (kwarg)     #kwarg
             45 STORE_MAP
             46 CALL_FUNCTION_V

Следователно във вашия случай първо ще се случи извикването pop(), последвано от оценката varargs.

Така че, ако three е част от kwargs, тогава ще получим грешка с map:

>>> x = [1, 2, 3]
>>> count(*map(float, x), **{'three': x.pop()})
Traceback (most recent call last):
  File "<ipython-input-133-e8831565af13>", line 1, in <module>
    count(*map(float, x), **{'three': x.pop()})
TypeError: count() got multiple values for keyword argument 'three'

Ще работи, ако го направим *мързеливо:

>>> x = [1, 2, 3]
>>> count(*(float(y) for y in x), **{'three': x.pop()})
1.0, 2.0, 3

*Причината, поради която генераторът работи и map или разбирането на списъка е неуспешно, е обяснена в края.


Python 3.5

Функцията ast_for_call тук поддържа само два списъка: args и keywords.

Тук varargs се вмъкват в списъка с аргументи и kwargs отиват в списъка keywords. И така, в крайна сметка обаждането изглежда така:

return Call(func, args, keywords, func->lineno, func->col_offset, c->c_arena);

Байт код:

>>> dis.dis(lambda: func(1, 2, *('k', 'j', 'l'), z=1, y =2, three=x.pop(), **{kwarg:1}))
  1           0 LOAD_GLOBAL              0 (func)
              3 LOAD_CONST               1 (1)
              6 LOAD_CONST               2 (2)
              9 LOAD_CONST               9 (('k', 'j', 'l'))
             12 LOAD_CONST               6 ('z')
             15 LOAD_CONST               1 (1)
             18 LOAD_CONST               7 ('y')
             21 LOAD_CONST               2 (2)
             24 LOAD_CONST               8 ('three')
             27 LOAD_GLOBAL              1 (x)
             30 LOAD_ATTR                2 (pop)
             33 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
             36 LOAD_GLOBAL              3 (kwarg)
             39 LOAD_CONST               1 (1)
             42 BUILD_MAP                1
             45 CALL_FUNCTION_VAR_KW   770 (2 positional, 3 keyword pair)
             48 RETURN_VALUE

Сега нещата могат да станат малко вълнуващи, ако изразът, който дава varargs, е мързелив:

>> def count(one, two, three):
        print (one, two, three)
...
>>> x = [1, 2, 3]
>>> count(*map(float, x), three=x.pop())  # map is lazy in Python 3
1.0 2.0 3
>>> x = [1, 2, 3]
>>> count(*[float(y) for y in x], three=x.pop())
Traceback (most recent call last):
  File "<ipython-input-25-b7ef8034ef4e>", line 1, in <module>
    count(*[float(y) for y in x], three=x.pop())
TypeError: count() got multiple values for argument 'three'

Байт код:

>>> dis.dis(lambda: count(*map(float, x), three=x.pop()))
  1           0 LOAD_GLOBAL              0 (count)
              3 LOAD_GLOBAL              1 (map)
              6 LOAD_GLOBAL              2 (float)
              9 LOAD_GLOBAL              3 (x)
             12 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             15 LOAD_CONST               1 ('three')
             18 LOAD_GLOBAL              3 (x)
             21 LOAD_ATTR                4 (pop)
             24 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
             27 CALL_FUNCTION_VAR      256 (0 positional, 1 keyword pair)
             30 RETURN_VALUE
>>> dis.dis(lambda: count(*[float(y) for y in x], three=x.pop()))
  1           0 LOAD_GLOBAL              0 (count)
              3 LOAD_CONST               1 (<code object <listcomp> at 0x103b63930, file "<ipython-input-28-1cc782164f20>", line 1>)
              6 LOAD_CONST               2 ('<lambda>.<locals>.<listcomp>')
              9 MAKE_FUNCTION            0
             12 LOAD_GLOBAL              1 (x)
             15 GET_ITER
             16 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             19 LOAD_CONST               3 ('three')
             22 LOAD_GLOBAL              1 (x)
             25 LOAD_ATTR                2 (pop)
             28 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
             31 CALL_FUNCTION_VAR      256 (0 positional, 1 keyword pair)
             34 RETURN_VALUE

Мързеливото извикване работи, защото разопаковането (известно още като действителна оценка на генератора) не се случва, докато функцията не бъде действително извикана, следователно в този случай pop() извикването ще премахне първо 3 и след това по-късно на картата ще премине само 1, 2.

Но в случай на разбиране на списък списъкът обект вече съдържа 3 елемента и след това, въпреки че pop() премахна 3 по-късно, ние все още предаваме две стойности за третия аргумент.

person Ashwini Chaudhary    schedule 20.10.2015
comment
Благодаря за подробното вникване! И така, за да обобщим: случва се да работи в CPython 2.7, но не като цяло в 3.5, освен когато се използва map; но това е детайл от изпълнението и не може да се разчита? - person mkrieger1; 21.10.2015
comment
@mkrieger1 Това не е детайл на изпълнението, а детайл на версията, Python 2 и Python 3 обработват нещата по различен начин. Но при различна реализация изходът трябва да съвпада, например тествах на PyPy 2.7.9 и той работи по същия начин като CPython 2.7.10. Нещата също се промениха между Python 3.4 и 3.5, например сега нещо като count(*(1,), *(2, 3)) е валидно (поради PEP-448) в Python 3.5, но не и в по-стари версии на Python 3.x. - person Ashwini Chaudhary; 21.10.2015