Почему в параметр функции идет только последний индекс?
Используется библиотека Pystray. Я пытаюсь собрать массив из кнопок, каждая из которых должна передавать индекс своего устройства в функцию, чтобы установить это устройством по умолчанию, но по неведомым причинам при нажатии на любую кнопку приходит только последний индекс. В моем случае индексы микрофонов - 4 5 6, и независимо от того, какую кнопку я жму - выбирается индекс 6.
menu_items = []
for device in only_recording_devices:
index = device[0][-1]
is_default = device[1].split('Default : ')[1]
name = device[4].split('Name : ')[1]
menu_items.append(MenuItem(f'{index} - {name}',
lambda _: set_default_microphone(index),
radio=True
))
Ответы (3 шт):
Это называется "замыкание переменной". В тот момент, когда у вас вызывается любая lambda
, значение index
в ней оказывается последним из присвоенных в эту переменную. Потому что в lambda
передаётся адрес переменной, а не её значение. Чтобы значение правильно "запомнилось", используйте форму записи лямбды с параметром. Выглядит она странно, но должна сработать. В этом случае значение переменной index
"запомнится" в параметр лямбды и при вызове этой лямбды будет использовано правильное значение переменной (потому что это будет уже внутренняя переменная лямбды index
, а не та внешняя переменная index
, на которую ссылалась ваша лямбда, хотя название переменной казалось бы одно и то же).
lambda _,index=index: set_default_microphone(index)
Update: Этой лямбде нужен ещё один параметр _
, который вы не используете, но он нужен поскольку это обработчик события. У вас он был, а я его сначала убрал, а теперь добавил обратно.
Update2: Добавил код, иллюстрирующий проблему. Переменная на каждой итерации разная, как и пишет в комментарии Grundy, но лямбда запоминает только последнюю. До конца объяснить почему так не могу.
Код, демонстрирующий проблему и проверяющий id
переменной:
lst = []
for i in range(5):
index = i + 1
print(index, id(index))
lst.append(lambda _: print(index, id(index)))
for l in lst:
l(None)
Вывод:
1 1605469667568
2 1605469667600
3 1605469667632
4 1605469667664
5 1605469667696
5 1605469667696
5 1605469667696
5 1605469667696
5 1605469667696
5 1605469667696
Исправленный код:
lst = []
for i in range(5):
index = i + 1
print(index, id(index))
lst.append(lambda _, index=index: print(index, id(index)))
for l in lst:
l(None)
Вывод:
1 1605469667568
2 1605469667600
3 1605469667632
4 1605469667664
5 1605469667696
1 1605469667568
2 1605469667600
3 1605469667632
4 1605469667664
5 1605469667696
Скорее всего Питон в лямбде запоминает даже не ссылку на переменную, а просто её название.
Код, который мы будем препарировать. Его можно запустить из консоли и посмотреть что получится:
class MenuItem:
def __init__(self, name, callback):
self.name = name
self._callback = callback
def click(self):
# print(self._callback.__closure__)
self._callback(self)
def set_default_microphone(index):
print('click', index)
def create_menu():
menu_items = []
for device in only_recording_devices:
index, name = device
menu_items.append(MenuItem(
f'{index} - {name}',
lambda _: set_default_microphone(index)
))
# index = 42
return menu_items
only_recording_devices = (4, 'NVIDIA'), (5, 'PANTEON'), (6, 'Ritmix')
for item in create_menu():
item.click()
Все вызовы выдали шестёрку:
$ python callbacks.py click 6 click 6 click 6
Разбираемся что делает выражение lambda _: set_default_microphone(index)
. Это функция с одним параметром, который игнорируется (_
). В теле два имени, которые в теле же не определены. Откуда они берутся? Лямбда определена внутри функции create_menu
, компилятор ищет имена там и находит index
. Чтобы найти set_default_microphone
надо сделать ещё шаг. Функция create_menu
определена в модуле (то есть, она не вложена ни в какую другую), компилятор просматривает все имена определённые в модуле и находит set_default_microphone
. В итоге у нас лямбда с двумя нелокальными именами.
Чего хотел программист? Лямбда определяется три раза, программист хочет получить три различные анонимные функции, каждую с своим значением index
. То есть:
... lambda _: set_default_microphone(4) ...
... lambda _: set_default_microphone(5) ...
... lambda _: set_default_microphone(6) ...
Но компилятор действует по другому. В всех лямбдах сохраняется ссылка на переменную index
и код вызывает set_default_microphone
со значением index
на момент вызова. Так как вызов делается после завершения цикла, то все видят последнее значение – шестёрку.
Попробуйте раскомментировать index = 42
и везде будет напечатано 42.
Ещё раз: в вызов подставляется не текущее значение index
, а ссылка на переменную. Если поменять эту переменную позже, изменения будут видны в вызовах лямбд. Обратите внимание что к тому времени как лямбды будут вызваны, функция create_menu
завершила свою работу и её локальные переменные перестали существовать. Мы разговариваем с духом покойной локальной переменной! Чтобы это стало возможно, компилятор помещает переменную index
в замыкание. И это замыкание хранит index
до тех пор пока есть лямбды, которые компилировались пока переменная была жива.
Раскомментируйте # print(self._callback.__closure__)
. Если функция ссылается на локальные переменные другой функции, то есть, если у функции есть замыкание, атрибут __closure__
хранит его. Вот что будет на печати:
python callbacks.py (<cell at 0x7f044430ffd0: int object at 0x7f0445650610>,) click 42 (<cell at 0x7f044430ffd0: int object at 0x7f0445650610>,) click 42 (<cell at 0x7f044430ffd0: int object at 0x7f0445650610>,) click 42
Замыкание – кортеж. В нашем случае в нём одна ячейка. Все лямбды используют одну и ту же ячейку и, конечно, они будут всегда видеть одно и то же значение одной и той же переменной.
Всё! Конец истории! Нельзя в цикле создать несколько лямбд с разными значениями. Одна переменная index
, одно значение, одно поведение.
Всё упирается в единственную переменную index
, а нам надо несколько разных. А когда в Питоне рождаются переменные? Когда вызывается функция! Сделаем функцию make_callback
, внутри неё создадим лямбду и вернём её наружу:
...
def make_callback(index):
return lambda _: set_default_microphone(index)
def create_menu():
menu_items = []
for device in only_recording_devices:
index, name = device
menu_items.append(MenuItem(
f'{index} - {name}',
make_callback(index)
))
index = 42
return menu_items
...
Разные лямбды получили в пользование разные ячейки в замыканиях, ссылающиеся на разные переменные, и значения этих переменных какие надо:
python callbacks.py (<cell at 0x7f01d0f77c40: int object at 0x7f01d22c0150>,) click 4 (<cell at 0x7f01d0f77820: int object at 0x7f01d22c0170>,) click 5 (<cell at 0x7f01d0f77790: int object at 0x7f01d22c0190>,) click 6
Это конечно громоздко, но функция make_...
позволяет точно указывать что лямбда будет видеть в замыкании, какие выражения должны быть вычислены до создания лямбды, какие вычислятся при вызове лямбды.
Почему в замыканиях запоминаются переменные, а не их значения? Это воля авторов языка. Можно было бы запоминать значения и тогда не нужно городить промежуточную функцию, что сделало бы вашу жизнь проще. Но в большинстве случаев переменные в замыканиях полезнее. Например, вы хотите часть кода большой функции поместить в локальную функцию. Замыкания с переменными позволяют делать такой рефакторинг почти (но не совсем) автоматически. Замыкания со значениями привели бы к серьёзному переписыванию кода.
Я отвлёкся. Как сделать код не таким громоздким?. Во-первых, make_...
можно поместить внутрь create_menu
:
def create_menu():
def make_callback(index):
return lambda _: set_default_microphone(index)
menu_items = []
...
Во-вторых, make_...
можно сделать лямбдой. Хотя правила хорошего тона запрещают именованные лямбды:
def create_menu():
make_callback = lambda index: lambda _: set_default_microphone(index)
menu_items = []
...
Привыкайте к lambda
-lambda
. Это часто встречается.
В-третьих, лямбду, которая make_...
, можно определить и вызвать прямо в выражении. Я такое не люблю, но если вы понимаете что, как и зачем, вы – прожжённый функциональщик:
def create_menu():
menu_items = []
for device in only_recording_devices:
index, name = device
menu_items.append(MenuItem(
f'{index} - {name}',
(lambda index: lambda _: set_default_microphone(index))(index)
))
index = 42
return menu_items
В-четвёртых, есть приём с значением аргументов по-умолчанию. Я его не люблю, но он очень популярен. Идея в том, что значение аргумента по-умолчанию вычисляется в момент определения функции. Выражение index=index
упоминает две разные переменные. Слева параметр нашей лямбды, справа локальная переменная из функции create_menu
. Значение параметра по-умолчанию создаётся на каждом вызове и мы получаем много разных переменныx index
, что нам и нужно для разного поведения разных лямбд. Более того, наша лямбда не имеет замыкания вовсе! В теле лямбды index
- параметр, локальная переменная самой лямбды.
def create_menu():
menu_items = []
for device in only_recording_devices:
index, name = device
menu_items.append(MenuItem(
f'{index} - {name}',
lambda _, index=index: set_default_microphone(index)
))
index = 42
return menu_items
Почему я тогда не люблю этот приём?
- Потому что это сложно объяснить: выражение одно, но вы должны хорошо понимать какие его части когда выполняются. Это же относится и к предыдущему примеру.
- Такую лямбду можно нечаянно вызвать с двумя параметрами и интерпретатор вам ничего не скажет, молча подставит лишнее значение в параметр
index
и будет вам отладка.
В общем, я не люблю, когда ради краткости кода тут, вы подкладываете мину под код там.
Для передачи параметров в таких замыканиях есть в питоне специальная функция.
import functools
def set_default_microphone(index, event):
print('click', index)
...
menu_items.append(MenuItem(f"{index} - {name}", functools.partial( set_default_microphone, index),
radio=True))
Эта функция часто используется именно для целей сохранения аргумента в колбэке.
Работает почти также как make_callback в соседнем ответе, но не использует лямбды.
Добавил event чтоб поймать в него что пришлет обработчик клика. В вопросе это _
(как по мне лучше переменную назвать, чем потом вспоминать что за мусор там.)