Необходимо прочувствовать идею асинхронности и сделать отзывчивый интерфейс с аватарками
Работа с данными ВК / подсказка к 5 задачке
Сейчас, когда листаешь страницу происходит небольшое подвисание интерфейса.
Связано это с тем что, при клике на кнопку, на сервер ВК отправляется запрос и ожидается ответ, и хотя это занимает не сильно много времени какие-то сотни миллисекунд, интерфейс в это время становится недоступным.
Чтобы блокировки интерфейса не происходило мы можем воспользоваться так называемым асинхронным подходом к выполнению кода. При таком подходе мы операцию по отправке запроса выносим в отдельный исполняемый поток и блокировка интерфейса пропадает
Делается это так. Сначала надо кусок кода ответственный за запрос вынести в отдельную функцию. Вот так:
class UI():
@staticmethod # обязательно помечаем функцию как staticmethod
def get_request(offset_from_start): # функця с параметром offset_from_start
proxies = {
"http": "http://172.27.100.5:4444",
"https": "http://172.27.100.5:4444",
}
r = requests.get("https://api.vk.com/method/groups.getMembers", {
"access_token": "203a983a203a983a203a983a58204daa522203a203a983a405dff18fea57bccc1c68d62",
"v": "5.130",
"group_id": "golos_irnitu",
"fields": "photo_max_orig",
"offset": self.offset_from_start, # тут вместо self.offset_from_start используем offset_from_start
"count": 10,
"lang": "ru",
}, proxies=proxies)
data = r.json()
pprint(data)
return data # функцию должна вернуть данные которые получит от ВК
def get_members(self):
""" отсюда убираем
r = requests.get("https://api.vk.com/method/groups.getMembers", {
"access_token": "203a983a203a983a203a983a58204daa522203a203a983a405dff18fea57bccc1c68d62",
"v": "5.130",
"group_id": "golos_irnitu",
"fields": "photo_max_orig",
"offset": self.offset_from_start,
"count": 10,
"lang": "ru",
})
data = r.json()
pprint(data)
"""
# дальше все так же
items = data['response']['items']
# ...
теперь код, который ответственен за наполнение формы (т.е. создание лейблов тоже вынесем в отдельную функцию с параметром data
class UI():
@staticmethod
def get_request(offset_from_start):
# ...
return data
# добавил новую функцию с копипастой кода по заполнению лейблов
def fill_labels(self, data):
items = data['response']['items']
for label in self.labels:
label.destroy()
self.labels = []
y = 10
for r in items:
first_name = r['first_name']
last_name = r['last_name']
label = Label(text=f"{first_name} {last_name}")
label.place(x=10, y=y)
y += 30
self.labels.append(label)
self.label_count['text'] = f"{self.offset_from_start} / {data['response']['count']}"
def get_members(self):
""" А ТУТ ВСЕ УБИРАЕМ
items = data['response']['items']
for label in self.labels:
label.destroy()
self.labels = []
y = 10
for r in items:
first_name = r['first_name']
last_name = r['last_name']
label = Label(text=f"{first_name} {last_name}")
label.place(x=10, y=y)
y += 30
self.labels.append(label)
self.label_count['text'] = f"{self.offset_from_start} / {data['response']['count']}"
"""
Теперь самое интересное)
У нас есть функция get_request которая делает запрос, и функция fill_labels которая результат запроса выведет на форму, можем попробовать их связать сначала синхронно:
class UI():
@staticmethod
def get_request(offset_from_start):
# ...
return data
def fill_labels(self, data):
# ...
self.label_count['text'] = f"{self.offset_from_start} / {data['response']['count']}"
def get_members(self):
# связали
data = self.get_request(self.offset_from_start)
self.fill_labels(data)
# ...
оно даже будет работать так же, с небольшими подвисаниями:
а теперь вызовем эти функции асинхронно:
# …
import multiprocessing # добавим пакет для многопоточности
class UI():
@staticmethod
def get_request(offset_from_start):
# ...
def fill_labels(self, data):
# ...
def get_members(self):
# создаем пул потоков
# эта фиговина позволяет управлять потоками
# и вызывать функции асинхронно
pool = multiprocessing.Pool()
# запускаем асинхронное выполнение
pool.apply_async(
self.get_request, # первый параметр -- это функция которая должна выполниться в отдельном потоке
(self.offset_from_start, ), # это параметры которые надо передать функции
callback=self.fill_labels # а это функция которой асинхронная функция передаст выполнение по завершению
)
def __init__(self, gui):
# ...
def on_button_click(self):
# ...
# НУ И ТУТ ОБЯЗАТЕЛЬНО НАДО НАПИСАТЬ ТАКУЮ КОНСТРУКЦИЮ
# без нее асинхронный вызов не сработает
if __name__== '__main__' :
gui = Tk()
UI(gui)
gui.mainloop()
тестим:
ура! теперь ничего не блокируется и можно тыкать даже по несколько раз =)
Раз уж такое дело добавим еще отображение аватарок профилей.
class UI():
@staticmethod
def get_request(offset_from_start):
# ...
def fill_labels(self, data):
# ...
y = 10
for r in items:
first_name = r['first_name']
last_name = r['last_name']
label = Label(text=f"{first_name} {last_name}")
label.place(x=40, y=y) # сдвинем вправ
# y += 30 ЭТО в КОНЕЦ УТАЩИМ
self.labels.append(label)
# ДОБАВЛЯЕМ ОТОБРАЖЕНИЕ КАРТИНОК, по стандарту
# создаем лейбл под картинку, с границей
lblImage = Label(borderwidth=1, relief="solid")
lblImage.place(x=10, y=y, width=20, height=20)
image_data = requests.get(r['photo_max_orig'], stream=True).raw # стягиваем картинку из интернета
# собираем данные в картинку
image = Image.open(image_data)
image.thumbnail([20, 20], Image.ANTIALIAS) # уменьшаем чтобы влезло в лейбл
lblImage.image = ImageTk.PhotoImage(image)
lblImage.configure(image=lblImage.image)
y += 30 # утащили вниз
self.label_count['text'] = f"{self.offset_from_start} / {data['response']['count']}"
как мы видим работает весьма тормознуто
тут как можно догадаться, что тормоза возникают из-за того, что каждая картинка тянется прямо после создания лейбла, а так как выводим мы по 10 пользователей, то на каждого пользователя приходится ждать пока его картинка скачается.
Стало быть, нам надо процедуру стягивания картинки и отображения ее на формы опять разбить на две функции. Первая – тянет данные из интернета и по завершению передает результат второй.
Создаем эти функции:
class UI():
@staticmethod
def get_request(offset_from_start):
# ...
# новая функция для стягивания картинки
# так как ее будем вызывать асинхронно то надо добавлять @staticmethod
# ну и на вход у нее один параметр -- url картинки
@staticmethod
def fetch_image(image_url):
proxies = {
"http": "http://172.27.100.5:4444",
"https": "http://172.27.100.5:4444",
}
image_data = requests.get(image_url, stream=True, proxies=proxies).raw # стягиваем картинку из интернета
# собираем данные в картинку
image = Image.open(image_data)
image.thumbnail([20, 20], Image.ANTIALIAS) # уменьшаем чтобы влезло в лейбл
return image
# еще одна функция в которую будет передаваться результат
# на входе картинка и лейбл в который надо картинку положить
# важно чтобы параметр image, то есть в который должны попасть данные из интернета, был первым
def set_label_image(self, image, lblImage):
lblImage.image = ImageTk.PhotoImage(image)
lblImage.configure(image=lblImage.image)
def fill_labels(self, data):
# ...
y = 10
for r in items:
# ...
lblImage = Label(borderwidth=1, relief="solid")
lblImage.place(x=10, y=y, width=20, height=20)
# вызываем наши две функции последовательно
image = self.fetch_image(r['photo_max_orig'])
self.set_label_image(lblImage, image)
y += 30
self.label_count['text'] = f"{self.offset_from_start} / {data['response']['count']}"
можно проверить, по идее должно работать так же:
теперь сделаем вызов асинхронно
# ...
# добавляем импорт, он нам потребуется чтобы связать функцию fetch_image
# которая возвращает один параметр, с функций set_label_image, которая ожидает два
from functools import partial
class UI():
@staticmethod
def get_request(offset_from_start):
# ...
@staticmethod
def fetch_image(image_url):
# ...
def set_label_image(self, image, lblImage):
# ...
def fill_labels(self, data):
# ...
# создаем пул потоков, он должен быть общий для всех картинок,
# чтобы запросы шли параллельно
pool = multiprocessing.Pool()
y = 10
for r in items:
# ...
# создание лейблов под картинки не трогаем
lblImage = Label(borderwidth=1, relief="solid")
lblImage.place(x=10, y=y, width=20, height=20)
# делаем асинхронный вызов
pool.apply_async(
self.fetch_image, # вызываем стягивание картинки
(r['photo_max_orig'], ), # передаем ссылку на картинку
# ну а тут самое хитрое: partial(self.set_label_image, lblImage=lblImage) позволяет нам
# сделать из функции с двумя аргументами -- функцию с одним аргументов
# а в качестве аргумента автоматом lblImage подставляет тот lblImage
# который мы создали выше, это достаточно сложная концепция, которая называется "Каррирование"
callback=partial(self.set_label_image, lblImage=lblImage)
)
y += 30
self.label_count['text'] = f"{self.offset_from_start} / {data['response']['count']}"
ну что ж, тестируем:
Красота!
И еще один тонкий момент сейчас у нас на каждые запрос создается отдельный пул потоков, это не очень эффективно и может даже замедлять работу программы.
Поэтому сделаем один общий пул и будем его использовать везде
class UI():
@staticmethod
def get_request(offset_from_start):
# ...
@staticmethod
def fetch_image(image_url):
# ...
def set_label_image(self, image, lblImage):
# ...
def fill_labels(self, data):
# ...
# pool = multiprocessing.Pool() этот убрали
y = 10
for r in items:
# ...
self.pool.apply_async( # тут self добавили
self.fetch_image,
(r['photo_max_orig'], ),
callback=partial(self.set_label_image, lblImage=lblImage)
)
y += 30
self.label_count['text'] = f"{self.offset_from_start} / {data['response']['count']}"
def get_members(self):
# pool = multiprocessing.Pool() этот убрали
self.pool.apply_async( # тут self добавили
self.get_request,
(self.offset_from_start, ),
callback=self.fill_labels
def __init__(self, gui):
gui.geometry("400x360")
self.pool = multiprocessing.Pool() # ДОБАВИЛ ОБЩИЙ ПУЛ
# ...
# ...
Как видим интерфейс стал еще отзывчиваее, совсем не подвисает и картинки грузятся параллельно =)