Привет, Хабр!


Идея статьи появилась, когда я начал повсюду замечать якобы подтверждения мифа, что «программирование — это просто»‬‬.


В новостях «восьмилетняя девочка, которая второй раз в жизни занимается программированием, наклепала чат-бота за 45 минут»‬ (ага, да!).


Курсы предлагают мне за 10 месяцев с нуля стать миддл+ (ага, да!).


Но я-то знаю, как оно на самом деле. Мы, программисты, обычно решаем проблемы и двигаемся дальше, но я решил запротоколировать всё как есть, и в течение пары месяцев скрупулёзно записывал всю ту хрень, что происходила со мной и моими коллегами, чтобы показать программирование без прикрас. Поехали!



❯ Миф 1: Это программирование, поэтому можно немного ошибиться, и ничего за это не будет


Блин, да даже если ошибиться в одном символе — всё может сломаться! Вот вам примеры.


Celery


Celery — это такая библиотечка для запуска задач в питоне. Я её хотел использовать наибанальнейшим образом: складывать задачи в очередь «default»‬ (крутое название, да?), и потом специальный воркер их бы оттуда забирал и выполнял одну за другой. Это самый простой сценарий, который только можно придумать.



Запустить воркер можно очень просто, вуаля:


celery -A project worker -Q=default --loglevel=info

Запускаю — не работает! Задачи в очереди накапливаются, а воркер чиллит в сторонке и ничего не делает.


Оказывается, воркер слушал очередь, которая называлась «=default»‬! Ведь так я и написал: -Q=default.



Я решил разобраться, какого чёрта параметр -Q так странно обрабатывается — я такое вижу впервые. Вот симуляция воркера на питоне:


from argparse import ArgumentParser

parser = ArgumentParser()
parser.add_argument('--queues', '-Q')
args = parser.parse_args()

print(args.queues)

Я потестировал вручную разные комбинации, в т.ч. ту, на которой у меня всё сломалось — но так и не смог воспроизвести баг:


> test.py --queues value
value
> test.py --queues=value
value
> test.py -Q value
value
> test.py -Qvalue
value
> test.py -Q=value
value  # это мой случай, но значение нормальное, без "равно"
> test.py -Q=”=value”
=value
> test.py -Q"=value"
value

Я почувствовал нотку безумия и полез в исходники celery. Выяснилось, что у них используется не встроенный в питон argparse.ArgumentParser, а библиотека click. Хорошо, перепишем нашу программу на click и потестируем...


import click

@click.command()
@click.option('--queues', '-Q')
def echo(queues):
    print(queues)

echo()

> test.py --queues value
value
> test.py --queues=value
value
> test.py -Q value
value
> test.py -Qvalue
value
> test.py -Q=value
=value  # хоба!
> test.py -Q=”=value”
==value  # хоба!
> test.py -Q"=value"
=value  # хоба!

Вот так я узнал, что click и argparse работают по-разному, но чтобы это узнать, нужно поставить куда-нибудь знак «=»‬. Люблю программирование!


Django settings


Написал приложение на Django и больше 100 тестов на все случаи жизни. Запускаю — работает, но как-то странно, с неправильными значениями. Никаких ошибок в логах, конечно, нет. Проверяю код несколько раз и нахожу это:


# settings.py
SUBSCRIPTION = {
  'duration': timedelta(days=30),
  'price': Decimal(100),
}

# service.py
subscriptions = getattr(
  settings, 
  'SUBSCRIPTIONS',
  DEFAULT_SETTINGS
)

Так уж устроен Django: настройки — это питон-файл с выражениями типа <КЛЮЧ> = <значение>. Так как это просто файл, а не какая-то хитрая структура данных, то ключи могут быть любыми. В данном случае я указал SUBSCRIPTION = ..., а нужно было SUBSCRIPTIONS = .... Получается, забыл одну букву «S»‬ — и все мои настройки идут коту под хвост и вместо них используются настройки по умолчанию! Ура!


Bumblebee


Это не моё, но одно из моих любимых. Как-то один разработчик в установочном скрипте случайно поставил пробел посреди строки, и скрипт сносил папку /usr на компах пользователей, делая систему нерабочей.


Мелочь, а приятно!



SqlAlchemy


Пытаюсь как-то записать несколько строк в Postgres. SqlAlchemy ругается на меня:


(psycopg.errors.UniqueViolation) duplicate key value violates unique constraint "ix_tag_name"
DETAIL:  Key (lower('name'::text))=(name) already exists.
[SQL: INSERT INTO tag (name, id) VALUES (%(name)s::VARCHAR, nextval('tag_id_seq')) RETURNING tag.id]
[parameters: {'name': 'ai'}]

В переводе на русский это значит «чел, ты пытаешься записать одно и то же имя, а сам сказал, что имена должны быть разными»‬. Проблема в том, что я пытаюсь записать разные имена. Хм, наверно, в базе уже есть какие-то записи, и я пытаюсь добавить дубликат. Иду в базу...



… а она пустая!


После некоторого расследования я нашёл это:


class Tag(BigIntBase):
    name: Mapped[str]

    __table_args__ = (
       Index("ix_tag_name", func.lower('name'), unique=True),
    )

Я хотел, чтобы индекс (и уникальность) брались из имени в нижнем регистре (func.lower('name')), вот только это не джанго, а алхимия, и строка «name»‬ здесь — это просто строка, а не значение из колонки «name»‬. Поэтому БД для каждой новой строки честно брала строку 'name', переводила её в нижний регистр и говорила, что я такую уже пытался записать только что!


Мини-рефакторинг


Жил-был вот такой код. Тут ничего сложного — есть «эпоха»‬ (интервал значений), и нужно отфильтровать объекты, которые из этой эпохи.


epoch = (100, 200)
jobs = Jobs.objects.filter(
    block__gte=epoch[0],
    block__lt=epoch[1],
)

Расслабьтесь — тут меня ещё не было, так что всё работает. Но вот я посмотрел на это и подумал: не по-сеньорски! Если подумать, то эпоха — это диапазон чисел, а для диапазонов в питоне есть встроенный тип — range. Его можно в type hints писать, и epoch: range явно лучше, чем epoch: tuple[int, int]. Поэтому мини-рефакторинг:


epoch = range(100, 201)
jobs = Jobs.objects.filter(
    block__gte=epoch[0],
    block__lt=epoch[1],
)

Я даже не ошибся в границах (ведь чтобы число 200 было включено в интервал, нужно передать в range 201). Но всё равно все тесты поломались. Почему? А потому что epoch[0] всё так же равнялось 100, но вот epoch[1] стало значить не «конец интервала»‬, а «второй элемент»‬, то есть 101. В итоге мой фильтр скукожился до [100, 101) :) А как правильно? А вот так: epoch.start и epoch.stop.


epoch = range(100, 201)
jobs = Jobs.objects.filter(
    block__gte=epoch.start,
    block__lt=epoch.stop,
)

Флаги bash


Этот случай утащил с работы. Простенький скрипт:


#!/bin/bash -e

false
rm -rf ~

В первой строчке мы говорим, что для его запуска нужно использовать bash с флагом -e. Команда false возвращает код ошибки, а так как у нас bash -e, то выполнение скрипта прерывается, и до rm -rf ~ не доходит. Надёжно? Надёжно.


Но только пока мы запускаем скрипт вот так: ./script.sh. Однажды приходит другой разработчик и запускает скрипт через bash script.sh. Башу плевать на все эти #!... в начале файла, он это считает комментарием и пропускает. Поэтому он выполняет команду false, она возвращает ошибку, но по умолчанию (чья это была идея, блин?) баш игнорирует ошибки и выполняет команды дальше — а дальше rm -rf ~. Удобно? Удобно.


Решение — ставить флаги так, чтобы их нельзя было проигнорировать:


#!/bin/bash

set -e
false
rm -rf ~

❯ Миф 2: У нас есть версии и системы контроля версий, так что ничего не ломается и всё отслеживается


ZeroVer


Да, у нас есть SemVer, которое позволяет отделить простые патчи от изменений, ломающих совместимость. У нас есть Changelogs. У нас есть LTS релизы, наконец.


Но всё равно находятся те, кто любит идти перпендикулярно best practices и добавляют перчинку в программирование. Вы же слышали про ZeroVer? Это когда разработчик говорит «чуваки, я не несу никакой ответственности за свою программу и могу её сломать хоть прям сейчас!»‬.



Самые популярные ZeroVer библиотечки — это, например, FastApi (на данный момент v0.115.0), react native (v0.75.3), за полным списком можно зайти на сайт https://0ver.org. Некоторые софтины живут в нулевой версии по 10+ лет!


Git


У нас есть git, мы можем откатываться, работать в ветках и вообще наглядно видеть всю историю, да?


Ну вот вам наглядная история, пожалуйста. Тут какие-то фиксы, постоянные мерджи, есть ещё stash, а в конце вообще начинается отдельная история, никак не связанная с основной!



Или вот, например, коллега пишет понятные сообщения, чтобы я не запутался:



Иногда git хранит душераздирающие истории релизов, мне даже захотелось снять фильм по этому сценарию (читать снизу вверх):



❯ Миф 3: Это программирование, поэтому все умные и пишут классный код


Langchain: sync vs async


Langchain — это фреймворк для создания приложений с использованием LLM. 14k форков и 91к звёзд на гитхабе говорят сами за себя.



Кто я такой, чтобы судить о проектах такого масштаба, да? Вот только я обнаружил, что этот замечательный фреймворк блокировал event loop, вызывая синхронную функцию вместо асинхронной.


Вообще langchain хочет уметь и в синхронный, и в асинхронный питон, и поэтому использует популярную тактику «добавлю async/await туда и сюда, всё синхронное запущу в потоках, методам добавлю букву a вначале — и вот я уже асинхронный»‬. Вот пример.


Синхронное:


class RunnableWithMessageHistory(RunnableBindingBase):
    def _exit_history(self, run: Run, config: RunnableConfig) -> None:
        hist: BaseChatMessageHistory = config["configurable"]["message_history"]

        inputs = load(run.inputs)
        input_messages = self._get_input_messages(inputs)
        if not self.history_messages_key:
            historic_messages = config["configurable"]["message_history"].messages
            input_messages = input_messages[len(historic_messages) :]

        output_val = load(run.outputs)
        output_messages = self._get_output_messages(output_val)
        hist.add_messages(input_messages + output_messages)

Меняем


  • def _exit -> async def _aexit
  • hist.add_messages -> await hist.aadd_messages

и дело в шляпе:


    async def _aexit_history(self, run: Run, config: RunnableConfig) -> None:
        hist: BaseChatMessageHistory = config["configurable"]["message_history"]

        inputs = load(run.inputs)
        input_messages = self._get_input_messages(inputs)
        if not self.history_messages_key:
            historic_messages = config["configurable"]["message_history"].messages
            input_messages = input_messages[len(historic_messages) :]

        output_val = load(run.outputs)
        output_messages = self._get_output_messages(output_val)
        await hist.aadd_messages(input_messages + output_messages)

Ну и конечно вы как только посмотрели на этот код, так сразу и скажете: Саня, ну это очевидно, тут в выражении config["configurable"]["message_history"].messages на самом деле messages — это не какой-то статичный атрибут со значением, а самый настоящий вычисляемый @property, который к тому же синхронный — мы это сразу поняли по имени!


Я не такой умный, у меня заняло некоторое время понять, в чём же проблема. Я завёл issue на github, его быстро пофиксили и побежали писать новые баги. Знаете, я даже подумываю написать статью про Langchain — он такой большой и в нём удивительно плохо всё, что я видел. А если вам хочется что-то подобное — про большущий проект, в котором куча дичи — то есть моя статья про Django.


Bittensor


Да, да, ещё один блокчейн. Но там крутятся деньги, и неплохие — вот картинка, которая должна сменить ваше лицо на менее скептическое:



Казалось бы, если крутятся деньги — то всё должно быть вылизано до блеска и покрыто тестами так, что кода не видно.


Конечно, ага!


Во-первых, они там не очень определились, как лучше: исключения или коды ошибок? Решили так: исключения и так сами будут вылетать из-за качества кода, так что для полноты картины можно ещё возвращать ошибки. Но коды и классы ошибок — это сложно, внешним разработчикам будет достаточно флага is_success и текста ошибки. Поэтому когда я вызываю их функцию do_stuff(), я как будто использую два презерватива — один проверяет и парсит ошибку, а другой ловит исключения:


try:
  is_success, msg = do_stuff()
  if not is_success:
    exc_class = parse_error(msg)
    raise exc_class(msg) 
except Exception:
  # handle exception

И так на каждый вызов библиотечной функции. Удобно!


Но недавно нас на работе убило кое-что получше. «Разрабы»‬, которые писали библиотеку, просто жить без неё не могут. Их библиотека — главная в мире, если она встречается у вас в программе, то ничего другого существовать не должно. Поэтому код инициализации такой:


class Subtensor:
    def __init__(
        self,
        network: Optional[str] = None,
        config: Optional[bittensor.config] = None,
        _mock: bool = False,
        log_verbose: bool = True,
    ) -> None:
        ...

        # Attempt to connect to chosen endpoint. Fallback to finney if local unavailable.
        try:
            # Set up params.
            self.substrate = SubstrateInterface(
                ss58_format=bittensor.__ss58_format__,
                use_remote_preset=True,
                url=self.chain_endpoint,
                type_registry=bittensor.__type_registry__,
            )

Как видно, сначала, разумеется, лезем в сеть! Какой смысл инициализировать хоть что-то без сети в 2024? Правильно, никакого!


Но что, если сеть недоступна? На это есть решение:


        except ConnectionRefusedError:
            _logger.error(
                f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint. Exiting...",
            )
            _logger.info(
                "You can check if you have connectivity by running this command: nc -vz localhost "
                f"{self.chain_endpoint.split(':')[2]}"
            )
            exit(1)

Горит сарай — гори и хата! Не получилось инициализировать класс — убиваем всю программу. Ну а чо?


❯ Миф 4: Это программирование, поэтому всё просто


В конце концов, программа — это просто текст на (в большинстве случаев) псевдо-английском языке. Можно что-то понять, а что непонятно — можно почитать в интернете или спросить chatGPT. Ну да, ну да.


Обычный тест


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


@patch("bittensor.subtensor", lambda *args, **kwargs: MockSubtensor())
@patch(
    "compute_horde_validator.validator.tasks.get_subtensor", lambda *args, **kwargs: MockSubtensor()
)
@patch("compute_horde_validator.validator.tasks._run_synthetic_jobs", MagicMock())
@pytest.mark.django_db(databases=["default", "default_alias"])
def test__run_synthetic_jobs__debug_dont_stagger_validators__true(settings):
    settings.DEBUG_DONT_STAGGER_VALIDATORS = True
    settings.CELERY_TASK_ALWAYS_EAGER = True

    run_synthetic_jobs()
    from compute_horde_validator.validator.tasks import _run_synthetic_jobs
    assert _run_synthetic_jobs.apply_async.call_count == 1

Всего-то нужно знать: pytest, fixtures, манкипатчинг, моки и call count, лямбды, pytest-django и его фичи, celery и что такое eager mode, late imports, assert. Изи!


Bash


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


set -euxo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"

Логика


Ну ладно, понятно, что есть сложный для чтения код. Но давайте что-нибудь простое! Вот, например:


await send_request(job)
start_time = now()

response = await ws.recv()
response_time = now()

reward = get_reward(response_time - start_time)

Тут вообще всё проще некуда: отправляем пользователю задачу (job), включаем таймер и замеряем время, пока он не пришлёт ответ. Ну и рассчитываем вознаграждение в зависимости от того, насколько быстро задачка была решена. Что не так?


Не так — это уверенность в том, что пользователь не считерит. А он может. Что, если он будет получать задание как обычно, но вот когда останутся последние байты (например, какие-нибудь закрывающик скобочки), он замедлит канал до 1 байта в несколько секунд? Тогда мы всё ещё будем ждать завершения передачи данных и не включать таймер, тогда как пользователь уже получил всё то, что нужно для начала решения задачи, и успешно стартовал раньше таймера, выиграв себе дополнительное время. Каково, а? Код простой, ситуация страшная (с)


NPM


Скажите, если программирование такое простое, то почему когда я пытаюсь поставить npx create-next-app, мой многострадальный ноутбук несколько минут качает 360 пакетов? Что? Мало? Ну вот npx create-remix ставит 631 пакет. А npx create-gatsby — 864 пакета. А npx create-react-app1480! Мне порой кажется, что npm — это какой-то аукцион, где важно победить по количеству зависимостей.


❯ Миф 5: Это программирование, поэтому всё логично


Бэкапы в postgres


Как мы делаем бэкап в postgres? Есть специальная утилита:


pg_dump postgres > dump.sql

Как восстановить? Есть специальная утилита...


pg_restore postgres < dump.sql
# pg_restore: error: input file does not appear to be a valid archive

Ну разумеется нет, для восстановления нужно не pg_restore, а просто


psql postgres < dump.sql

Почему? Потому что pg_dump по умолчанию выводит текст, а pg_restore ожидает бинарный формат. Всё логично.


Dockerfile build args


Иногда при сборке контейнеров нужно в Dockerfile передавать какие-то параметры. Ну, например, вы хотите «вшить»‬ версию приложения прям в контейнер. Дело несложное: docker build --build-arg VERSION=123. Нюансы начинаются, когда у вас multi-stage build.


Тут понятно: аргумент передаётся в child image, так что base image про этот аргумент ничего не знает:


FROM alpine AS base
RUN echo "Base image: $VERSION"

FROM base
ARG VERSION
RUN echo "Child image: $VERSION"

#5 Base image: 
#6 Child image: 123

Правила наследования вроде как работают: если передать аргумент в base image, то child image его унаследует:


FROM alpine AS base
ARG VERSION
RUN echo "Base image: $VERSION"

FROM base
RUN echo "Child image: $VERSION"

#5 Base image: 123
#6 Child image: 123

Но что случится, если аргумент объявить глобально? Ничего хорошего — ни base image, ни child image его не увидят!


ARG VERSION

FROM alpine:{$VERSION} AS base
RUN echo "Base image: $VERSION"

FROM base
RUN echo "Child image: $VERSION"

#5 Base image: 
#6 Child image: 

А почему? Ну вот так работает, это же даже в документации написано! Вы же читаете документацию от начала и до конца, прежде чем что-то использовать, да?


Javascript


Щас мне жсники влепят минусов за шутки в их адрес, но ведь с точки зрения питониста это правда забавно!


Во-первых, в js нельзя верить ==:


> [1, 2, 3] == [1, 2, 3]
false

Вроде === тоже есть, но мне не помогло:


> [1, 2, 3] === [1, 2, 3]
false

Зато помогло приведение к строкам! Вот уж откуда помощи не ждал:


> JSON.stringify([1, 2, 3]) == JSON.stringify([1, 2, 3])
true

Погнали дальше, в сортировку:


> [-5, -2, 2, 1].sort()
[-2, -5, 1, 2]

Вот ещё удобный способ работы со строками:


> ('b' + 'a' + + 'a' + 'a').toLowerCase()
'banana'

Нулевая дата, конечно, вернёт дату и время, а так как 0 и "0" равны, то можно использовать любой вариант, и результат будет один и… что?!


> new Date(0)
Thu Jan 01 1970 03:00:00 GMT+0300 (Moscow Standard Time)

> 0 == '0'
true

new Date('0')
Sat Jan 01 2000 00:00:00 GMT+0300 (Moscow Standard Time)

Разделители тоже не имеют значе......


> new Date('2024-01-01')
Mon Jan 01 2024 03:00:00 GMT+0300 (Moscow Standard Time)

> new Date('2024/01/01')
Mon Jan 01 2024 00:00:00 GMT+0300 (Moscow Standard Time)

Python


В питоне тоже всё просто, скобки всегда означают одно и то же...


{'a': 1}  # это словарь
{'a'}  # это множество (set)
{}  # это не пустое множество, это пустой словарь
set()  # а вот это пустое множество

1  # это просто число (int)
(1+2)  # это число
(1, 2)  # это кортеж (tuple)
(1)  # это тоже просто число
1,  # это тоже кортеж
()  # это пустой кортеж
(,)  # SyntaxError

[]  # это пустой список
[1]  # это список с одним элементом
[1, 2, 3]  # это список с 3 элементами
[i for i in range(3)]  # это тоже список с 3 элементами
()  # это пустой кортеж
(1)  # это просто число
(1, 2, 3)  # это кортеж
(i for i in range(3))  # это не кортеж, это generator expression

И поведение в питоне тоже очевидное. Например, он не даст изменять коллекцию в ходе итерации...


data = {1: 2}
for key, value in data.items():
  data[key+1] = value+1
# RuntimeError: dictionary changed size during iteration

data = [1, 2]
for value in data:
  data.append(value+1)
# Всё ок! Прощай, RAM :)

Pytest


Запускаю как-то в проекте pytest:


> ls
bot.py  config.py  history.py  knowledge.py  logs.py  __main__.py  models.py  __pycache__/  shell.py  tests/  tools.py  utils.py
> pdm run pytest .
ERROR collecting src/tests/test_bot.py 
ImportError while importing test module '~/workspace/project/src/tests/test_bot.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback: .pyenv/versions/3.11.4/lib/python3.11/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
tests/test_bot.py:3: in <module>
    from ..bot import message_handler
E   ImportError: attempted relative import beyond top-level package

А он мне говорит знаменитое питоновское «у тебя относительные импорты, а top-level package не задан»‬. Обычно это решается указанием «верхнего»‬ модуля при помощи python -m top_level_module.script. Но тут я запускаю pytest через pdm, всё не так просто… Я трачу время, пытаясь понять, какой ключ от меня хотят и в каком месте, и внезапно узнаю: чтобы pytest нашёл верхний модуль, нужно просто создать в нём файл __init__.py. Почему? Зачем?..


Dataclasses


Вот ещё пример логичности и простоты. Датаклассы Base и Child:


@dataclass
class Base:
  a: str

class Child(Base):
  b: str

Внезапно оказывается, что Base ведёт себя как приличный датакласс, а Child — как неприличный:


> Base()
TypeError: Base.__init__() missing 1 required positional argument: 'a'

> Base(a=’test’)
Base(a='test')

> Child(a=’test’)
Child(a='test')

> Child(a=’test’, b=’test’)
TypeError: Base.__init__() got an unexpected keyword argument 'b'

Почему так? Потому что «датаклассовость»‬ не наследуется. Если хочется, чтобы Child тоже был датаклассом, его нужно тоже декорировать.


@dataclass
class Base:
  a: str

@dataclass
class Child(Base):
  b: str

Так что будьте осторожны при наследовании.


❯ Миф 6: Это программирование, поэтому никакой неведомой фигни не происходит


Github checkout action


Работаю в своей ветке. Делаю pull request, запускаются тесты и падают с ошибкой — какой-то конфликт файлов. На домашнем компе тесты, конечно, проходят без проблем. Смотрю логи — конфликтуют два файла, один в моей ветке есть, второй не существует и вообще непонятно откуда взялся при тестировании. Ищу этот файл повсюду, нахожу в ветке master. Как файл из master просочился в мою ветку?!



Опять чертовщина какая-то! Пошёл разбираться и узнал: оказывается, если вы пишете вот так...


# .github/ci.yml

test:
  steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0

… и у вас pull request, то этот экшен сначала делает merge с целевой веткой, а уже потом checkout результата.




Представьте, я всю свою жизнь не знал об этом!


PgBouncer


Самый простой способ подключить приложение к базе данных — это подключить приложение к базе данных! Ваш кэп.



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



К счастью, есть решение: PgBouncer. Он выступит в роли прослойки между вашими приложениями и БД, эффективно выделяя соединения для приложений:



Казалось бы, круто! Что может пойти не так?


В один прекрасный день коллега (как же я счастлив, что не я!) по какой-то причине восстанавливает данные из бэкапа. Бэкап — это результат работы утилиты pg_dump, то есть простой текстовый файл типа


SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
...

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



Почему? А потому что строчка SELECT pg_catalog.set_config('search_path', '', false); из бэкапа как бы говорит: «postgres, забудь всё, что ты знал до этого, щас будем жёстко восстанавливаться из бэкапа»‬. Postgres всё забывает, но т.к. мы работаем через PgBouncer, то после восстановления данных соединение с БД не исчезает в никуда, а как есть передаётся другому приложению. Да, прям в состоянии «забудь всё»‬.



Приложение пытается что-то делать через это соединение, а оно испорчено, и приложение падает. Решение — для восстановления напрямую подключаться к БД, минуя PgBouncer. Такие дела.


❯ Миф 7: Это программирование, поэтому помощь повсюду: документация, IDE, LLM, …


PDM


Вот пакетный менеджер мне говорит: «Саня, ну не получилось ничего установить. Какая ошибка — хз. Кстати, установить ничего не получилось! Хочешь квест? Иди в логи и посмотри сам, что пошло не так».‬


> pdm add django-attachments~=1.5 django-bleach~=0.6.1 django-bootstrap3~=12.1.0 django-cacheops~=7.0.2 django-ckeditor~=5.9.0 django-contrib-comments~=1.9.2 django-cors-headers~=3.7.0 django-debug-toolbar~=3.5.0 django-elasticsearch-dsl~=7.1.1 django-filter~=2.3.0
Adding packages to default dependencies: django-attachments~=1.5, django-bleach~=0.6.1, django-bootstrap3~=12.1.0, django-cacheops~=7.0.2, django-ckeditor~=5.9.0, django-contrib-comments~=1.9.2, django-cors-headers~=3.7.0, django-debug-toolbar~=3.5.0, django-elasticsearch-dsl~=7.1.1, django-filter~=2.3.0
  0:00:09 ? Lock failed.  ERROR: 
  0:00:09 ? Lock failed.  
See ~/.local/state/pdm/log/pdm-lock-4aauctl8.log for detailed debug log.
[ResolutionImpossible]: Unable to find a resolution
WARNING: Add '-v' to see the detailed traceback

Langchain


Опять мой любимый фреймворк, на этот раз он говорит мне, что в функции parse_json_markdown аргумент json_string — это markdown строка. Ну теперь-то всё стало понятно!


def parse_json_markdown(
    json_string: str, 
    *, parser: Callable[[str], Any] = parse_partial_json
) -> dict:
    """
    Parse a JSON string from a Markdown string.
    Args:
        json_string: The Markdown string.
    ...
    """

Vscode


Свою IDE я вообще боюсь. Даже если она правильно работает, всё равно в логах developer tools постоянно мясо: какие-то ошибки, deprecation warnings, угрозы, что всё не работает и прочее, и прочее. Мне кажется, там какая-то своя атмосфера, и я только успеваю удивляться, как при всём этом оно работает.



Хотя что это я, какое «работает»‬? Вот, например, No definition found for "_request", хотя этот самый _request всего несколько строчек ниже:



Celery flower


Есть такая софтина: celery flower. Как-то я хотел её хитро настроить и спросил её, что она умеет:


> celery flower --help
Usage: celery flower [OPTIONS] [TORNADO_ARGV]...
Web based tool for monitoring and administrating Celery clusters.
Options:
  --help  Show this message and exit.

А она мне и отвечает: «я умею только показывать вот эту помощь, которую ты сейчас видишь, а чтобы её посмотреть, нужно меня вызвать с ключом --help, как ты и сделал. Что тебе ещё надо, пёс?».‬


Copilot


Иногда Github Copilot тоже думает, что я пёс, и дополняет строчку только наполовину, обрывая автокомплит где-то посередине. В этом есть резон: умный робот заботится, чтобы я не разучился программировать.



ChatGPT


Лень — моё второе «я»‬. Зачем читать документацию, если можно спросить у ChatGPT?


И вот я в Github задал некоторые «repository variables»‬. Это просто какие-то значения, которые можно использовать внутри github actions. Мне они нужны были, чтобы ставить дополнительные пакеты при деплое, но это не важно. Важно — это как к ним обратиться в yaml-файлах? Сейчас спрошу у ChatGPT, дело на 5 секунд, зашли и вышли...


Ну да, ну да





















Экранов 15 я пытался добиться ответа от языковой модели, а она не знала! Но самое ужасное — что модели не могут сказать «я не знаю»‬, а вместо этого пытаются выдать хоть что-то, неважно, правда это или нет.


Пришлось искать ответ самому. Эх...


Ruff


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


last_weights = Weights.objects.order_by('-created_at').first()
if (
    last_weights and
    last_weights.revealed_at is None and
    last_weights.block <= current_block_number - (reveal_weights_interval - reveal_in_advance)
):

Видите, тут условия красиво выровнены. Но у ruff есть идея, как сделать ещё красивее! Ну-ка...


last_weights = Weights.objects.order_by('-created_at').first()
if (
    last_weights 
    and last_weights.revealed_at is None
    and last_weights.block 
    <= current_block_number - (reveal_weights_interval - reveal_in_advance)
):

Ну да, строчки должны начинаться с and, а если какая-то из строк начинается с <= — так вообще красота неописуемая!


Или вот — сложный list comprehension, я его написал максимально читаемо:


 calls = [
    ("archive", dump.netuid, block)
    for block in list(chain.from_iterable(dumpable_blocks))[::-1]
    if block != 1673
]

Но ruff говорит, что вот так ещё красивей:


 calls = [
    ("archive", dump.netuid, block) for block in list(chain.from_iterable(dumpable_blocks))[::-1] if block != 1673
]

Спасибо, ruff, так гораздо лучше. Как, говоришь, тебя удалить?


React


Документация тоже всегда-всегда классная.


Решил я как-то поизучать React. У них там в туториале крестики-нолики, с таким вот кодом:


  const [squares, setSquares] =
          useState(Array(9).fill(null));
  function handleClick() {
    const nextSquares = squares.slice();
    nextSquares[0] = "X";
    setSquares(nextSquares);
  }

Поле — это массив из 9 элементов, при нажатии на любое поле ставится крестик… но только в левое верхнее поле!



Я реально сломал себе мозг, пытаясь понять, ПОЧЕМУ. Может, в javascript всё само как-то по-хитрому сдвигается, а я не знаю? Короче, залип минут на 5, так и не понял ничего, решил читать дальше. А дальше написано:


Теперь вы можете добавлять крестики на доску… но только в верхний левый квадрат. Ваша функция «handleClick»‬ жестко запрограммирована для обновления индекса для верхнего левого квадрата (0). Давайте обновим «handleClick»‬, чтобы иметь возможность обновлять любой квадрат.


❯ Миф 8: Это программирование, поэтому всё надёжно


Недавно я узнал, что на github можно достать коммиты из удалённых или приватных репозиториев.



Раз!


  1. Вы создаете публичный репозиторий
  2. Вы добавляете код в свой форк
  3. Вы удаляете свой форк
  4. Код из удалённого форка всё ещё всем доступен ;)


Два!


  1. У вас есть публичный репозиторий на GitHub.
  2. Пользователь форкает ваш репо
  3. Вы делаете коммит после его форка (и он никогда не синхронизируют свой форк с вашим репо)
  4. Вы удаляете весь репозиторий
  5. Код, который вы закоммитили, всё ещё доступен! ;)


Три!


  1. Вы создаете приватный репо, который в конечном итоге станет публичным
  2. Вы создаете ещё одну приватную версию этого репозитория (посредством форка) и пишете там какой-то супер-секретный код
  3. Вы делаете свой первый репозиторий общедоступным (а форк остаётся приватным)
  4. Супер-секретный код всем доступен! ;)


Почитайте, мир не будет преждним: https://trufflesecurity.com/blog/anyone-can-access-deleted-and-private-repo-data-github.


❯ В заключение


Я не считаю себя каким-то особенным. Я просто работаю и иногда записываю что-нибудь интересное с работы, и я вижу, как мои коллеги периодически тоже постят подобные истории — а значит, страдают все.


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




Подписывайтесь, чтобы узнать, как редко я пишу в телегу: Блог погромиста




А ещё я держу все свои проекты у одного облачного провайдера — Timeweb. Поэтому нагло рекламирую то, чем сам пользуюсь — вэлкам:


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале

Комментарии (87)


  1. SSranked
    21.09.2024 08:31
    +5

    Момент с «мини-рефакторингом» не понял. Выглядит как изменение ради изменения.


    1. ainoneko
      21.09.2024 08:31
      +1

      И epoch.stop же будет возвращать 201, а не 200? :)


      1. kesn Автор
        21.09.2024 08:31

        Ага, я неправильно написал в статье :(


  1. AnimeSlave
    21.09.2024 08:31
    +12

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


    1. apoltavcev
      21.09.2024 08:31
      +7

      Плюсую! Постмодерн, симулякры и прочий Бодрийяр)

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

      Со временем реальность искажается больше: кто-то первым начинает приукрашивать чуть сильнее, остальные подтягиваются до этого уровня — и вот уже будущим разрабам обещают 300k в наносекунду за работу, с которой якобы и дурак справится.


  1. DashBerlin
    21.09.2024 08:31
    +14

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


    1. kuzzdra
      21.09.2024 08:31

      поиск ошибок при запуске контейнеров

      Это не программирование. debug.


      1. DashBerlin
        21.09.2024 08:31

        согласен, это один из инструментов, как и IDE, которые болью откликаются при работе


      1. omxela
        21.09.2024 08:31
        +3

        Это не программирование.

        После беглого прочтения статьи, у меня возникло ощущение, что она вся целиком - не о программировании, если не понимать этот термин максимально расширительно. Сложности обитания в конкретных средах проектирования - это проблемы кодировщика (со всем уважением), а не программиста. Имхо.


        1. Vladekk
          21.09.2024 08:31
          +3

          Дык а программист всегда и сразу такие проблемы решает?


        1. netch80
          21.09.2024 08:31

          Сложности обитания в конкретных средах проектирования - это проблемы кодировщика (со всем уважением)

          "The myth of the coder", свежатинка.

          Но как минимум случай с PgBouncer это уже архитектурный вопрос. А доступность вроде бы удалённого в github - уже уровень общего секьюрити.


    1. AnimeSlave
      21.09.2024 08:31

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

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


    1. Shado_vi
      21.09.2024 08:31
      +1

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


    1. voldemar_d
      21.09.2024 08:31

      Ещё бывает отладка многопоточных приложений с гонками, deadlocks и прочими весёлостями.

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


  1. temaweb10
    21.09.2024 08:31
    +2

    Привет с PythonNN


  1. mallo_c
    21.09.2024 08:31
    +23

    ZeroVer - это прям про Rust. КУЧА библиотек имеют версию 0.*. Смотрим:
    axum - v0.7.6
    tower - v0.5.1
    sqlx - v0.8.1

    Окей, с вебом понятно, может с чем-то базовым получше?
    rand - v0.8.5
    num - v0.4.3
    hashbrown - v0.14.5
    itertools - v0.13.0

    И, наконец, мой любимец:
    base64 - v0.22.1

    Алгоритм кодирования в base64 же у нас каждые полгода меняется и всё никак не хочет стабилизироваться, верно? :]


    1. Gorthauer87
      21.09.2024 08:31
      +1

      derive-more пару лет имел версию 0.99


    1. netch80
      21.09.2024 08:31
      +4

      Алгоритм кодирования в base64 же у нас каждые полгода меняется и всё никак не хочет стабилизироваться, верно? :]

      А вдруг на следующей машине в байте будет не 8, а 8.5 бит?
      (крякает и убегает)

      А ещё лучше делать версии с двумя нулями! Вот например Discord клиент для Linux - у меня последняя сейчас 0.0.64. Это значит, меняться может всё.


      1. kuzzdra
        21.09.2024 08:31

        Алгоритм то может и не меняется, а реализация вполне может. С оптимизацией под многопоточность, под особенности конкретного процессора... Прежде чем хихикать, надо в changelog смотреть ;)


        1. iroln
          21.09.2024 08:31
          +1

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

          С ZeroVer тоже можно гарантировать, что изменение патч-версии не сломает обратную совместимость. Вот, например, caret requirements в Poetry.

          Другое дело, что зачастую разработчики не придерживаются вообще никаких правил и ломают что угодно когда угодно, хотя вроде бы на словах придерживаются SemVer.


  1. Advisers
    21.09.2024 08:31
    +6

    А чтобы "добавить энтузиазма" начинающим - надо иметь ввиду, что скажем лет через 5-10-20... весь ваш опыт с нынешним стэком будет никому не нужен, ибо будут новые языки, новые железки, новые экосистемы (поверьте, так уже было и не раз...) и вам придётся все учить снова, и снова, и снова... если конечно эту инициативу у вас не заберёт ЭйАй )

    - а вот карандаш и бумага - живёт дольше...) остальное в песок...


    1. Shado_vi
      21.09.2024 08:31
      +5

      вы про операторов фреймворков и им подобных.
      нормальный программист-разработчик со временем/опытом будет понимать как работает код на глубоких уровнях и сможет на не знакомом ЯП за пол часа написать полезную программу.


      1. Advisers
        21.09.2024 08:31
        +2

        Не уверен, что за пол часа разберётся..., тем более никакая приличная компания не даст писать в прод без опыта в стэке и в языке.


      1. Yuuri
        21.09.2024 08:31

        Сможете сходу написать что-то полезное на Haskell, Prolog, Common Lisp и Forth?


      1. edogs
        21.09.2024 08:31

        сможет на не знакомом ЯП за пол часа написать полезную программу.

        За полчаса может полезную и сможет. А вот сможет ли написать качественную за месяц?
        Это как взять переводчика не знающего китайский и сказать что он за полчаса напишет статью на нем. Просто потому, что он 20 лет лингвистом работает. Да, напишет, но насколько хорошо? А если надо не статью, а книгу?
        Не зная язык, его особенностей, его функций, его библиотек, его экосистемы, практики его применения - ничего качественного на серьезном уровне не напишешь.
        Эта современная манера рассуждать, что мол "язык знать не надо, ИДЕ подскажет да и погуглить можно" звучит как "японский знать не надо, есть словари и гугл транслейт".


    1. voldemar_d
      21.09.2024 08:31
      +3

      Учите c++ или Fortran, они будут всегда :)


      1. dalerank
        21.09.2024 08:31
        +1

        Ага-ага, вам ещё зоопарка ошибок 30 летней выдержки не хватает? Сижу вот и плачу над комитом 96 года, и починить вроде надо, и изменения в легаси у нас апрувят от слова никак.


        1. voldemar_d
          21.09.2024 08:31
          +1

          Каких ошибок? Используйте C++30, потом C++33 и т.д.


          1. perfect_genius
            21.09.2024 08:31

            *C++29, потом C++32 и т.д.


  1. dimas846
    21.09.2024 08:31

    Можно добавить в список обновление зависимостей. Например в Symfony или npm


    1. zoto_ff
      21.09.2024 08:31

      что с ним не так? если не тянете на каждую задачу по зависимости, используете только качественные библиотеки - никаких неожиданных проблем не будет


  1. iroln
    21.09.2024 08:31

    Вот так я узнал, что click и argparse работают по-разному, но чтобы это узнать, нужно поставить куда-нибудь знак «=»‬. Люблю программирование!

    click использует старую библиотеку optparse из stdlib. Она именно так и работает. Проверьте. :)

    from optparse import OptionParser
    from argparse import ArgumentParser
    import click
    
    def prog_optparse():
        p = OptionParser()
        p.add_option('--queues', '-Q')
        print(p.parse_args())
    
    def prog_argparse():
        p = ArgumentParser()
        p.add_argument('--queues', '-Q')
        print(p.parse_args())
    
    @click.command()
    @click.option('-Q', '--queues')
    def prog_click(queues):
        print(queues)
    
    if __name__ == '__main__':
        prog_optparse()        # (<Values at 0x7f2c115e7890: {'queues': '=values'}>, [])
        # prog_argparse()        # Namespace(queues='values')
        # prog_click()           # =value
    

    А библиотека Celery - это вообще дно, не надо этим пользоваться. :)


    1. zoto_ff
      21.09.2024 08:31
      +14

      в питоне каждая библиотека - дно, если так смотреть. всё из-за низкого порога входа и разработчиков, которые сами не понимают что делают


      1. kirekov
        21.09.2024 08:31
        +1

        Вообще, многие проблемы в статье решаются использованием языка со статической, а не динамической типизацией. Взял ту же Java/C#/Scala/Rust/etc и забыл о проблемах «я не правильно указал символ в django-конфиге».

        Примеры с JavaScript валидные, но надуманные. Если брать реакт, то вряд ли кто-то будет писать проект с нуля на JavaScript, а не typescript.

        Меня всегда интересовало, зачем вообще использовать динамические языки для чего-то сложнее, чем скрипт/или dag в airflow? Берешь статическую типизацию и больше не надо есть кактус


        1. kruslan
          21.09.2024 08:31
          +1

          Вы СИЛЬНО ошибаетесь (((

          • стат. типизация не решает проблемы кривых рук (говорю, как обладатель). Ничто и никогда не помешает использовать any (((

          • новый проект на реакте без ts - ЭТО НОРМА! наблюдение последних 4-5 лет.

          • динамическая типизация - не проблема. проблема - не правильное использование.


          1. lennylizowzskiy
            21.09.2024 08:31

            • стат. типизация не решает проблемы кривых рук (говорю, как обладатель). Ничто и никогда не помешает использовать any (((

            Использование Any (или подобного ему псевдотипа) в сколько-нибудь осмысленной форме возможно только в TS. Автор комментария не приводил его как пример статической типизации (её там, собственно, и нет)


            1. Lirik-Keltran
              21.09.2024 08:31
              +1

              В TS статическая типизация, мне было бы интересно услышать аргументы против этого.


          1. netch80
            21.09.2024 08:31

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

            Если её максимально использовать - решает. По крайней мере количество проблем типа "хотел число, получил строку" сокращает в десятки раз.

            Бывают и более хитрые ситуации. Недавно в одном проекте заменял везде секунды на миллисекунды. Если бы это был C++, то его типизация в chrono не пустила бы меня передать значение другого типа, компиляция бы свалилась и я бы был вынужден вставлять преобразования. Но это Python, и всем там плевать, поэтому я менял названия переменных, полей всяких JSON и функций, чтобы линтеры и потом тесты показали, где я недоработал. (Можно потом обратно поменять, если нужны фиксированные имена, но промежуточное состояние должно быть зафиксировано отдельным коммитом.)

            динамическая типизация - не проблема. проблема - не правильное использование.

            Когда "правильное использование" динамической типизации в разы дороже статической, это таки проблема. "Зачем платить больше?" (c)


        1. 3263927
          21.09.2024 08:31

          уже лет 10 на C# программирую, ни одной из этих проблем не встречал... ну JS да куролесит иногда, но щас есть blazor поэтому можно JS вобще не использовать



  1. Igorello74
    21.09.2024 08:31

    Насчёт pytest, pdm и relative import. Прочитайте внимательно ошибку. Там речь про "top-level package" — пакет, а не модуль. Теперь внимательно смотрим, что в питоне понимается под пакетом: папка, содержащая __init__.py


    1. iroln
      21.09.2024 08:31
      +4

      Частая ошибка разработчиков в Python, что они пытаются импортировать пакеты и не через relative, и не через absolute import. Вот простой пример:

      foo
        __init__.py
        types.py
        logging.py
        bar.py
      

      И кто-то пишет в bar.py: import types, logging и получает конфликт с stdlib, импортирует stdlib пакеты вместо своих.
      Потому что надо либо from . import types, либо from foo import types, то есть использовать либо относительный, либо абсолютный импорт. И это всегда будет работать как ожидается. Импортируют неправильно и вылезают баги в самых неожиданных местах, потому что система импорта в Python на самом деле сложная и запутанная.


  1. Ant80
    21.09.2024 08:31
    +10

    Хирургия - это просто. Режем лягушку за 15 минут.

    Автомеханика - это просто. Меняем колесо за 10 минут.

    Ну и так далее.


    1. Advisers
      21.09.2024 08:31
      +1

      Учёба - это просто. Научу чему угодно за минуту.

      Подписывайтесь. Ставьте лайк.


      1. voldemar_d
        21.09.2024 08:31
        +1

        А где ссылка на ТГ-канал?


        1. anonymous
          21.09.2024 08:31

          НЛО прилетело и опубликовало эту надпись здесь


      1. netch80
        21.09.2024 08:31

        По бразильской системе?


        1. timur_sh
          21.09.2024 08:31

          Забавный факт: сами бразильцы про эту "бразильскую систему" никогда не слышали.


          1. voldemar_d
            21.09.2024 08:31

            Так же, как и французы про "мясо по-французски", залитое маянезиком.


          1. timur_sh
            21.09.2024 08:31

            В другую сторону тоже работает, кстати. В некоторых странах Европы наш "оливье" называют "русский салат".


            1. geher
              21.09.2024 08:31

              А еще есть горки, которые в Америке русские, а в России американские, французский насморк, который во Франции, если не путаю, английский


      1. Advisers
        21.09.2024 08:31

        И да... миллионы ботов уже подписались, лайкнули, и откомментили свой восторг..., а вы все ещё думаете? )


  1. iroln
    21.09.2024 08:31
    +1

    Спасибо, ruff, так гораздо лучше. Как, говоришь, тебя удалить?

    ruff ещё и "очень логичный". Чтобы запустить авто-форматтер вместе с линтером и фиксами надо вот так и никак иначе. :)

    ruff format . && ruff check --fix .
    

    То есть, просто check делается вот так:

    ruff check .
    

    А проверка формата (например, в CI) вот так:

    ruff format --check .
    

    Всё очень логично и консистентно, правда же? :)
    У них даже issues на этот счёт есть (лень искать).

    Но вообще тула очень быстрая и в целом норм. Можно не удалять, можно добавить метакомменты для пометки кусочков кода, чтобы он туда не лез со своим "правильным" форматированием.


  1. netch80
    21.09.2024 08:31
    +8

    С опцией - как раз click работает тут так, как работают подобные опции вообще изначально, начиная с libc getopt() и большинством его продолжателей. Однобуквенные опции (которые к тому же с одиночным дефисом перед ними) имеют жёстко заданное наличие или отсутствие параметра, и параметр пишется или следующим argv, или до конца того же, но без всяких '='. Упомянутый рядом optparse работает точно так же. То есть для них всех "-Qdefault" или "-Q" "default", но никак не "-Q=default".

    Формат с = для длинных опций (которые начинаются уже с двух дефисов) введён для того, чтобы, в первую очередь, можно было твёрдо определять конец названия опции; во вторую, чтобы можно было делать вариант optional_argument - у таких опций, если он есть, идёт после '=' в том же argv (не в следующем). Это разработка ещё 80-х, POSIX.2 (но у него вместо -- было -W ; -- это уже доработка GNU).

    Наоборот, то, что делает argparse, это что-то неожиданное (я вот впервые из вашей статьи узнал про это) и я не понимаю, почему они так сделали, сломав совместимость. Может, это вообще баг, надо поискать, были ли на него тикеты.

    Собственно, общего вывода вашей статьи это не нарушает. Но отношение к собственному примеру у вас какое-то странное.

    (продолжение, возможно, следует. у вас много букв;))


    1. iroln
      21.09.2024 08:31

      Согласен. Думаю, в argparse как раз баг, потому что короткие (односимвольные) опции по стандарту с = не пишутся. Всё, что идёт после опции является значением, поэтому там даже пробел между опцией и значением не обязателен.


      1. netch80
        21.09.2024 08:31
        +5

        Зафайлил баг: https://github.com/python/cpython/issues/124305

        Наблюдаем...


  1. Anti-antivakser
    21.09.2024 08:31
    +1

    Весь этот бред нелогичности, абсурда и безумия в кодах создали люди)) Просто из статьи кажется будто эти программы появились из ни откуда сами по себе. Источник абсурдных решений в кодировании это люди которые сами не понимают что делают, то есть не профессионалы, либо если вы профи то в очень редких случаях вы сами что-то не допонимаете и то или ное решение только кажется нелогичным. Вся эта статья отличный пример того, что ИИ еще далеко до того, чтобы полностью заменить профессионалов программистов, правда если верить последним новостям то ИИ о1 чуть ли не бог программирования в сравнении с чатом гпт4.

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

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

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


    1. voldemar_d
      21.09.2024 08:31
      +3

      На Хабре уже немало статей в духе "как я за полчаса создал сложную программу с помощью ChatGPT, программисты скоро станут не нужны". Но мало создать программу, даже если это получилось успешно и быстрее, чем если бы её человек сам делал с нуля.

      Следующий уровень - развивать и поддерживать программу, в том числе по замечаниям и пожеланиям пользователей. И вряд ли ChatGPT сумеет выполнить запрос "переделай готовую программу так, чтобы она приносила в 10 раз больше денег, чем сейчас".


    1. Advisers
      21.09.2024 08:31

      "придется в слепую доверять результату ИИ"

      - и почему "ИИ" вообще "захочет" делать бесконечно что-то полезное для человека?


    1. SamMetalWorker
      21.09.2024 08:31

      придется в слепую доверять результату ИИ

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

      Хороший ИИ может работать так же, как и наёмный профессионал, в диалоговом режиме, с уточняющими вопросами, конструктивной критикой идеи и её согласованием от общего к частному.


  1. voldemar_d
    21.09.2024 08:31
    +8

    Вот прямо вчера было. Взял репозиторий из рабочего (локального) github. Что-то поисправлял, сделал commit. А push не проходит - Visual Studio пишет сообщение "не могу, смотрите логи".

    Пошёл то же самое делать в командной строке. git add, git commit - всё ок.

    git push выдаёт: repository not found! Да как же не найден, ты же сам мне показываешь URL, из которого я час назад сделал clone?

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

    Ну хорошо, это всё понятно, и догадаться, наверное, можно было сразу, но почему в сообщении написано "repository not found", а не "access denied" или хоть что-то про права доступа?

    Напоминает какую-то древнюю историю, когда база данных кому-то выдала сообщение "не могу".


    1. gyzl
      21.09.2024 08:31
      +3

      Точно так же работает Oracle, если нет прав на таблицу: выдает "table or view does not exist". Кажется, это сделано для того, чтобы атакующий подбором имен объектов получал меньше информации.


      1. voldemar_d
        21.09.2024 08:31
        +6

        Я догадывался, что здесь есть технические причины для этого. Но кто мешает в клиенте добавить в сообщении подсказку вроде "Check your access rights"? Тем более, клиент вполне в состоянии понять, что раз я смог из этого же репозитория получить проект, значит, хотя бы в момент получения этот репозиторий был existing. Можно и так написать: please check that repository still exist and you have enough rights for pushing to it.


    1. saboteur_kiev
      21.09.2024 08:31

      но почему в сообщении написано "repository not found", а не "access denied" или хоть что-то про права доступа?

      Чтобы потенциальный взломщик не мог перебрать названия существующих репозиториев по access denied.


  1. Octabun
    21.09.2024 08:31

    Примеры показывают только одно - программирование это не написание литературного текста. Оно всё ещё требует точного (ещё раз для читателей статьи считающих что в статье описаны реальные проблемы - ТОЧНОГО, господа егэшники) понимания, и уже создаёт иллюзию что всё можно нагуглить. Это плохо, а когда добавится иллюзия с AI - будет очень плохо, это как с пятницей 13-го и субботой 14-го.

    Как капля в которой отражается океан -

    В питоне тоже всё просто, скобки всегда означают одно и то же...

    • да, всё просто, не веришь не видишь не понимаешь - отойди от компа навсегда

    • с какой стать скобки должны означать одно и то же, ведь тогда они смогут означать только что-то одно

    • скобки сами по себе ничего не означают

    Когда человеку что-то говорят, в том числе письменной речью, нужно стремиться понять что тебе говорят. Но строить свою собственную теорию и усваивать её - проще. Программистов сужественно больше чем людей которым энергетические ресурсы мозга позволяют поступать иначе. Это не завуалированное оскорбление, это медицинский факт. Не понимать смысл == и === в JavaScript - естественное следствие.

    Кто виноват - капиталистический способ производства (поганого) программного обеспечения.

    Что делать? Пользоваться дарованной Господом в Его беспричинной милости свободой воли, как именно - утверждение рекурсивное.

    Нытьё бесит, но наверно не всех, чем и следует утешаться.


    1. andmerk93
      21.09.2024 08:31
      +8

      господа егэшники

      Егэшникам по 30+ лет, дедуля. Это взрослые люди, многие уже с семьями и ипотеками. Повежливее надо бы с коллегами.


  1. DuhovichSasha
    21.09.2024 08:31

    Вы были бы очень хорошим следователем


  1. qiper
    21.09.2024 08:31
    +4

    Плюс десктоп на Электроне


  1. Daddy_Cool
    21.09.2024 08:31

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


  1. DrArgentum
    21.09.2024 08:31

    А я говорил, надо становиться было магом-целителем премиум класса. Там хотя бы нету магии, как в погромировании


  1. geher
    21.09.2024 08:31
    +2

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

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


    1. onets
      21.09.2024 08:31
      +1

      Это же шрёдинбаг


  1. rukhi7
    21.09.2024 08:31
    +4

    А главное не то что все не так просто, а то что оно все не-проще и не-проще со временем.

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

    Скачать сотни репозиториев из Гита чтобы скомпилировать какой-то Хело-Верд это уже практически какой-то стандарт "нормального" проэкта. Мне кажется не долго осталось.


  1. Ascar
    21.09.2024 08:31

    Занимайтесь тем что вам по душе.


  1. Arkanium77
    21.09.2024 08:31

    Согласен с некоторыми из предыдущих ораторов. Автор, как будто, говорит "Рисование - это не просто! Вот поглядите: Картину художника Х украли из охраняемого музея. Или ещё, другой художник упал с лестницы когда расписывал стену".

    Программирование - это просто. Не всегда очевидно, но не сложно. Другое дело, что вокруг программирования есть ещё много других активностей - от глубокого погружения в чужой код до ведения проекта, от строения сетей до всратых собесов в 20 этапов. И что с того? Вокруг любой деятельности можно найти кучу всего, чем придется заниматься помимо неё.
    Другое дело, что в отношении программирования со всем этим можно разобраться в разумные сроки и, если не решить, то хотя бы обойти проблему. Плюс эксперименты бесплатные. Физикам и химикам остается о таком только мечтать


  1. saboteur_kiev
    21.09.2024 08:31

    Поэтому он выполняет команду false, она возвращает ошибку, но по умолчанию (чья это была идея, блин?) баш игнорирует ошибки и выполняет команды дальше — а дальше rm -rf ~. Удобно? Удобно.

    По дефолту баш не останавливает выполнение, если какая-то из команд завершилась с ошибкой, и это нормально - ВСЕ языки так делают. Если не продолжать выполнение, то то банальное if/else невозможно будет создать, потому что if должен всегда быть true.

    В баш есть возможность поменять это условия для определенных моментов, например отладка или какие-то специфичные пайплайны, где любой не true должен привести к остановке скрипта. То, что кто-то добавил опцию в интерпретатор, так причем тут баш?
    Добавьте какой-нить #!/usr/bin/python /etc/passwd
    И посмотрим почему этот скрипт не хочет нормально выполняться, а почему-то начинает пытаться выполнить /etc/passwd

    P.S. А в целом статья шикарная


  1. Drucocu
    21.09.2024 08:31
    +2

    fk my life once again


  1. onets
    21.09.2024 08:31
    +1

    Из моей копилки, последнее. Mysql 8.0 innodb, который блокировки накладывает по строчно.

    Код в скрипте и код в скрипте внутри хранимки должен же работать одинаково? Правда же ведь?

    SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    START TRANSACTION;
            
    INSERT INTO MyTable
    SELECT Name, ... другие поля...
    FROM MyTable
    WHERE Id = @MyId;
        
    COMMIT/ROLLBACK;

    Вот эта штука, при параллельном запуске с двумя разными @MyId внутри хранимки работало так, будто блокирует всю таблицу. Если запускать вне хранимки - работает как и описано в документации, с блокировкой по строкам.

    Так то дело было в уникальном индексе на одно из полей в списке select, что как бы логично. Но какого хрена…???


  1. ainoneko
    21.09.2024 08:31

    Почему так? Потому что «датаклассовость»‬ не наследуется.

    А если бы наследовалась? Пришлось бы добавлять декоратор @not_a_dataclass_anymore_ha_ha, чтобы отменить, когда нужно?


  1. Fedorkov
    21.09.2024 08:31

    Миф 2: У нас есть версии и системы контроля версий, так что ничего не ломается и всё отслеживается

    Миф 7: Это программирование, поэтому помощь повсюду: документация, IDE, LLM, …

    Миф 8: Это программирование, поэтому всё надёжно

    Если человек верит рекламе, что программирование — это просто, то он вряд ли мыслит в категориях VCS, IDE и качества кода.


  1. netch80
    21.09.2024 08:31

    По сумме всей статьи.

    ​1. Категорически согласен с общим выводом. Почти все примеры не из моих областей, но я их понимаю и верю описанному. (Ну с argparse уже разобрались до уровня багрепорта.) Мог бы привести пачку своих аналогичных, но это было бы повторением того же с другой спецификой.

    ​2. Но теперь нужно переходить к двум главным вопросам, как обычно. Но при этом на метауровне. Главное, "Что делать?" превращается в "Для чего именно - что делать?" Первая проблема - обучение. Уже были голоса, что заметная часть входящих в IT спотыкается именно на том, что на них наваливается реальная сложность, и должно хватить сил для её преодоления. А ещё это создаёт барьер между теми, кто "вошёл", и теми, кто не идёт напрямую в IT, но мог бы брать на себя простейшие задачи. Вторая - как в реальных системах уменьшать избыточную сложность, а перед этим - как её детектировать и помечать. Одна из этих задач более техническая, а две - гуманитарные, в хорошем смысле.


  1. souls_arch
    21.09.2024 08:31
    +3

    Коммит с названием maybe ready for prod гениален даже в одном экземпляре. И по бессмысленности своей и по кровожадности. Что уж говорить про несколько. Мне вот интересна была бы подборка комментов от лидов и ревьюверов к непринятому PR с подобным названием. Желательно без незапиканных матов.


    1. SeApps
      21.09.2024 08:31

      Писал похожие коммиты, будучи лидом.

      Это же не значит что чел запускает код на прод, как правило код запускается на тестовую или бета-тестовую среду (хотя даже бета-тест это все равно прод в большинстве случаев), и все равно пройдет этап тестирования тестировщиками / контрольной группой.


  1. stpotanin
    21.09.2024 08:31

    А когда-то я искренне полагал, что лучшие байки - армейские.


  1. IvanSTV
    21.09.2024 08:31

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

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

    При понимании, что все ЯП - языки искусственные, половина описанных в статье сложностей просто пропадает. Да пофигу, обозначают скобки одно и то же или что-то разное в разных случаях. Просто держим в уме это и работаем. Никто не требует от английского отказаться от некоторых явно излишних форм прошедшего времени, которые причем сами носители употребляют неправильно в более чем половине случаев, кроме продвинутых филологов. И все нормально употребляют язык. Так и тут. Многие вещи непривычны новичку (как сложение в JS), но это искусственный язык, у него может быть произвольная логика и еще более произвольный синтаксис. Ну, типично - вон в экселе в стандартных формулах параметр 0 означает строгое соответствие, единица нестрогое, я в пользовательские функции писал буквенные параметры, просто чтобы юзера читали хелпик, так как надо было понимать, что ставишь, а не просто по дефолту, потому что "у VLOOKUP вот так, и я привык". Логика некоторых вещей в ЯП скрыта или исторически обусловлена. Это данность с ней и живем. К вопросу, с множеством нелогичных или непонятных вещей как с данностью работают, и весьма успешно, во многих областях.

    Про документирование и хелпики. Ну, не везде. Напороться в прикладном программировании на незадокументированную или не описанную где-то функцию или проблему надо еще суметь. Описанные в статье случаи сколько времени собирались? Если неделю, то да, программирование - это сложней, чем бегать по минному полю. А у меня вот всяких странностей, сложностей, нелогичностей, ошибок и прочего с объемом на статью лет за 10 разве что накопилось. И ситуаций, когда я нигде помощи найти не мог, буквально три за последние три года. Одна связана с аппаратными проблемами. Только одна из ситуаций как-то повлияла на работу и зарплату (долго возился, развел руками и отдал на аутсортс).

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


  1. Mausglov
    21.09.2024 08:31
    +1

    С пробелом и я сегодня столкнулся. Потребовалось мне воспользоваться магией ./configure , которому передаётся куча параметров. И добрый человек в примере записал это так:

    ./configure \
       --with-a \
       --with-b \
       (и так далее..)

    Поскольку мой опыт в этой магии близок к нулю, я эту команду строил в текстовом файле. Сделал, копирую в консоль - не работает. И ошибки какие-то странные:

    configure: WARNING: you should use --build, --host, --target
    configure: WARNING: invalid host type:
    configure: WARNING: unrecognized options: --with-webp-dir

    Причём в примере никакие "--build, --host, --target" не указаны 100%!

    Разгадка проста в строчке перед "--with-webp-dir" у меня после обратного слеша затесался пробел, и обратный слеш стал экранировать не перевод строки, а этот самый пробел.