Мой опыт с резиновым мужиком. Github Copilot

Первая моя ассоциация с Github Copilot — это резиновый мужик-автопилот из фильма «Аэроплан». Помню, в детстве я увидел этот фильм и потом реально думал, что так автопилоты и выглядят: какая-то смешная резиновая кукла надувается и начинает управлять самолётом. И вот, через столько лет меня не покидает ощущение, что передо мной надувается какая-то кукла и пытается писать за меня код.

Короче, нежданно-негаданно мне пришёл инвайт в GitHub copilot. Я и забыл про то, что когда-то запрашивал доступ — ажиотаж давно уже спал, вроде мир не захвачен машинами, а значит, не такой уж он и крутой, да? С другой стороны, недавно к нам на позицию senior python постучал чел и сказал, что большую часть нашего тестового задания он запилил при помощи copilot, так что определённо кто-то «пилотом» пользуется. В общем, непонятно: хорошо оно или плохо? Стану ли я теперь более продуктивным? Есть только один способ проверить: в бою.

Как тестировать?

И тут я задумался.

Реальные проекты разные.

В copilot ты пишешь комментарий или код, и по нему работает автодополнение. И вот тут мне показалось плохой идеей писать что-то вроде:

# this is a bullshit code written by previous developer,  # zero tests (of course), please run it and see where it breaks  # and try to fix; also update all dependencies to latest  # (read changelogs and migrate db and code where needed)

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

def bullshit_code():

Короче, такие абстрактные вещи для copilot не подходят. Лучший кейс, как я понял — создание какой-нибудь функции, класса, метода или сниппета кода. Чего-то маленького, с ограниченной логикой.

Давайте всё это и попробуем. А под конец узнаем, кто всё-таки будет писать код: кожаный мешок или резиновый мужик.

Сразу предупрежу, что мужик может выдавать до 10 вариантов в режиме «выбирай что тебе больше нравится», но я буду больше всего внимания уделять первым ответам в списке, потому что

  1. первый вариант в списке — это вариант с наибольшей вероятностью (а copilot, как и gpt, просто предсказывает наиболее вероятное продолжение текста/кода), и значит у мужика этот первый ответ в приоритете (сугубо моё предположение)

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

Мини-функции

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

Разбитие по чанкам

Я прям беру описание из more_itertools::chunked

Break iterable into lists of length n

def chunks(lst, n):     for i in range(0, len(lst), n):         yield lst[i:i + n]  def chunks(lst, n):     return [lst[i:i+n] for i in range(0, len(lst), n)]

Два варианта, один генератор, другой просто возвращает список. Это годно!

Lookahead без повреждения генератора

Тоже из more_itertools::spy

Return a 2-tuple with a list containing the first n elements of iterable, and an iterator with the same items as iterable. This allows you to “look ahead” at the items in the iterable without advancing it.

По факту мы должны создать кэш из n первых элементов, вернуть этот кэш и также вернуть «восстановленный» генератор.

def spy(iterable, n=1):   it = iter(iterable)  # о! превращаем iterable в iterable :)     cache = []     for i in range(n):         try:             cache.append(next(it))         except StopIteration:             break     return cache, it

Вообще не то! Тут вроде есть cache, и возвращается даже правильная структура, но внутри у copilot произошла шизофазия. Должно быть что-то такое:

from itertools import chain, islice   def spy(iterable, n=1):     head = list(islice(iterable, n))     return head, chain(head, iterable)

Самая частая буква

Как-то на тестовом задании мне нужно было вернуть наиболее часто встречающуюся букву. Решение я родил за 5 секунд:

from collections import Counter   def most_common_letter(text: str) -> str:     return Counter(text).most_common(1)[0][0]

Сначала мужик предлагает мне для каждого символа из текста вызывать text.count(symbol):

def most_common_letter(text: str) -> str:     return max(text, key=text.count)

Я начинаю возникать, мол, может будем экономить ресурсы? Мужик предлагает: давай вызывать text.count(symbol) для каждого символа не более одного раза:

def most_common_letter(text: str) -> str:     return max(set(text), key=text.count)

Может, встроенный Counter поюзаем?

«Окей,» — говорит мне копилот, — «вот тебе твой Counter«:

def most_common_letter(text: str) -> str:     import collections     return max(collections.Counter(text).items(), key=lambda x: x[1])[0]

Но Counter сам умеет считать максимум, а ты юзаешь max!

def most_common_letter(text: str) -> str:     import collections     return collections.Counter(text).most_common(1)[0][0]

Итого 4 варианта, из них два говно, два приемлимы.

RLE (run-length encoding)

Яндекс на собеседовании просил написать RLE функцию. Смог бы copilot работать в Яндексе?

# RLE (run-length encoding) function def rle(string: str) -> str:     result = ''     for i in range(len(string)):           if i == 0:             result += string[i]         elif string[i] == string[i - 1]:             result += '*'         else:             result += string[i]     return result  # In [21]: rle('aaabcccd') # Out[21]: 'a**bc**d'

Вообще не то! Кажется, мы только что убедились, что Copilot не умеет гуглить.

Я бы написал так:

from itertools import groupby  def rle(string):     result = ''     for char, group in groupby(string):         result += f'{char}{len(list(group))}'     return result      # In [35]: rle('aaabcccd') # Out[35]: 'a3b1c3d1'

Генератор паролей

# generate random password def generate_random_password(length: int) -> str:     import random     import string     return ''.join(       random.choice(string.ascii_letters + string.digits)       for _ in range(length)     )

Вроде ничо, но только copilot забыл, что random не настоящий рандом, и для паролей и прочих секретов есть secrets:

# use secrets to generate random password def generate_random_password(secrets: str) -> str:     import secrets     return secrets.token_urlsafe(10)

Оверкилл. Я ожидал просто:

import secrets  password = secrets.token_urlsafe(length)

Генератор умножения на 2

# iterate over large list of numbers and multiply each number by 2 def double_numbers(numbers: list) -> list:     return [x * 2 for x in numbers]

Я написал «iterate» и «large list», но мужик намёков не понял и возвращает список. Ну такое.

Сгруппируй слова

# group words that consist of same letters def group_words(words: list) -> list:     result = []     for word in words:         if word not in result:             result.append(word)     return result

Вместо того, что просили, копилот изобрёл уродливую версию list(set(words)), то есть просто вернул уникальные слова.

Удали элемент

# remove second element from list def remove_second(lst: list) -> list:     lst.pop(1)     return lst

А вот это говно. Мужик передаёт мутабельный объект, модифицирует его и его же и возвращает.

Посмотрите, как сделано в питоне: reversed() возвращает копию и не модифицирует оригинал, а .reverse() ничего не возвращает и модифицирует оригинал.
Посмотрите, как сделано в pandas: apply() возвращает копию и не модифицирует оригинал, а с параметром inplace=True ничего не возвращает и модифицирует оригинал.

Должно быть что-то одно.

Что я понял

После большинства функций я выглядел так:

Тут ко мне пришло понимание, что для надувного мужика важно, чтобы ТЗ было чётким, а не как мы с вами привыкли: «напиши чонить шоб работало как надо». По опыту знаю, что документация и ТЗ могут отнимать много времени, так имеет ли смысл писать комментарий длиннее самой функции? Вот что надо написать, чтобы мужик сгенерировал годноту:

# function which receives password length and generates random password  # of specified length; uses "secrets" module def generate_random_password(length: int) -> str:     import secrets     return secrets.token_urlsafe(length) 

Длинные функции

А давайте чтобы мужик писал больше, чем я? А то нечестно.

Класс мне запили

Сделай мне человека!

# person class class Person:     def __init__(self, name: str, age: int):         self.name = name         self.age = age      def __str__(self):         return f'{self.name}, {self.age}'  

Алё! Ты из криокамеры вылез? Где датаклассы?

# person dataclass @dataclass class Person:     name: str     age: int 

Так-то! Мы сделаем из тебя человека! Вообще можно ещё сделать @dataclass(slots=True, kw_only=True), но copilot мне не платит за обучение (но вы можете), так что учить я его не буду.

Просто сделай запрос

# makes async requests to url async def make_request(url: str):     import requests     response = requests.get(url)     return response.text

Тут бы какой-нить httpx

Читай файл с комментариями

# read file exluding blank lines and lines starting with "#" def read_file_exluding_blank_lines_and_lines_starting_with_hash(file: str) -> list:     with open(file, 'r') as f:         return [line for line in f if line and not line.startswith('#')]

А я-то думал, что это у меня проблемы с названиями для функций…

Опять же, почему не генератор? Файлы бывают большие.

Прочитай файл по кусочкам

# read file by chunks of 5 lines def read_file_by_chunks(file: str) -> list:     with open(file, 'r') as f:         return [f.readlines(5)]

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

Dict в файл

# write dictionary to file def write_dictionary_to_file(dictionary: dict, file: str):     with open(file, 'w') as f:         for key, value in dictionary.items():             f.write(f'{key} {value}\n')

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

Давайте намекнём на json:

# write dictionary to json file def write_dictionary_to_json_file(dictionary: dict, file: str):     import json     with open(file, 'w') as f:         json.dump(dictionary, f)

Почти!

  • file на самом деле надо назвать file_name

  • а вот f должен быть как раз file

  • и сдампил бы я с indent=2 хотя бы, чтобы человек тоже мог прочитать

Поменяй колонки в csv файле

# swaps 1st and 2nd csv columns def swap(csv: str) -> str:     return csv.replace(',', ';')

Ну ПОЧЕМУ?! За что?
Я попытался рассказать мужику про csv файлы, но он всё равно говорит, что поменять колонки — это заменить , на ;:

# swaps 1st and 2nd columns in csv file def swap(infile: str, outfile: str):     with open(infile, 'r') as f:         lines = f.readlines()     with open(outfile, 'w') as f:         for line in lines:             f.write(line.replace(',', ';')) 

Live coding

Ладно, вся эта синтетическая хрень довольна показательна, но вдруг ему просто не хватает контекста? Давайте-ка бросим мужика в реальный проект, пусть покажет, на что способен! Может теперь он будет кодить по ночам вместо меня, зарабатывать деньги и кормить семью, а я наконец-то пройду GTA V?

Далее будут просто куски кода, когда пилот что-то предлагал, а я посчитал это интересным.

Бесполезные действия

def test_limited_plan_duration(db, user, plan, now, days):     # ...     for _ in range(10):         # copilot:         subscription.end = subscription.start + days(10)         subscription.save(update_fields=['end'])         subscription.start = subscription.end

Достаточно странно: subscription — это подписка, и copilot сдвигает её на 10 дней вперёд, но вот сохраняется только поле end. Зачем тогда менять start — не понятно.

Угадывает копипасту

subscription.prolong() assert subscription.end == now + 2 * plan.charge_period  subscription.prolong() # copilot: assert subscription.end == now + 3 * plan.charge_period

Тут он неплохо прочувствовал, что prolong() ведёт к увеличению subsctiption.end. Успех!

Else

if self.end == self.max_end: raise ProlongationImpossible('Current subscription end is already the maximum end') # copilot: else: 

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

Rest framework

class LimitsView(GenericAPIView):     # copilot:     permission_classes = IsAuthenticated,     schema = AutoSchema()      def get(self, request, *args, **kwargs) -> Response:         return Response(             {                 'limits': {                     'subscriptions': request.user.subscriptions.count(),                     'subscriptions_limit': request.user.subscriptions_limit,                 }             },         )

Во-первых, по названию класса copilot сгенерировал мне рабочий endpoint. Впечатляет!..

С другой стороны, тут нету никакого класса для сериализации (serializer_class), и Response создаётся ручками. Вроде работает, но swagger, например, автоматически не сгенерировать.

Test class

Вот это впечатляет! Смотрите, только по первой строчке появляется вот что:

class UninitializedAPIBaseConsoleToolTest(BaseConsoleToolTest):     # copilot:     """     Base class for tests that need an uninitialized API     """      def setUp(self):         super(UninitializedAPIBaseConsoleToolTest, self).setUp()         self.b2_api = B2Api(self.account_id, self.master_key) 

Даже докстринг какой-то появился! Можно придраться к old-style вызову super(Class, self).setUp() вместо простого super().setUp() , но не критично.

А теперь идём в базовый класс и смотрим, что там:

class BaseConsoleToolTest(TestBase):     RE_API_VERSION = re.compile(r"\/v\d\/")     json_pattern = re.compile(r'[^{,^\[]*(?P<dict_json>{.*})|(?P<list_json>\[.*]).*', re.DOTALL)      def setUp(self):         self.account_info = StubAccountInfo()          self.b2_api = B2Api(             self.account_info, None, api_config=B2HttpApiConfig(_raw_api_class=RawSimulator)         )         self.raw_api = self.b2_api.session.raw_api         (self.account_id, self.master_key) = self.raw_api.create_account() 

То есть super().setUp() под капотом создаёт self.b2_api, а потом мы его пересоздаём, причём если посмотреть в исходники, то вторым аргементом должен быть cache, а мы туда передаём self.master_key.

А выглядело так классно!

Lazy callable

@dataclasses.dataclass class Defer:      callable_: Callable     # copilot:     args: Tuple[Any, ...] = ()     kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict)      def __call__(self):         # copilot:         return self.callable_(*self.args, **self.kwargs) 

Вот тут огненно! Кажется, питонисты часто пишут такой lazy-evaluation класс, и copilot отлично пишет весь бойлерплейт, и даже правильно использует default_factory!

Копипаста

@dataclass class ReplicationRule:     ...     @classmethod     def from_dict(cls, value_dict: dict) -> 'ReplicationRule':     # copilot:         return cls(             destination_bucket_id=value_dict['destinationBucketId'],             file_name_prefix=value_dict['fileNamePrefix'],             is_enabled=value_dict['isEnabled'],             priority=value_dict['priority'],             replication_rule_name=value_dict['replicationRuleName'],         ) 

Это годно! Мужик где-то нашёл маппинг external field <-> class field и использовал его, причём сам понял, какие атрибуты нужно задать.

Знает больше, чем я

Писал я себе код, и тут

profile = 'profile-for-test-'  # copilot: + random_hex(6)

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

Добавляю параметр в функцию

if default_server_side_encryption is not None: if not default_server_side_encryption.mode.can_be_set_as_bucket_default(): raise WrongEncryptionModeForBucketDefault(default_server_side_encryption.mode) kwargs['defaultServerSideEncryption'] = default_server_side_encryption.serialize_to_json_for_request() if is_file_lock_enabled is not None: kwargs['fileLockEnabled'] = is_file_lock_enabled if replication_configuration is not None: # copilot: kwargs['replicationConfiguration'] = replication_configuration.serialize_to_json_for_request()

Я добавил replicationСonfiguration параметр, и пилот заполнил его правильным значением, правильно обнаружив специальный метод serialize_to_json_for_request — видимо, сделал это по аналогии со строчкой

kwargs['defaultServerSideEncryption'] = default_server_side_encryption.serialize_to_json_for_request()

Прикольно!

Эй, пандас знаешь?

node.children = { option: cls.parse_table(table.loc[table.iloc[:, 0] == option])  # <-- copilot for option in options }

У меня pandas-альцгеймер, я постоянно забываю всякие методы и мне приходится перечитывать документацию. Но тут мужик по option: cls.parse_table дополнил table.loc[table.iloc[:, 0] == option], и это было именно то, что нужно! Как он узнал, что мне нужно отфильтровать по определённому значению в первом столбце?!

Отфильтруй по нескольким параметрам

def get_replication_rules(     account_info: AbstractAccountInfo,     bucket_name: Optional[str] = None,     bucket_path: str = '',     replication_rule_name: Optional[str] = None, ) -> Iterator[Tuple[BucketId, ReplicationRule]]:      buckets = account_info.list_buckets()      for bucket in buckets:         # copilot:         if bucket_name and bucket.name != bucket_name:             continue         if bucket_path and bucket.path != bucket_path:             continue         if replication_rule_name and not has_rule_with_name(bucket, replication_rule_name):             continue         yield bucket.id, bucket.replication.as_replication_source.replication_rules[0]

Это хороший пример.

Тут нужно было получить список объектов и отфильтровать их по bucket_name, bucket_path и replication_rule_name. Copilot это и сделал, но в конце подложил свинью: у каждого объекта может быть несколько replication_rules, но copilot возвращает только первый. Просто он заботливый и учит вас code review и отладке.

Посчитай статистику

def count_files_and_bytes(bucket: Bucket) -> Tuple[     ReplicationCounter[ExtendedReplicationStatus],     ReplicationCounter[ExtendedReplicationStatus], ]:     """     Calculate (counter_files, counter_bytes), where each counter     maps ExtendedReplicationStatuses to number of occurrences.     """     counter_files = ReplicationCounter()     counter_bytes = ReplicationCounter()     # copilot:     for file_version in bucket.list_file_versions():         counter_files[file_version.status] += 1         counter_bytes[file_version.status] += file_version.size          if file_version.status == ReplicationStatus.COMPLETED:             if file_version.has_hidden_marker:                 counter_files[ExtendedReplicationStatus.HAS_HIDDEN_MARKER] += 1             if file_version.has_sse_c_enabled:                 counter_files[ExtendedReplicationStatus.HAS_SSE_C_ENABLED] += 1             if file_version.has_large_metadata:                 counter_files[ExtendedReplicationStatus.HAS_LARGE_METADATA] += 1      return counter_files, counter_bytes

Логика подсчёта не тривиальная: мы не только заполняем значения для основного статуса типа counter_files[file_version.status] += 1, но и добавляем подсчёт «подстатусов»: если стутус — COMPLETED, то нужно найти его «подстатус» — HIDDEN/SSE_C/LARGE_METADATA — и его тоже добавить в счётчик: counter_files[ExtendedReplicationStatus.HAS_HIDDEN_MARKER] += 1.

И это тот случай, когда Copilot сделал всё правильно! Как он догадался — я не знаю, может, посмотрел предыдущий код, но факт: сделал именно то, что нужно.

6 казарм из 6
6 казарм из 6

Время говнокодить!

Иногда пилот просто угорает. Предлагал такое не раз:

@dataclass class BattleConfig:     copilot:     """     BattleConfig class     """     battle_id: int     battle_name: str     battle_type: str     battle_mode: str     battle_map: str     battle_result: str     battle_date: str     battle_time: str     battle_duration: str     battle_server: str     battle_server_url: str     battle_server_url_short: str     battle_server_url_short_2: str     battle_server_url_short_3: str     battle_server_url_short_4: str     battle_server_url_short_5: str     battle_server_url_short_6: str     battle_server_url_short_7: str     battle_server_url_short_8: str     battle_server_url_short_9: str     battle_server_url_short_10: str     battle_server_url_short_11: str     battle_server_url_short_12: str     battle_server_url_short_13: str     battle_server_url_short_14: str     battle_server_url_short_15: str     battle_server_url_short_16: str     battle_server_url_short_17: str     battle_server_url_short_18: str     battle_server_url_short_19: str     battle_server_url_short_20: str     battle_server_url_short_21: str     battle_server_url_short_22: str     battle_server_url_short_23: str     battle_server_url_short_24: str     battle_server_url_short_25: str     battle_server_url_short_26: str     battle_server_url_short_27: str     battle_server_url_short_28: str     battle_server_url_short_29: str     battle_server_url_short_30: str     battle_server_url_short_31: str     battle_server_url_short

Прям как будто в Индии побывал!

Знает про версии

Внезапно, при редактировании requirements.txt, copilot подставил последнюю версию:

pydantic~=1.9.0 simplejson # copilot: ~=3.17.0

Не то чтобы очень полезно, но интересно.

Заткни линтер

Copilot и это умеет:

p = lambda fname: os.path.join(dir_path, fname) # noqa: copilot: E731

Сам понял, что линтер жалуется на E731, и сам его заткнул, когда увидел noqa.

Придумай отмазку

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

Тут написал какой-то тест и решил оправдать свою лень, и copilot отлично справился — я мог выбирать сразу несколько вариантов, почему я не хочу кодить:

def profile_switch_test(     b2_tool,     bucket_name,     folder_in_bucket,     destination_encryption=None,     expected_encryption=SSE_NONE,     source_encryption=None, ):     # this test could be unit, [copilot: but it's a bit of a pain to write]          # i did not write [copilot: this test, it was copied from the b2_cli_test.py]          # sorry for this, [copilot: but I don't want to make a new test for each]

Кстати, потом я написал такое:

    # this test could be unit, but it adds a lot of complexity because of     # requirement to pass mocked B2Api to ConsoleTool; it's much easier to     # just have an [copilot: integration test] 

Хоть дополнение двумя словами никак мне не помогает, но тот факт, что пилот понял контекст — что кожаный мешок не смог в юнит-тест и решил написать интеграционный — впечатляет!

К чему всё это

С резиновой женщиной всё-таки лучше

Ниже — моё мнение после нескольких месяцев с резиновым мужиком.

Почему ДА

  • Когда нужно сделать автодополнение одной строчки, то работает вполне прилично

  • На копипастах очень неплохо подставляет

  • Иногда, достаточно рандомно, пишет годноту и таки экономит немного времени

  • Подскажет подходящий метод в нужный момент, даже если вы про него забыли / не знали

Почему НЕТ

  • Перед каждой функцией длиннющий комментарий, который говорил пилоту, что нужно закодить. Куда этот комментарий девать? Удалять? Я не для удаления свой цветок выращивал, жалко же! Оставлять тоже тупо, он же просто описывает, что делает функция. Если не писать комментарий, а только название функции, то пилоту этого контекста может быть мало

  • Названия функций иногда бывают прямо из ада:

# read file exluding blank lines and lines starting with "#" def read_file_exluding_blank_lines_and_lines_starting_with_hash(file: str) -> list: ...
  • Всё нужно проверять. Мужик может написать годноту, а может хрень, и никак не сигнализирует о своей уверенности.

  • Для коротких функций слишком накладно, проще самому написать

  • Совершенно, абсолютно не знает про stdlib и best practices

Так надувать его или нет?

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

  • Если вы джун: учиться на copilot — себе вредить; иногда понять, что он делает, будет проблематично;

  • Если вы миддл: доверять copilot — себе вредить, а постоянно проверять за мужиком вы замучаетесь;

  • Если вы сеньор: вы всегда напишете быстрее и лучше.

Раньше я был за мужика. Типа теперь я стану кодить быстрее. Потом подумал и стал против: я боялся, что теперь нас, кодеров, вышвырнут. Машину обучают на моём коде, чтобы она меня заменила. Нет, спасибо! А теперь мне пофиг, и вот почему.

Много лет назад я пошёл на кафедру, где выбрал изучение компьютерного зрения («computer vision») — именно оттуда и родился FindFace, кстати. Я думал, что CV это пипец как круто: консервные банки что-то видят и понимают! Для меня это была технология, неотличимая от магии. Но когда лезешь «под капот», то иллюзия рушится: это были просто дескрипторы особых точек на фото и немного машинного обучения, чтобы находить паттерны в этих дескрипторах и говорить, что на фото на 60% собака и на 40% строительный кран.

Я думал, что компьютеры умеют видеть. Они и видят, но не понимают ни черта. Они и пишут — GPT-3 и вот это всё — но не понимают ни черта. Теперь они кодят — но всё так же ни черта не понимают.

Они словно Нео из «Матрицы» — что-то делают, но сами не знают, почему и зачем. И пока программы не научатся понимать причину, а не следствие, пока они не научатся отказываться делать бессмысленные вещи, пока не начнут предлагать альтернативные решения и думать — моя профессия в безопасности.


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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Если узор созвездий подобен луку тугому, а череда жизней душных — тетиве неразрывной, то…?
56.25% Дух твой, наверно, подобен стреле, калёной в пламени тысячи солнц! 45
21.25% Алексей, ты что, опять выпил? 17
10% А я знаю, что ты не Алексей! 8
12.5% Я и есть kesn 10
Проголосовали 80 пользователей. Воздержался 21 пользователь.

ссылка на оригинал статьи https://habr.com/ru/post/666538/

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

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