Lambda на службе оконных приложений

Сегодня я хочу поделиться одним небольшим как это сейчас принято говорить лайфхаком. Речь пойдёт об использовании lambda-функций в приложениях Qt и PyQt.
Итак, как учит нас семья, школа и википедия,
Анонимная (безымянная) функция— в программировании особый вид функций, которые объявляются в месте использования и не получают уникального идентификатора для доступа к ним. Обычно при создании они либо вызываются напрямую, либо ссылка на функцию присваивается переменной, с помощью которой затем можно косвенно вызывать данную функцию.Wikipedia
В python для определения анонимных функций (которые все называют просто лямбда-функциями) используется ключевое слово lambda и синтаксис простейших lambda-функций выглядит так:
1 2 3 4 5 |
>>> foo = lambda x: x**x >>> foo(3) >>> 27 |
Или можно написать вот так:
1 2 3 4 5 6 7 8 9 |
>>> foo = lambda x: x**2 if x %2 == 0 else x**3 >>> foo(3) >>> 27 >>> foo(4) >>> 16 |
или даже так:
1 2 3 4 5 |
>>> foo = lambda x: [t**2 for t in range(x) if not t%2 == 0] >>> foo(7) >>> [1, 9, 25] |
Все анонимные функции в python могут принимать несколько аргументов, поддерживают рекурсию и могут быть вызваны непосредственно сразу после декларирования:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
>>> foo = lambda x, y: x**y if x%2 == 0 else y/x >>> foo(2, 3) >>> 8 >>> foo(3, 7) >>> 2,3333333333335 >>> f = lambda x: x+f(x-1) if x > 2 else 2 >>> f(5) >>> 14 >>> (lambda a, b: True if b in a else False)("string", "in") >>> True |
Но хватит об анонимных функциях — наша цель не в том, чтобы изучить тонкости их использования, а в том, чтобы понять как использовать их в оконных приложениях.
Основным моим инструментом разработки оконных приложений является Qt и его привязка для python PyQt. Есть много причин, почему именно эту библиотеку я использую, а не другую, но об этом как-нибудь в другой раз.
Для того, чтобы понять как и где можно использовать lambda в PyQt (начну я именно с него), я написал простенькое приложение (python 3.4, PyQt5):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#! /usr/bin/python # -*- coding: utf-8 -*- import sys from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5.QtWidgets import * class Window(QWidget): def __init__(self): super(Window, self).__init__() self.layout = QGridLayout() self.checkbox = QCheckBox("Я управляю состоянием кнопок", self) self.push1 = QPushButton("Я кнопка, вызывающая QMessageBox", self) self.push2 = QPushButton("Я буду увеличивать значение SpinBox", self) self.push3 = QPushButton("Я буду уменьшать значение SpinBox", self) self.spin = QSpinBox(self) self.slider = QSlider(Qt.Horizontal, self) self.spin.setRange(0, 25) self.spin.setValue(0) self.slider.setRange(0, 25) self.line = QLineEdit(self) self.line.setReadOnly(True) self.checkbox.setChecked(True) self.layout.addWidget(self.checkbox, 0, 0, 1, 2) self.layout.addWidget(self.push1, 0, 2) self.layout.addWidget(self.push2, 0, 3) self.layout.addWidget(self.push3, 0, 4) self.layout.addWidget(self.spin, 1,0) self.layout.addWidget(self.slider, 1, 1, 1, 3) self.layout.addWidget(self.line, 1, 4) self.setLayout(self.layout) self.setWindowTitle("Lambdas") def main(): application = QApplication(sys.argv) window = Window() window.show() sys.exit(application.exec()) if __name__ == "__main__": main() |
Это самое простое приложение, смысл которого только в одном — показать возможность использования анонимных функций. У нас есть форма с чекбоксом, тремя кнопками, спинбоксом, слайдером и текстовым полем. Допустим, нам необходимо сделать следующее:
- Если пользователь снимает галочку с нашего чекбокса, то кнопки становятся некликабельны, если же наоборот, пользователь ставит галочку, то кнопки становятся доступными.
- Простая связь между спинбоксом и слайдером — изменяя значение спинбокса, автоматически изменяется значение слайдера и наоборот.
- Если значение спинбокса (или слайдера) становится больше 10 в текстовом поле выводится уведомление, если меньше 10, то уведомление исчезает.
- Клик по кнопке «Я буду увеличивать значение SpinBox» увеличит значение спинбокса (и слайдера) на 1, клик по кнопке «Я буду уменьшать значение SpinBox» соответственно уменьшает значение на 1 (естественно уведомление также будет появляться при значении больше десяти и исчезать, если значение меньше 10).
- Ну и наконец, кликая по кнопке «Я кнопка, вызывающая QMessageBox» будет появляться QMessageBox, причём, если в текстовом поле есть какое-то значение, то будет появляться уведомление об ошибке, если же текстовое поле пусто, то мы увидим информационное сообщение со значением спинбокса (слайдера).
Конечно, этот пример несколько надуманный, но он довольно неплохо показывает возможности анонимных функций.
Давайте для начала реализуем наши фантазии классическим образом. Во первых, свяжем сигналы со слотами:
1 2 3 4 5 6 |
self.checkbox.stateChanged.connect(self.onCheckBoxStateChanged) self.spin.valueChanged.connect(self.onSpinBoxValueChanged) self.slider.valueChanged.connect(self.onSliderValueChanged) self.push1.clicked.connect(self.onPush1Clicked) self.push2.clicked.connect(self.onPush2Clicked) self.push3.clicked.connect(self.onPush3Clicked) |
А затем реализуем слоты:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
def onCheckBoxStateChanged(self): if not self.checkbox.isChecked(): self.push1.setDisabled(True) self.push2.setDisabled(True) self.push3.setDisabled(True) else: self.push1.setEnabled(True) self.push2.setEnabled(True) self.push3.setEnabled(True) def onSpinBoxValueChanged(self, value): self.slider.setValue(value) if value > 10: self.line.setText("Warning!") else: self.line.setText("") def onSliderValueChanged(self, value): self.spin.setValue(value) if value > 10: self.line.setText("Warning!") else: self.line.setText("") def onPush1Clicked(self): if self.line.text() == "": QMessageBox.information(self, "Information", str(self.spin.value())) else: QMessageBox.critical(self, "Error", self.line.text()) def onPush2Clicked(self): self.spin.setValue(self.spin.value() + 1) def onPush3Clicked(self): self.spin.setValue(self.spin.value() - 1) |
В принципе, ничего сложного, однако некоторые наши методы (так бывает и в реальных приложениях) занимают всего-лишь одну строчку. Лично мне всегда обидно определять метод, который будет вызван только один раз и который состоит из какой-нибудь строки типа anotherWindow.show()
. Вот тут то нам lambda и поможет. Давайте смело закомментируем все наши слоты и вместо них определим анонимные функции. Начнём с push2 и push3 как с самых элементарных. Достаточно переписать connect вот так:
1 2 |
self.push2.clicked.connect(lambda: self.spin.setValue(self.spin.value() + 1)) self.push3.clicked.connect(lambda: self.spin.setValue(self.spin.value() - 1)) |
Функциональность совсем не изменится, а вот количество кода уменьшится. Следующая задача чуть сложнее — push1. Тут на помощь кроме лямбда-функций придут инлайновые выражения. С их помощью получится такая вот конструкция:
1 2 3 |
self.push1.clicked.connect(lambda: QMessageBox.information(self, "Information", str(self.spin.value())) if self.line.text() == "" else QMessageBox.critical(self, "Error", self.line.text())) |
Если раскомментировать методы onSpinValueChanged
и onSlideValueChanged
, то можно убедиться, что всё работает аналогично. Конечно, такая конструкция не блещет гармонией, более того, я практически убеждён, что это не pythonic way, но python хорош именно тем, что позволяет разрабатывать приложения крайне быстро, в режиме жёстких временных ограничений. Это во многом связано с лаконичностью языка (если вы мне не верите, то доказательство будет ниже, когда мы перейдём к C++), анонимные функции же позволяют сделать код ещё более лаконичным.
Ну и наконец, главный фокус (когда я его «открыл», помнится был очень горд собой) — анонимной функции можно передать несколько методов. Иллюстрируя сказанное,
1 2 3 4 5 6 |
self.spin.valueChanged.connect(lambda: (self.slider.setValue(self.spin.value()), self.line.setText("Warning") if self.spin.value() > 10 else self.line.setText(""))) self.slider.valueChanged.connect(lambda: [self.spin.setValue(self.slider.value()), self.line.setText("Warning") if self.slider.value() > 10 else self.line.setText("")]) |
Lambda может принять либо tuple, причём скобки обязательны, либо list. С помощью этой техники на самом деле можно создавать сколь угодно сложные анонимные слоты, но увлекаться этим сильно не советую — это не только вредит читабельности, но и может сказаться на производительности. Ну а мы заканчиваем погружение в PyQt. Последний connect также элементарен:
1 2 3 4 5 6 7 |
self.checkbox.stateChanged.connect(lambda: [[self.push1.setDisabled(True), self.push2.setDisabled(True), self.push3.setDisabled(True)] if not self.checkbox.isChecked() else [self.push1.setEnabled(True), self.push2.setEnabled(True), self.push3.setEnabled(True)]]) |
Я думаю, что синтаксис не требует пояснений.
В заключение этой части хочу ещё вас предостеречь от избыточного использования этой техники. Абсолютно нормально, когда вместо того, чтобы определять целый метод и связывать его с сигналом, вы пишете просто lambda: window.show()
. Но когда ваша лямбда занимает 10 строчек с бесконечными инлайновыми выражениями, вложенными друг в друга, это не совсем то, что нужно. Будьте сдержанны! 🙂 Мы же переходим ко второй части — анонимные функции в C++.
Лямбда-функции в C++ появились в стандарте C++11, то есть не так давно. В сети довольно много информации по этому стандарту и по лямбдам в частности. Я в своё время пользовался этой и этой статьями от Майкрософт.
Итак, синтаксис анонимной функции выглядит так: [capture] (parameters) {body}
или так [capture] (parameters)->return type {body}
. Давайте по порядку, в квадратных скобках идёт так называемое предложение фиксации. Предложение фиксации — это всего-лишь список переменных во внешней области, к которым лямбда будет иметь доступ. Предложение фиксации может быть пустым, если наша лямбда ничего не захватывает. Доступ к внешним переменным может осуществляться по ссылке (&), либо по значению переменной. В круглых скобках идут параметры — это локальные переменные. Конструкция ->return type
показывает тип возвращаемого значения, к примеру для int
нужно написать просто ->int
. Если лямбда ничего не возвращает, то конструкцию ->return type
можно пропустить. Ну и в фигурных скобках собственно само тело лямбда-функции. Кодом проще показать, что, где и как:
1 2 3 4 5 6 7 |
#include <iostream> using namespace std; int main() { auto f = [] (int x, int y) {return x*y;}; cout << f(3, 4) << endl; } |
Как видите, используется ключевое слово auto
, для того, чтобы автоматически выводить тип возвращаемого значения. Подробнее про auto
можно также прочитать у Майкрософт. Другой вариант, использовать std::function<type>
:
1 2 3 4 5 6 7 8 |
#include <iostream> #include <functional> using namespace std; int main() { function<int(int, int)> f = [] (int x, int y) {return x*y;}; cout << f(3, 4) << endl; } |
Этот пример аналогичен предыдущему, однако иногда он не имеет альтернатив. К примеру, рекурсивная лямбда не может быть определена через auto
(в C++14 такая возможность есть):
1 2 3 4 5 6 |
#include <iostream> #include <functional> int main() { std::function<int(int)> factorial = [&factorial] (int x) {return x > 0 ? factorial(x-1)*x : 1}; } |
Но в сторону теорию! Нас интересуют оконные интерфейсы, ими мы и займёмся. Для начала как и с python мы напишем простенькое приложение (небольшие различия в компоновке и названиях прошу списать на мою невнимательность и лень 🙂 ). Итак, window.h
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#ifndef WINDOW_H #define WINDOW_H #include <QWidget> #include <QCheckBox> #include <QPushButton> #include <QSpinBox> #include <QLineEdit> #include <QGridLayout> #include <QSlider> class Window : public QWidget { Q_OBJECT public: Window(QWidget *parent = 0); ~Window(); private: QGridLayout layout; QCheckBox *checkbox; QPushButton *push1, *push2, *push3; QSpinBox *spin; QSlider *slider; QLineEdit line; private slots: void onCheckBoxStateChanged(int state); void onPush1Clicked(bool); void onPush2Clicked(bool); void onPush3Clicked(bool); void onSpinBoxValueChanged(int value); void onSliderValueChanged(int value); }; #endif // WINDOW_H |
Вряд ли в этом файле есть что-то, что необходимо объяснить. Не менее прост и файл window.cpp
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
#include "window.h" #include <QMessageBox> Window::Window(QWidget *parent) : QWidget(parent) { checkbox = new QCheckBox("checkMe", this); push1 = new QPushButton("1", this); push2 = new QPushButton("2", this); push3 = new QPushButton("3", this); slider = new QSlider(Qt::Horizontal, this); slider->setRange(0, 25); spin = new QSpinBox(this); spin->setRange(0, 25); spin->setValue(0); line.setReadOnly(true); layout.addWidget(checkbox, 0, 0); layout.addWidget(push1, 0, 1); layout.addWidget(push2, 0, 2); layout.addWidget(push3, 0, 3); layout.addWidget(spin, 1, 0); layout.addWidget(slider, 1, 1); layout.addWidget(&line, 1, 2); this->setLayout(&layout); connect(checkbox, SIGNAL(stateChanged(int)), this, SLOT(onCheckBoxStateChanged(int))); connect(push1, SIGNAL(clicked(bool)), this, SLOT(onPush1Clicked(bool))); connect(push2, SIGNAL(clicked(bool)), this, SLOT(onPush2Clicked(bool))); connect(push3, SIGNAL(clicked(bool)), this, SLOT(onPush3Clicked(bool))); connect(spin, SIGNAL(valueChanged(int)), this, SLOT(onSpinBoxValueChanged(int))); connect(slider, SIGNAL(valueChanged(int)), this, SLOT(onSliderValueChanged(int))); checkbox->setChecked(true); } Window::~Window() { } void Window::onCheckBoxStateChanged(int state) { state == 0 ? (push1->setDisabled(true), push2->setDisabled(true), push3->setDisabled(true)) : (push1->setEnabled(true), push2->setEnabled(true), push3->setEnabled(true)); } void Window::onPush1Clicked(bool) { line.text() == "Warning!" ? QMessageBox::critical(this, "Error", line.text()) : QMessageBox::information(this, "Information", QString::number(spin->value())); } void Window::onPush2Clicked(bool) { spin->setValue(spin->value() + 1); } void Window::onPush3Clicked(bool) { spin->setValue(spin->value() - 1); } void Window::onSpinBoxValueChanged(int value) { slider->setValue(value); value > 10 ? line.setText("Warning!") : line.setText(""); } void Window::onSliderValueChanged(int value) { spin->setValue(value); value > 10 ? line.setText("Warning!") : line.setText(""); } |
Файл main.cpp
не имеет ни одного изменения, поэтому захламлять статью не буду, оно и так всё довольно сложно читаемо… Теперь наша задача стереть/закомментировать все коннекты и все методы в файлах window.cpp
и window.h
и переписать все коннекты через лямбды. И как всегда, всё не так просто!
Во-первых, если мы просто напишем что-то типа
1 |
connect(push1, SIGNAL(clicked(bool)), this, SLOT([](){какая-то лямбда})); |
или
1 |
connect(push1, SIGNAL(clicked(bool)), this, [](){какая-то лямбда}); |
или
1 |
connect(push1, SIGNAL(clicked(bool)), [](){какая-то лямбда}); |
то компилятор сразу отправит нас либо в документацию, либо гуглить, кому что ближе. Погуглив (или даже почитав документацию), мы узнаем, что сигналы и слоты в Qt — это объекты (наследники QObject
) и сигналы соединяются только с объектами. Лямбда объектом Qt не является, поэтому соединить её с сигналом нельзя.
Как вы понимаете, я не стал бы городить весь этот огород, если бы всё было так 🙂 На самом деле, всё прекрасно соединяется, но только в Qt5. Дело в том, что в Qt5 появился новый синтаксис соединения, почитать про все плюсы которого вы сможете на Хабре.Я же скажу только, что новый синтаксис выглядит так:
1 |
connect(sender, &Sender::valueChanged, receiver, &Receiver::updateValue ); |
и в документации английским по белому написано:
can be used with c+11 lambda expressions
и даже приведён пример:
1 2 3 |
connect(sender, &Sender::valueChanged, [=](const QString &newValue) { receiver->updateValue("senderValue", newValue); }); |
Тогда вперёд! Начнём с чекбокса:
1 2 3 4 5 6 7 8 |
connect(checkbox, &QCheckBox::stateChanged, [this](int state){state == 0 ? (push1->setDisabled(true), push2->setDisabled(true), push3->setDisabled(true)) : (push1->setEnabled(true), push2->setEnabled(true), push3->setEnabled(true));}); |
Я предпочитаю всегда писать [this]
вместо [=]
, мне кажется, что так выразительней. Смысл же этого — открыть телу лямбды доступ к нашему классу. Далее идёт объявление переменной типа int
, так как мы помним, что сигнал stateChanged
передаёт именно её. Ну и в теле функции обычный тернарный оператор с той лишь тонкостью (если это можно вообще назвать тонкостью), что последовательность действий должна быть заключена в скобки.
Не менее просто переписать и соединения кнопок, если push1
1 2 3 4 5 |
connect(push1, &QPushButton::clicked, [this](){line.text() == "Warning!" ? QMessageBox::critical(this, "Error", line.text()) : QMessageBox::information (this, "Information", QString::number(spin->value()));}); |
ещё может вызвать какие-то затруднения, то push2
и push3
совсем элементарны — и это как раз те случаи (сугубо на мой взгляд), когда применения лямбд оправдано:
1 2 3 4 5 |
connect(push2, &QPushButton::clicked, [this](){spin->setValue(spin->value() + 1);}); connect(push3, &QPushButton::clicked, [this](){spin->setValue(spin->value() - 1);}); |
Реализуем слайдер:
1 2 3 4 |
connect(slider, &QSlider::valueChanged, [this](int value){spin->setValue(value); value > 10 ? line.setText("Warning!") : line.setText("");}); |
А теперь самое сладенькое! Написав
1 2 3 4 |
connect(spin, &QSpinBox::valueChanged, [this](int value){slider->setValue(value); value > 10 ? line.setText("Warning!") : line.setText("");}); |
я получил от компилятора подарок. Любит он меня 🙂 Смысл подарка заключался в том, что он не может понять какое значение шлёт ему spin
. Дело в том, что у спинбокса есть сигнал valueChanged(int i)
и valueChanged(const QString &text)
. Я про это знал, но ожидал, что если я определяю в параметрах лямбды (int x)
, он меня поймёт. Оказалось, что компилятор как маленький ребёнок — всё ему нужно разжёвывать, рассказывать, объяснять. Зато как только он всё поймёт, опять же как маленький ребёнок способен делать гениальные выводы. В нашем случае, он просто скомпилирует программу 🙂
Поняв, что как-то нужно объяснить компилятору, что именно я хочу и погуглив, я нашёл ответ — и это тот самый static_cast, та самая вишенка на торте:
1 2 3 4 |
connect(spin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [this](int value){slider->setValue(value); value > 10 ? line.setText("Warning!") : line.setText("");}); |
Теперь всё работает!
Вот такие они, лямбды на службе оконных приложений! Пользуйтесь, и да будет ваш код работающим, компилятор послушным, а программы полезными и GNU GPL! 🙂
Предыдущая статья: Занимательная математика, очаровательный python. Эпизод 3: Финальный аккорд
Следущая статья: ѢѢ или трудности перевода. Обучаем QtCreator великому и могучему
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: