Весь Discord-бот

Я сделал бота Discord, и (насколько мне известно) код довольно хорош. Тем не менее, я в основном учил себя Python и discord.py (библиотека Discord для Python), поэтому мне было интересно, есть ли какие-либо улучшения.

Единственное, что я могу понять прямо сейчас, это то, что я, вероятно, мог бы сделать еще несколько комментариев, но в остальном я не совсем уверен.

https://github.com/Sinel-Servers-Limited/JoyBot

Правила «по теме» гласят, что я могу размещать внешние ссылки, но самая важная часть кода должна быть на сайте. Я считаю это своей самой важной частью:

# JoyBot/cogs/bump.py

    @commands.Cog.listener()
    async def on_message(self, message):
        try:
            if Ban(message.guild.id).is_banned():
                return
        except AttributeError:
            return

        if Counting(message.guild.id).channel_get_id() == message.channel.id:
            return

        firstbump = False
        if message.author.id == config['DISBOARDID']:
            for embed in message.embeds:
                if ":thumbsup:" in embed.description:
                    bumpID = await get_id().member(embed.description)
                    bump = Bmp(message.guild.id, bumpID)
                    oldtop = bump.get_top(raw=True)

                    if oldtop is None:
                        firstbump = True
                        oldtop = 0

                    bump.add_total()
                    newtop = bump.get_top(raw=True)

                    send_msg = f"<@{bumpID}>, your bump total has been increased by one!nType `.bumptotal` to view your current bump total!"
                    if not firstbump:
                        if oldtop != newtop:
                            send_msg += "nYou also managed to get the top spot! Nice!"
                            send_msg += f"nn<@{oldtop}>, you've lost your top spot!"

                    else:
                        send_msg += "nnYou were also the first to bump the server. Congrats!"

                    await message.channel.send(send_msg)

Этот код отслеживает, сколько раз человек, который «нажимал» сервер на Disboard. Я считаю это важным, потому что это главная особенность моего бота, на которую я тратил больше всего времени.

1 ответ
1

Декораторы

Проблема, которую нужно решить

Верх вашего кода следует переместить в другую функцию, поскольку вы дублировали код в другом месте. Чтобы СУШИТЬ ваш код, мы можем создать декоратор. Чтобы объяснить декораторы, я начну с двух придуманных функций и продолжу работу.

В нашем мире отрицательные числа недопустимы, если мы получим отрицательное число, мы return. У нас есть две функции: одна для удвоения чисел, а другая — для прибавления 1 к числу.

def double(number):
    if number < 0:
        return
    return 2 * number

def add_one(number):
    if number < 0:
        return
    return 1 + number

Теперь наличие одного и того же кода в двух функциях — это запах кода. Если спецификация изменится, вам необходимо изменить код как минимум дважды. Если вы забыли хотя бы одно место, вы внесли ошибку в свой код! Например, не обрабатывает None может быть недосмотр там, где нам действительно нужно.

Мы можем ввести функцию для проверки правильности числа. Но здесь это не совсем полезно, потому что все, что мы сделали, это спрятали number < 0 за функцией. И у нас все так же if ...: return шаблон.

def is_positive(number):
    return 0 <= number

def double(number):
    if not is_positive(number):
        return
    return 2 * number

Выполняет функции первоклассных граждан

Вместо этого мы можем передать функцию в is_positive для вызова, если число положительное. Есть обратная сторона: если мы не вызываем наши функции запутанным образом, мы фактически не проверяем ввод.

def is_positive(fn, number):
    if number < 0:
        return
    return fn(number)

def double(number):
    return 2 * number

def add_one(number):
    return 1 + number

print(is_positive(double, 2))
print(is_positive(double, -1))
print(double(-1))
print(is_positive(add_one, 2))
4
None
-2  # Oops, should be None
3

Закрытия спешат на помощь

Всегда звонить is_positive перед звонком; double и add_one, мы можем использовать закрытие. Замыкание примет функцию (double или же add_one) и вернуть новую функцию (что is_positive в настоящее время есть).

Если мы переназначим double с вновь созданной функцией вопрос решен.

def is_positive(fn):
    def inner(number):
        if number < 0:
            return
        return fn(number)
    return inner

def double(number):
    return 2 * number
    
print(is_positive(double)(2))
print(is_positive(double)(-1))
print(double(-1))  # Still has the issue

double = is_positive(double)  # Fix the issue

print(double(2))
print(double(-1))
4
None
-2  # Look the issue
4
None  # Now the issue is fixed

Декоратор синтаксического сахара

я нахожу double = is_positive(double) быть довольно некрасивым. Однако у Python есть @ синтаксис для украшения функций.
@ то же самое, что и использование double = is_positive(double).

def is_positive(fn):
    def inner(number):
        if number < 0:
            return
        return fn(number)
    return inner

@is_positive
def double(number):
    return 2 * number

print(double(2))
print(double(-1))
4
None

Общие шаблоны-оболочки

Прежде чем выбрать, как применять декораторы к вашему коду, нам нужно посмотреть, как мы можем предоставить аргументы декоратору.

  1. Без обертки

    Pro: Никаких лишних скобок и очень простая реализация.
    С: Нет опции для позиционных или ключевых аргументов.

    import functools
    
    def is_positive(fn):
        @functools.wraps(fn)
        def inner(*args, **kwargs):
            # ...
        return inner
    
    @is_positive
    def double(number):
        return 2 * number
    
    @is_positive
    def add_one(number):
        return 1 * number
    
  2. Обертка закрытия

    Pro: Разрешить позиционные и ключевые аргументы.
    С: Всегда использовать круглые скобки, даже если не приводятся аргументы.

    import functools
    
    def is_positive(include_zero=True):
        def wrapper(fn):
            @functools.wraps(fn)
            def inner(*args, **kwargs):
                # ...
            return inner
        return wrapper
    
    @is_positive(False)
    def double(number):
        return 2 * number
    
    @is_positive()
    def add_one(number):
        return 1 * number
    
  3. Частичная обертка

    Pro: Не нужно использовать круглые скобки, если не приводятся аргументы.
    С: Разрешены только аргументы ключевого слова.

    import functools
    
    def is_positive(fn=None, *, include_zero=True):
        if fn is None:
            return functools.partial(include_zero=include_zero)
    
        @functools.wraps(fn)
        def inner(*args, **kwargs):
            # ...
        return inner
    
    @is_positive(include_zero=False)
    def double(number):
        return 2 * number
    
    @is_positive
    def add_one(number):
        return 1 * number
    

Что вам следует выбрать, может быть немного сложно. Основы:

  • Если вам не нужно приводить аргументы; используйте 1.
  • Если вы обновляетесь с 1 и вам необходимо поддерживать обратную совместимость; использовать 3.
  • В противном случае выберите то, что вам больше нравится, от 2 до 3.

По большей части выбор между 2 и 3 похож на выбор между кока-колой и пепси. Выберите тот, который вам больше нравится. Я предпочитаю 3. Обычно мне не нужны позиционные аргументы. Я считаю, что использование аргументов в качестве ключевых слов является преимуществом. И мне не нравится использовать ненужные скобки. Однако 2 проще и я думаю, что это более общий подход. Например, мы можем угадать commands.Cog.listener использует 2.

Ваш код

Как украсить

Примечание: Код отсюда не протестирован

Чтобы украсить вашу функцию, мы можем пойти одним из четырех способов.

  1. Ваш метод должен быть статическим. Вы никогда не используете self в коде, поэтому мы можем украсить @staticmethod первый.

    Проблема: Вы не сможете украсить нестатический метод.

  2. Пользователь должен указать индекс аргумента, где находится сообщение.

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

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

    Проблема: Нам нужно преобразовать в декоратор класса и заставить message быть первым аргументом. Наверное, было бы неприятно правильно работать со статическими методами и методами класса.

  4. Вы можете осмотреть подпись функции чтобы получить индекс аргумента с именем «сообщение».

    Подобно тому, как инъекция зависимостей вводит правильный материал. Однако мы не будем вводить зависимости и будем использовать имена, а не типы.

    Проблема: Вы должны назвать аргумент «сообщение».

Лучший вариант — это действительно ваш выбор. Мне очень нравится 4, но DI на самом деле не вещь в Python. Не говоря уже о нетипированных полусырых узорах DIesque. Тем не менее, я покажу вам, как реализовать 2. 2, вероятно, является лучшим общим способом решения проблемы. Не слишком умен и работает в большинстве ситуаций.

Поскольку мы идем по индексу, мы можем для упрощения кода принимать только аргументы.

def validate_message(index=0):
    def wrapper(fn):
        @functools.wraps(fn)
        async def inner(*args):
            message = args[index]
            try:
                if Ban(message.guild.id).is_banned():
                    return
            except AttributeError:
                return

            if Counting(message.guild.id).channel_get_id() == message.channel.id:
                return
            return await fn(*args)
        return inner
    return wrapper
@commands.Cog.listener()
@validate_message(index=1)
async def on_message(self, message):
    firstbump = False
    # ...

Разрешение аргументов ключевого слова и значений по умолчанию

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

import inspect


def validate_message(index=0):
    def wrapper(fn):
        @functools.wraps(fn)
        async def inner(*args, **kwargs):
            _args = signature.bind(*args, **kwargs)
            message = _args.arguments.get(parameter.name, parameter.default)
            if message is inspect.Parameter.empty:
                raise ValueError("a message must be provided")
            # ...
            return await fn(*_args.args, **_args.kwargs)

        signature = inspect.signature(fn)
        parameter = list(signature.parameters.values())[index]
        return inner
    return wrapper

Рассмотрение

  • Ваш код следует анти-шаблону стрелки. Вы можете использовать операторы защиты, чтобы ваш код не двигался все больше и больше вправо.

    if message.author.id != config["DISBOARDID"]:
        return
    # ...
    
  • Ваш стиль непостоянен, некоторые места вы используете ' другие места, которые вы используете ". То же и со случаем переменных firstbump, bumpID и send_msg.

    Вы взяли и изменили части своего кода из кода в Интернете? Соблюдали ли вы лицензию (лицензии), если вы взяли и изменили IP других людей?

  • Не гарантируется, что строки будут иметь быструю конкатенацию. Однако с вашим кодом я не вижу необходимости. Однако вы можете составить список и "".join чтобы гарантировать быстрое объединение строк во всех совместимых со спецификациями средах.

    На мой взгляд, код здесь стал менее красивым.

  • Если у вас более одной вставки в одно сообщение, где одна — первая выпуклость; ваш код скажет, что все вложения — это первый удар.

  • Остальная часть кода выглядит как шаблонный код вокруг get_id и Bmp. Мой ответ достаточно длинный, поэтому я оставлю свой отзыв здесь.

@commands.Cog.listener()
@validate_message(index=1)
async def on_message(self, message):
    if message.author.id != config['DISBOARDID']:
        return
    for embed in message.embeds:
        if ":thumbsup:" not in embed.description:
            continue

        bump_id = await get_id().member(embed.description)
        bump = Bmp(message.guild.id, bump_id)
        old_top = bump.get_top(raw=True)
        _old_top = old_top or 0
        bump.add_total()
        new_top = bump.get_top(raw=True)

        msg = [f"<@{bump_id}>, your bump total has been increased by one!nType `.bumptotal` to view your current bump total!"]
        if old_top is None:
            msg.append("nnYou were also the first to bump the server. Congrats!")
        elif _old_top != _new_top:
            msg.append(
                "nYou also managed to get the top spot! Nice!"
                f"nn<@{_old_top}>, you've lost your top spot!"
            )

        await message.channel.send("".join(msg))

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *