Computer Science

Пишем про компьютеры и их строение для программистов, инженеров и компутерщиков. Статьи выходят раз в неделю: по пятницам или субботам, на 8-10 минут чтения.
Планы канала на трелло.
По всем вопросам — в телеграм.

Парадигмы программирования

Когда мы проходили пайтон, у нас была статья про два стиля описания кода: декларативный и императивный.
Вообще стилей много, но условно их разделяют на шесть стилей: функциональный, структурный, модульный, ООП, декларативный и императивный.

Это не значит, что в программе нужно использовать только один, но для каких-то задач подходит только функциональный или только декларативный, как в SQLe.

Функциональное программирование

Программа состоит из кучи функций и маленького main-блока, откуда их вызывают. Получается длинная змея вложенных вызовов. Этот стиль трудно читается и отслеживается глазами, но занимает мало места и в чистом виде выглядит классно. Маленькая программа для создания экспоненты со степенью значения в аргументе

from math import exp, sin, sqrt, fabs


def get_exp(x):
    return exp(sin(sqrt(fabs(x))) + 1)


print(get_exp(6))

В функциональном стиле используются чистые функции. Чистая функция работает только со своими аргументами и не трогает данные за своими пределами, на одни и те же аргументы, она возвращает одинаковый результат.

Для реального примера можно представить банк: чтобы получить справку 414, нужно получить справку 519, для неё нужна 139, а для неё паспорт и так далее. Вызов выглядит вот так

справка_414(справка_519(справка_139(паспорт)))

Структурное программирование

Программа выстраивается постепенно сверху вниз и разрастается на ветвления условий, как корни у дерева. Этот стиль часто показывают новичкам, потому что он простой в понимании и отслеживании глазами.
Вот самый простой алгоритм для определения большего числа из трёх в структурном стиле

a = input('Первое число: ')
b = input('Второе число: ')
c = input('Третье число: ')

if a > b and a > c:
    print('Самое больше число {}'.format(a))
elif b > c and b > a:
    print('Самое большое число {}'.format(b))
else:
    print('Самое большое число {}'.format(c))

Модульное программирование

Программу делят на отдельные файлы и вызывают из главного когда нужно.
В модульном программировании данные распределяются по модулям и используются в одном главном файле.
Когда мы писали бота, мы его дробили на файлики для прокси, для ответа на сообщения и другие. Эти файлы — модули, которые вызывались в главный файл. Прокси ченджер отвечал за поиск и хранение работающих прокси, лист контрол за управление списком покупок и напоминаниями, а логер — за ведение отчётов.

import proxy_changer
import list_control
import loger
'Дальше по коду идут вызовы через точку loger.функция, list_control.функция ,proxy_changer.функция'

Объектно-ориентированное программирование

Код состоит из объектов — классов. Классы работают как рецепты для еды. У них есть название, есть ингредиенты и способы готовки. Но это рецепт, чтобы его приготовить, нужно вызвать рецепт из main-блока, то есть — создать объект этого рецепта.
Для примера сделаем крабсбургер

class Крабсбургер
  {
  булочки = 2
  сыр = 2
  котлетки = 1
  томаты = 2
  салат = 1
  кетчуп = 1,25
   
  def готовка():
    'положить одно на другое и прогреть'
}

мой_перекус = Крабсбургер()

С ООП мы сталкивались когда писали бота.

# Создаём объект бота через класс TeleBot.
bot = tb.TeleBot('Токен бота', threaded=False)
# Используем метод send_message, который теперь доступен у объекта бота
bot.send_message(message.chat.id, about_me)

Декларативный

Описывает сам объект, что он из себя представляет и его характеристики.
Например, мы хотим бутерброд. «ЗАХААААР, сделай мне бутерброд». Он идёт на кухню и делает бутерброд как умеет.
Декларативный факториал

def factorial(num):
    if (num == 1):  # Это условие для факториала единицы 1! = 1 
        return 1

    # А это вычисление любого факториала
    num = num * factorial(num - 1)
    return num

Императивный

Описывает как найти объект или вычислить его значение.
Опять таки мы хотим бутерброд. «ЗАХАААР, иди на кухню, возьми и порежь хлеб, намажь на него масло и положи сверху три кусочка колбасы.» Идёт и делает по вашему алгоритму, а не как он умеет.
Императивный факториал

def factorial(num):
    counter = 1  
    result = 1  

    while(counter <= num):  # Выполняется пока счётчик не дойдёт до края, а если край один — сразу выходит из цикла
        result *= counter  # Сокращённое выражение result = result * counter
        counter += 1  # Сокращённое выражение counter = counter + 1

    return result

В следующей статье расскажем про компиляторы и интерпретаторы.

Напоминания от бота

В прошлой статье мы научили бота выводить список покупок через кнопки телеграма. В этой статье научим его напоминать нам о покупках.

Когда в программе появляется таймер, он блокирует поток выполнения и бот замолчит до конца таймера. Такое нам не надо, поэтому нужно сделать отдельный поток для таймера, чтобы он не мешал общению с ботом.

В стандартной библиотеке пайтона есть библиотека для работы с потоками — threading. В ней есть класс Timer, его будем использовать для таймера. Для работы таймера нужно два аргумента: время таймера в секундах и функция для вызова по окончанию таймера.
Импортируем в файл list_control.py библиотеку для работы со временем и потоками:

from datetime import datetime, date, time
from threading import Timer

Функция для вычисления разницы

Назовём функцию get_time_delta. На вход она получает обязательный аргумент время и необязательный аргумент дату. Так получается из-за разных напоминаний, вот пример: во фразе «напомни через две минуты» бот найдёт только время, а во фразе «напомни завтра в два часа дня» дату и время.

От сервера ДФ дата и время приходит в текстовом формате, их нужно перевести в формат даты или времени.
Потом нам нужно собрать дату и время в один формат — datetime и вычислить текущий datetime, чтобы получить время для таймера в секундах.
Проверку даты мы вынесли в отдельную функцию, чтобы не дублировать код.
Получилась вот такая функция

def get_time_delta(time_notify, date_notify=None):
    def calculating_date():
        if date_notify is None:
            return date.today()
        return date.fromisoformat(date_notify)

    # Переводим время в формат времени
    parsed_time_notify = time.fromisoformat(time_notify)
    # Переводим дату в формат даты
    parsed_date_notify = calculating_date()
    # Собираем дату и время в формат даты и времени
    parsed_datetime = datetime.combine(parsed_date_notify, parsed_time_notify)
    datetime_now = datetime.now()
    delta = parsed_datetime - datetime_now
    # Получаем разницу во времени в секундах
    delta_seconds = delta.seconds

    return delta_seconds

Ставим таймер

Назовём функцию set_notify, она будет принимать три аргумента: разницу времени, объект бота и айди чата.
В функции set_notify сделаем ещё одну, она будет отправлять пользователю напоминание о списке и прикреплять его к напоминанию.

def notify():
        shop_list = list_from_file()
        markup = create_buttons(shop_list)
        bot.send_message(chat_id, 'Ты просил напомнить про покупки.\rВот список',
                         reply_markup=markup)
        # Удаляем таймер
        timer.cancel()

Теперь создаём таймер и запускаем его. В итоге файл list_control.py выглядит вот так

Прикручиваем к главному файлу

Интент с напоминанием о списке активирует такой action: create_list.shop_list.notify.
Добавляем в главный файл такие блоки

# Создаем напоминание
    elif action == 'create_list.shop_list.notify':
        # Если бот не смог распарсить время
        if not parameters['time']:
            bot.send_message(chat_id, 'Не могу прочитать время')

        # Если напоминание поставлено без даты
        if not parameters['date']:
            bot.send_message(chat_id, 'Напомню в {}'.format(parameters['time']))
            time_delta = list_control.get_time_delta(parameters['time'])
            list_control.set_notify(time_delta, bot, chat_id)

        # Если напоминание поставлено с датой и временем
        else:
            bot.send_message(chat_id, 'Напомню {} в {}'.format(parameters['date'],
                                                               parameters['time']))
            time_delta = list_control.get_time_delta(parameters['time'], parameters['date'])
            list_control.set_notify(time_delta, bot, chat_id)

Файл bot.py выглядит теперь вот так

Теперь у вас есть бот для помощи с покупками на ИИ. Рабочий код лежит в нашем репозитории на гитлабе, можно скачать, поставить свои токены и пользоваться. Если что-то не работает, пишите @cmp_sci.

На этом с ботами в телеграме закончим. Это была серия статей для знакомства с ИИ и телеграм ботами, позже мы вернёмся к боту и сделаем его лучше.

В следующей статье расскажем про парадигмы языков программирования.

Делаем кнопки для чата

В прошлой статье мы подключили бота к ИИ. В этой статье научим бота делать список покупок из кнопок и удалять их по нажатию. В итоге получится вот так

Про напоминания объясним в следующей статье

Забираем и обрабатываем список с ДФ

Сделаем отдельный файлик, чтобы не захламлять основной файл. Мы его назвали list_control.py

В прошлой статье мы остановились на том, что отправили список покупок на сервер ДФ.
С сервера ДФ список приходит в слипшемся виде, вот так: [продукт_1, продукт_2, продукт_3].
Чтобы сделать кнопки, нужно расклеить продукты и сделать отдельными, вот так: [продукт_1, продукт_2, продукт_3].

def parse_list(shop_list):
    shop_list = shop_list['list'][0].split(', ')
    for item in range(len(shop_list)):
        shop_list[item] = shop_list[item].capitalize()

    return shop_list

Чтобы было красиво, к каждому элементу применяем функцию capitalize. Она делает первую букву в слове большой, а остальные маленькими.

Бэкап списка

Со списком есть одна проблема: если бот упадёт, то доступа к кнопкам у него не будет. Чтобы список не терялся, сделаем буферный файл.

Пишем функцию list_to_file, она принимает готовый список и записывает его в файл. Одна строка — одна позиция, так удобней потом собирать список из файла.
Символ ’\n’ нужен для переноса на следующую строку.

def list_to_file(parsed_list):
    file = open('shop_list.txt', 'w')

    for item in parsed_list:
        file.write(item + '\n')
    file.close()

Забираем из файла

Теперь нужна функция получения списка из файла. Напишем list_from_file, она читает вещи из файла и составляет их в список.

def list_from_file():
    file = open('shop_list.txt', 'r')

    shop_list = [line.strip() for line in file]
    file.close()

    return shop_list

Кнопки

Теперь напишем функцию create_buttons, она делает из отделённых элементов кнопки. Количество кнопок зависит от количества строк в файле.

def create_buttons(parsed_list):
    shop_list = types.InlineKeyboardMarkup()
    for item in parsed_list:
        button = types.InlineKeyboardButton(item, callback_data=item)
        shop_list.add(button)

    return shop_list

В итоге получается такой файл

Выводим список

Теперь нужно сделать так, чтобы бот выводил список из кнопок. Он должен выводиться после сообщения от пользователя со списком покупок.

От ДФ нам приходит большой json файл, в нём ключ action со значением активного интента. Если в прошлой статье вы делали как у нас, то action выглядит так: create_list.shop_list.

В код главного файла бота добавляем такой код

if action == 'create_list.shop_list':
        # Парсим список
        shop_list = list_control.parse_list(parameters)

        # Записываем его в файл
        list_control.list_to_file(shop_list)

        # Собираем список из файла
        shop_list = list_control.list_from_file()

        # Делаем кнопки
        markup = list_control.create_buttons(shop_list)

        # Отправляем ответ с кнопками
        bot.send_message(message.chat.id, 'Записал. Вот список', reply_markup=markup)

Обрабатываем список

Теперь сделаем так, чтобы по нажатию они удалялись из списка. Переходим в главный файл bot.py и листаем до хэндлеров.

Когда вы нажимаете на кнопку, она отправляет боту сигнал, что её нажали. Этот сигнал называется callback.
Делаем обработчик вот так: @bot.callback_query_handler(lambda query: True).
’lambda query: True’ значит, что обработчик будет реагировать на любой коллбэк сигнал.

В обработчик добавляем код для удаления кнопки и обновления списка в файле.
В конце добавим условие: если удалили все кнопки, то сообщение со списком удаляется.
Получается вот так

@bot.callback_query_handler(lambda query: True)
def delete_button_from_list(query):
    # Получаем список из файла
    file_shop_list = list_control.list_from_file()
    # Создаём список из кнопок
    shop_list = list_control.create_buttons(file_shop_list)

    # Удаляем кнопку
    for button in shop_list.keyboard:
        if button[0]['callback_data'] == query.data:
            index_for_remove = shop_list.keyboard.index(button)
            del shop_list.keyboard[index_for_remove]
            file_shop_list.remove(button[0]['text'])
            list_control.list_to_file(file_shop_list)

    # Если список пустой – удаляем сообщение с ним
    if not shop_list.keyboard:
        bot.delete_message(query.message.chat.id, query.message.message_id)
    # Если не пустой, обновляем сообщение со списком
    else:
        bot.edit_message_reply_markup(query.message.chat.id, query.message.message_id,
                                                                 reply_markup=shop_list)

Обновляем файлы, пушим на сервер, перезапускаем докер на сервере.
Все готовые файлы лежат у нас на ГитЛабе.
Если что-то не получается, пишите @cmp_sci, попробуем помочь.


В следующей статье объясним как сделать таймер для напоминания.

Ранее Ctrl + ↓