Мой опыт с резиновым мужиком. 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/

Rust с 0 до 80% для JavaScript разработчиков

Эта статья — перевод оригинальной статьи Michael Salim «Rust from 0 to 80% for JavaScript Developers«

Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

Это список тем, которые помогут вам быстро понять Rust, если вы разработчик JavaScript. Есть много учебников, которые начинаются с нуля. Но если вы уже знаете что-то еще, почему бы не сравнить их?

Это различия, на которые я хотел бы сослаться перед тем, как приступить к Rust, вкратце.

Предупреждение!

Я очень далек от того, чтобы хорошо разбираться в Rust. Документация по Rust обширна, поэтому, если вам нужны подробности, поищите их в Google. Эта книга также является хорошей отправной точкой для изучения Rust. Моя цель — перечислить важные вещи, чтобы вы (и я в будущем) могли пропустить общие концепции программирования и сосредоточиться на различиях, основанных на уже известных вам знаниях.

Типы

Rust — типизированный язык, поэтому он ближе к TypeScript. У вас будет гораздо лучший опыт, если вы уже знаете TS.

По большей части синтаксис похож (variable_name: Type). Ура!

snake_case

Ага, не обойти.

Что это за символ?

1. Вопросительный знак (?)

Вы можете встретить (?) после вызова функции, например: my_function()?;.

Нет, это не optional chaning. Это магия обработки ошибок для функций, которые могут дать сбой (функции с ошибкой). Вы часто будете видеть это при работе с асинхронными функциями. Подробнее об этом позже.

Документация

2. Восклицательный знак на функциях (!)

Пример: println!("{:?}", my_variable);

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

3. Символ (&)

Пример: &my_var

Это способ получить ссылку. Вы встречали это, если использовали языки низкого уровня, такие как C. Подробнее позже.

Синтаксис

  1. Точка с запятой (;) в конце каждой строки обязательна.

  2. Исключение: точка с запятой (;) не обязательна в последней строке функции. В данном случае это сокращение для возврата этой строки.

  3. Синтаксис функций немного отличается. Не ахти какое дело.

    fn foo(num: i32) -> i32 {   3 // See no.2   // or   // return 3; }
  1. Синтаксис декоратора также отличается. Его также называют атрибутами.

Что это за ключевые слова?

struct

Это объект JSON. (Хорошо, может быть, сложнее, но см. документы для этого).

type Person = {   firstName: string;   lastName: string; };
struct Person {     first_name: String,     last_name: String, }

trait

Интерфейс

impl

Реализация trait. Ближе всего к этому у нас есть классы. Это связь между trait и типом. я не использовал его

enum

В некотором смысле очень похоже на enum в Typescript. Но вы можете хранить в нем данные. Это довольно изящная и довольно важная концепция для понимания асинхронности.

Console.log

Не так просто, к сожалению. Больше похоже на printf из других языков

println!("{:?}", my_variable);

Library/Dependencies

Используйте Cargo.toml вместо package.json. Вам надо будет добавить их вручную (вместо использования такой команды, как yarn add)

Документация

Пример:

[dependencies] chrono = "0.4" egg-mode = "0.16.0"

Импорт

В Rust есть модули. Они сильно отличается от JS, но в основном:

Это что-то вроде пространств имен. Вот разбивка по импорту зависимости

use rocket::serde::{json::Json, Deserialize, Serialize};

use — используется вместоimport

rocket — это название пакета

:: — доступ к модулю

serde — это название модуля

{json::Json, Deserialize, Serialize} — штуки, которые нужно импортировать

Еще немного синтаксиса:

use chrono::prelude::*;

use rusqlite::Result;

Импортирование из локальных файлов

Лучшее объяснение: https://doc.rust-lang.org/rust-by-example/mod/split.html

Используйте mod для пути/файла, который вы хотите импортировать, чтобы компилятор включил модуль.

Затем используйте use для импорта. Примечание: mod также автоматически импортирует его. В этом случае вам понадобится префикс crate.

Пример:

use crate::your_file_or_module;

Примечание: mod.rs — это специальное имя файла, которое действует как index.js.

Const vs let

В JavaScript вы бы использовали const большую часть времени, потому что она неизменяемая.

В Rust лучше использовать let. Он не переопределяемый по умолчанию. Если вы хотите, чтобы он был изменяемым, используйте ключевое слово mut. const зарезервированы для фактических констант (поэтому вы не можете вычислить значение из другой переменной)

let immutable_variable = ...; let mut mutable_variable = ...; const MY_CONSTANT = "CONSTANT";

Документация библиотек

Если в репозитории Github нет ссылки на страницу документации, вы, вероятно, можете перейти к ней следующим образом:

Асинхронность

Безусловно, две самые запутанные темы — это futures и ownership. Я бы порекомендовал прочитать более полную документацию для них. Сначала поговорим о Future.

Future похоже на Promise. В отличие от JS, в Rust есть тип результата promise/future который называется Result. Он также принимает тип ошибки в дженериках (хотелось бы, чтобы JS имел это). Вы также можете использовать Result без future.

Вызов Future

Стандартной библиотеки обычно не хватает, поэтому вам нужно будет импортировать что-то еще (например, bluebird для JS). Вам нужен исполнитель, чтобы управлять Future. Я рекомендую использовать tokio и читать их документацию.

.await чтобы подождать функцию

async_function().await; Интересный синтаксис, да? На самом деле он довольно приятный, так как вам не нужно оборачивать его скобками, как в JS.

Управление Result

Это еще один важный момент. Rust безопасен, поэтому вам нужно будет обработать все ошибки. Да, обязательно все случаи ошибок в отличие от JS!

В Result enum есть Ok и Err. Если Future успешно, оно возвращает Ok, иначе Err.

Наиболее полный способ обработки обоих случаев:

let f = File::open("hello.txt");  let mut f = match f {     Ok(file) => file,     Err(e) => return Err(e), };

В приведенном выше примере используется синтаксис match, который тоже хорош.

Но он довольно многословный, поэтому есть 2 распространенных способа сократить его:

  1. Используя .unwrap()

Пример: let my_value = async_function().await.unwrap();

Он получает значение успеха или кидает ошибку, если Err

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

  1. Используя ? синтаксис

Это передает ошибку вверх. Таким образом, ваша функция также должна иметь возможность возвращать ошибку (либо Result, либо Option).

Глянь этот пример и его эквивалент

Ownership и References

Слышали о borrow checker? Здесь мне особо нечего сказать. Это самая сложная вещь в этом списке, поскольку она уникальна для Rust. И если вы никогда раньше не работали со ссылками, эта тема может быть немного сложной.

К счастью, книга о Rust снова спасает положение.

Заключение

Этот список на самом деле короче, чем я ожидал. Я надеюсь, что это вам поможет.


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

Как написать свой прокси с кроликом и рейт-лимитами и не изменить змее с сусликом

Пару лет назад мы в Just Work делали несколько похожих проектов, которые должны были обрабатывать данные, получаемые из одного внешнего HTTP API. Это API, несмотря на согласованные повышенные лимиты, изредка банило наши ключи доступа за малейшее превышение. Из-за этого ответственность за соблюдение лимитов лежала на клиентах. В дальнейшем, проектов, использующих это API, должно было становиться все больше, и заказчика не устраивала перспектива разбираться с каждой реализацией по отдельности.

В итоге было решено сделать собственный прокси-сервер, который реализовывал бы контроль скорости и предоставлял бы асинхронный доступ к API.

Производительность

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

Больше всего опасений вызывал разбор HTTP-протокола и накладные расходы на обеспечение параллелизма. Исходя из всего выше описанного, были выбраны три асинхронных фреймворка (tornado, gevent и asyncio) и пара HTTP-клиентов (requests + libcurl). В итоге в категории «фреймворки» всех уделал gevent, который «шаманит» со стеком и тредами где-то во внутренностях интерпретатора питона, и libcurl, который помимо того, что написан на C, имеет еще один «козырь» в рукаве под названием «multi-curl». Это API библиотеки, которое позволяет libcurl выполнять несколько HTTP-запросов параллельно.

Реализация

После того, как удалось заставить работать multi-curl и gevent вместе, способ реализации стал очевиден: берем Celery в режиме gevent и каждую задачу отправляем в конвейер multi-curl, ждем пока запрос завершится и ставим еще одну Celery-задачу с ответом API в «очередь» ответов, указанную клиентом.

Забегая вперед, стоит сказать, что все проекты на тот момент использовали Python, и предоставляемый Celery протокол общения для прокси был скорее преимуществом: ведь Celery позволяет описывать цепочки задач, выполняемые друг за другом. Но и к клиентам «из других миров» мы были готовы, т.к. есть возможность трансформировать на лету входящее сообщение так, чтобы Celery считала его нормальной задачей. Похожим способом, например, пользуется библиотека celery-message-consumer. Но давайте вернемся к контролю за соблюдением лимитов.

Rate-Limiting

Для того, чтобы ограничить скорость отправки запросов к API, мы просто останавливаем получение сообщений из очереди RabbitMQ, соответствующей нужному эндпоинту. С помощью таймеров Celery мы возобновляем обработку задач через время, соответствующее слоту отслеживания лимитов. Задача, попавшая под rate-limit, возвращается в начало очереди с помощью AMQP-команды reject(requeue=True).

Поскольку для нас важно не выходить за рамки лимитов, мы начинаем снижать скорость обработки задач еще до полного исчерпания слота. По сути, вероятность получить «стоп» линейно возрастает с нуля на 50% загрузки слота до 100% при полной загрузке. С таким подходом, при наличии большого количества воркеров Celery, к исчерпанию слота мы приходим с практически нулевой скоростью, что дает ровную линию на графике обработки задач.

HTTPS и логи

Поскольку связка gevent + multi-curl всё еще не вылезает за пределы одного ядра CPU, нам захотелось перенести с этого ядра накладные расходы, связанные с шифрованием HTTPS-трафика. Для этого мы использовали nginx в режиме реверс-прокси, где апстримом выступало API, к которому мы отправляли запросы. Это также позволило нам получить подробные логи, которые нам сильно пригодились в дальнейшем.

В какой-то момент в продакшне все ответы API стали возвращать 404 ошибку. Когда стало очевидно, что это не связано с техническими работами или со сбоями «апстрима», мы стали изучать логи celery и nginx. Тогда-то и выяснилось, что 404 ошибку нам возвращает не наше API а какой-то чужой веб-сайт с совершенно левым HTTP-заголовком Server:. Если честно, я с таким ранее не сталкивался, поэтому просто сидел и смотрел в окно следующие 20 минут. Оказалось, что виновата «облачная погода»: AWS, на котором был развернут API, погасил его контейнер и стартанул вместо него чужой веб-сайт с тем же IP-адресом. nginx, который тоже работал внутри AWS, ресолвил DNS «апстрима» только при старте, а потом просто возвращал ответы любого сервиса, который развернут на этом IP-адресе.

Решение было довольно простым: динамический proxy_pass и короткий resolver_timeout. В результате, при миграции API на другие адреса, прокси это замечает за 5 секунд.

Использование nginx избавило нас еще и от забот по обработке таймаутов и сетевых ошибок, поддержке keep-alive и переиспользованию SSL-соединений. В общем, идея оказалась удачной.

Маршрутизация в RabbitMQ

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

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

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

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

Получилась следующая реализация:

  1. Каждому клиенту при заведении создаются свои request_exchange и response_exchange.

  2. После заведения нового клиента прокси автоматически создает и начинает обрабатывать очереди RabbitMQ для каждого активного эндпоинта. Очереди биндятся к request_exchange.

  3. Клиент отправляет сообщение с запросом к API в request_exchange с routing key, соответствующим запрашиваемому url. Это обеспечивает попадание сообщения в нужную очередь, обрабатываемую воркерами прокси.

  4. После успешного выполнения запроса прокси ставит в response_exchange сообщение с ответом API. Routing key сообщения задается в теле исходного сообщения-запроса за счет использования цепочек задач.

  5. Клиент сам создает и биндит очереди обработки ответов к response_exchange.

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

Такая схема позволила обеспечить изоляцию, но одновременно сломала пару фичей Celery, таких как heartbeat, mingle и gossip. Если кто-то знает, как их вообще используют (хотя бы теоретически), напишите в комментариях, хочется знать, чего конкретно мы лишились.

В итоге

В итоге на нагрузочном тестировании удалось «разогнать» прокси до обработки 6000 запросов в секунду на одном ядре (к локалхосту, правда), при том что согласованный лимит API составлял всего 700 RPS. В таком виде система живет уже почти 3 года.

Хотелось ли переписать данный сервис на какой-либо другой язык вроде golang? Точно нет, пока затраты на хостинг из-за увеличившейся на 2 порядка нагрузки не перевесят затраты на написание всего задействованного функционала, предоставляемого Celery.


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

История очень терпеливого пациента, который чуть не умер прямо у нас в кабинете

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

Понятно, что хорошего — ничего. Но хотелось бы конкретики.

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

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

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


Снимок того пациента и причинный зуб

Детективная история

Пациент ушёл, а терапевт продолжил про него думать. Доктора насторожило, что за рамками зуба на слизистой не было никаких признаков воспаления: ни красноты, ни отёков — ничего. И это была очень странная картина. Ему пришла в голову мысль, что, может, стоило сделать надрез на десне, чтобы удостовериться, что всё действительно в порядке, и он позвонил мне. Я ответила, что практика такая есть, но, скорее всего, ничего страшного в том, что он этого не сделал, нет.

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

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

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

Я решила сама посмотреть, что это за загадочный пациент.

На первый взгляд — ничего особенного, но всё-таки появились смутные догадки. Дело в том, что я пришла в стоматологию из челюстно-лицевой хирургии и накопила опыт работы с флегмонами, т. е. внутренним гнойным воспалением в подкожной межмышечной жировой клетчатке. Главная их опасность в том, что они «разливаются» по тканям, совершенно ничем не ограничиваясь. Один из явных признаков, которые позволяют определить, что перед вами именно флегмона, даже если нет ни горячности, ни покраснения, — благодаря сильному внутреннему отёку кожа не собирается в складку. Ни в какую, даже самую крошечную. Именно это я и увидела.


Так обычно выглядит человек, у которого флегмона. В нашем случае такого явного отёка не было.

Ну или вот так флегмона обычно выглядит на видео.

И что же это было?

Я тоже сделала диагностический надрез, но не вестибулярно — там, где обычно проводится вскрытие абсцессов или периодонтитов, а со стороны языка. И получила густое, как сметана, гнойное отделяемое. Много. Предположение подтвердилось!

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

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

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


Ткани дна полости рта устроены так

И тут резко стали понятны две вещи

Во-первых, ситуация критическая.

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


Клетчаточные пространства шеи. Чтобы всё дренировать, придётся резать. Много-много и некрасиво резать.

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


В зависимости от того, где находится флегмона, разрезать человека можно по-разному

При этом делать что-либо вне стационара опасно: через шею проходят все жизненно важные сосуды и нервные пучки.

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

Как его в итоге «отремонтировали»

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

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

История на этом не закончилась, потому что приняли его только во второй больнице по счёту и прооперировали в первом часу ночи.

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

Крайне неприятное видео реальной операции

Видео и правда неприятное, еще раз подумайте

Зато музыка классическая

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

Пять дней наш товарищ провёл в стационаре, три из которых — с дренирующими трубками в шее.


Выглядел он при этом примерно как на этой картинке из учебника


Разрезы чаще всего получаются совсем некрасивыми: гнойные раны очень плохо заживают. Но здесь уже совершенно не до красоты. Вопрос — выживет человек или нет.

Откуда такие сложности с диагностикой

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

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


Вот так выглядит на снимке явное воспаление

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

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

Фактически человека спасла наша с терапевтом врачебная интуиция. Изначальная картина была совершенно рядовая, хирурга в подобных случаях обычно не зовут. Я оказалась там почти случайно.

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

Как развивается «зубная» флегмона

Типичная последовательность выглядит примерно так:

  1. Кариес.
  2. Пульпит — потерпели, поели анальгетиков, ткань умерла, трупам не больно — всё здорово.
  3. Периодонтит — опять болит, потому что теперь инфекция вышла за пределы верхушки корня, но сдерживается иммунной системой. Вся эта инфицированная дрянь постоянно течёт из захваченного зуба в периапикальное пространство, но иммунная система строит оборонительные валы и всячески сопротивляется.
  4. Стресс/болезнь/переохлаждение — и вот патоген мутировал. Защита прорвана, процесс полетел дальше. Начиная с этого момента всё может развиваться очень быстро.
  5. Периостит — воспаление надкостницы. Валикообразный отёк, обычно двусторонний, вокруг причинного зуба.
  6. Остеомиелит — гнойное расплавление костной ткани.
  7. Флегмона — пришло время разрезать половину головы на лоскуты и навтыкать туда трубочек.
  8. Инфекция оказалась агрессивной? Что ж, привет, тромбозы кавернозного синуса, поражение центральной нервной системы, а то и спуск всей этой гнойной массы вниз по клетчаточным пространствам шеи и медиастенит — острое гнойное поражение органов и тканей грудной клетки. Вот тут уже почти без вариантов.

В острую фазу обычно счет идёт не на дни, а на часы. Процесс протекает очень-очень быстро.

Кстати, о причинах возникновения флегмоны вообще. Зубы бывают виноваты во всём этом безобразии чаще, чем что-либо другое. Вот для примера тридцатилетняя статистика Архангельской областной клинической больницы:

И, кстати, ещё про зубы. Чаще всего причиной флегмоны становятся нижние моляры:

А как не дойти до жизни такой?

Я ни разу не встречала подобных проблем у человека, который регулярно посещает стоматолога для профилактики и следит за состоянием своей полости рта.

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

Подытожим

  1. Если долго и мужественно терпеть зубную боль, то можно попасть в челюстно-лицевую хирургию. А если не повезёт, то и вовсе на тот свет.
  2. Гнойные воспаления, даже очень серьёзные, иногда могут слишком хорошо замаскироваться.
  3. КТ и рентген далеко не всемогущи: гнойное воспаление глубоко в тканях с их помощью не разглядеть.
  4. Иногда врачебная интуиция — это единственное, что может спасти пациента.
  5. И ещё раз — терпеть зубную боль нельзя ни в коем случае, даже если вы сильный мужчина, которому всё нипочём.

P.S. Если вы дойдёте до наших клиник, то говорите, что вы с Хабра, будет скидка 5%.


ссылка на оригинал статьи https://habr.com/ru/company/belayaraduga/blog/666140/

Ускоряем dplyr: бекенды dtplyr, multidplyr и dbplyr (видео урок + конспект)

dplyr один из наиболее популярных пакетов для языка R, основным преимуществом которого является удобочитаемый и понятный синтаксис. Из недостатков данного пакета можно отметить, что при работе с данными большого объёма он значительно уступает в скорости вычислений например data.table.

В этом видео уроке мы разберёмся с тем, как можно ускорить вычисления на dplyr, за счёт бекендов dtplyr и multidplyr, а так же узнаем о том, как и зачем можно использовать бекенд dbplyr, предназначенный для работы с базами данных.

Содержание

Если вы интересуетесь анализом данных возможно вам будут полезны мои telegram и youtube каналы. Большая часть контента которых посвящены языку R.

  1. Видео

    1. Тайм коды

  2. Презентация

  3. Краткий конспект

    1. dtplyr

    2. dbplyr

    3. multidplyr

    4. Какой бекенд выбрать

  4. Заключение

Видео

Тайм коды:

  • 00:00 Вступление

  • 00:59 Какие бекенды мы рассмотрим

  • 01:48 Цель dtplyr

  • 02:30 Синтаксис dtplyr

  • 03:33 Пример работы с dtplyr

  • 05:38 Как осуществляется перевод глаголов dplyr в синтаксис data.table

  • 07:51 Функция show_query()

  • 11:27 Почему dtplyr медленнее чем data,table

  • 13:24 Цель dbplyr

  • 13:40 Синтаксис dbplyr

  • 14:37 Пример работы с dbplyr

  • 16:54 Перевод dplyr глаголов в SQL запросы

  • 17:53 Как реализованы подзапросы в dbplyr

  • 18:25 Перевод неизвестных R функций и инфиксных операторов в SQL запрос

  • 19:48 Проброс SQL команд в запросы

  • 20:15 Как происходит перевод функций внутри dplyr глаголов в SQL запросы, функция translate_sql()

  • 21:26 Введение в multidplyr

  • 22:17 Варианты применения multidplyr

  • 22:50 Пример работы с multidplyr

  • 28:24 Какой пакет использовать dtplyr или multidplyr

  • 29:27 Заключение

Презентация

Краткий конспект

dtplyr

Цель dtplyr — позволить вам писать код dplyr, который под капотом транслируется в эквивалентный, но зачастую более быстрый, код data.table.

Синтаксис dtplyr:

  • lazy_dt() — создаёт объект dtplyr для ленивых вычислений (первое действие);

  • dplyr — далее используем созданный с помощью lazy_dt() объект с глаголами dplyr, для реализации необходимых вычислений. При этом фактически никакие вычисления не производятся, а лишь оцениваются;

  • show_query() — показывает в какое data.table выражение будут преобразованы ваши вычисления;

  • as.data.table() / as.data.frame() / as_tibble() — выполняет вычисления, и приводит результат к тому типу, который соответствует выбранной вами функции.

Примеры кода dtplyr

Пример вычисленний с dtplyr
# dtplyr library(data.table) library(dtplyr) library(dplyr, warn.conflicts = FALSE)  # dtplyr использует ленивые вычисления mtcars2 <- lazy_dt(mtcars)  # проверка результата mtcars2 %>%    filter(wt < 5) %>%    mutate(l100k = 235.21 / mpg) %>% # liters / 100 km   group_by(cyl) %>%    summarise(l100k = mean(l100k))  # но для выполнения вычислений следует использовать #  as.data.table(), as.data.frame() или as_tibble() mtcars2 %>%    filter(wt < 5) %>%    mutate(l100k = 235.21 / mpg) %>% # liters / 100 km   group_by(cyl) %>%    summarise(l100k = mean(l100k)) %>%    as_tibble()  # более подробно о том, как осуществляется перевод кода df <- data.frame(a = 1:5, b = 1:5, c = 1:5, d = 1:5) dt <- lazy_dt(df)  # посмотрим предварительную оценку перевода dt # если мы хотим посмотреть в какое выражение data.table # будет преобразован код dplyr используем show_query() dt %>% show_query()  # простые глаголы  ## filter() / arrange() - i dt %>% arrange(a, b, c) %>% show_query() dt %>% filter(b == c) %>% show_query() dt %>% filter(b == c, c == d) %>% show_query()  ## select(), summarise(),transmute() - j dt %>% select(a:b) %>% show_query() dt %>% summarise(a = mean(a)) %>% show_query() dt %>% transmute(a2 = a * 2) %>% show_query()  ## mutate - j + := dt %>% mutate(a2 = a * 2, b2 = b * 2) %>% show_query()  # Другие глаголы dplyr ## rename - setnames dt %>% rename(x = a, y = b) %>% show_query() ## distinct - unique dt %>% distinct() %>% show_query() dt %>% distinct(a, b) %>% show_query() dt %>% distinct(a, b, .keep_all = TRUE) %>% show_query()  # Операции объединения dt2 <- lazy_dt(data.frame(a = 1))  ## [.data.table dt %>% inner_join(dt2, by = "a") %>% show_query() dt %>% right_join(dt2, by = "a") %>% show_query() dt %>% left_join(dt2, by = "a") %>% show_query() dt %>% anti_join(dt2, by = "a") %>% show_query()  ## merge() dt %>% full_join(dt2, by = "a") %>% show_query()  # Группировка keyby dt %>% group_by(a) %>% summarise(b = mean(b)) %>% show_query()  # Комбинации вызовов dt %>%    filter(a == 1) %>%    select(-a) %>%    show_query()  dt %>%    group_by(a) %>%    filter(b < mean(b)) %>%    summarise(c = max(c)) %>%    show_query()

dbplyr

Цель dbplyr — позволить вам манипулировать данными, хранящимися в удалённых базах данных, так же как если бы они были дата фреймами в среде R. Данный бекенд переводит глаголы dplyr в операторы SQL.

Синтаксис dbplyr

  • DBI — для инициализации подключения к базе данных;

  • tbl() — для подключения к конкретной таблице;

  • dplyr — реализуем все вычисления на основе глаголов dplyr , на данном этапе идёт только оценка вычислений;

  • show_query() — позволяет посмотреть в какой SQL запрос будет конвертирован написанный на предыдущем шаге код;

  • collect() — выполняет вычисления, и возвращает результат.

Пример кода dbplyr

Пример вычислений с dbplyr
library(dplyr) library(dbplyr) library(dplyr, warn.conflicts = FALSE)  con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") copy_to(con, mtcars)  # обращаемся к таблице mtcars2 <- tbl(con, "mtcars") # пока что это всего лишь оценка вычисления mtcars2  # dbplyr в действии ## генерация SQL запроса summary <- mtcars2 %>%    group_by(cyl) %>%    summarise(mpg = mean(mpg, na.rm = TRUE)) %>%    arrange(desc(mpg))  ## просмотр запроса summary %>% show_query()  ## выполнение запроса, и извлечение результата summary %>% collect()   # автоматическая генерация подзапросов ------------------------------------ mf <- memdb_frame(x = 1, y = 2)  mf %>%    mutate(     a = y * x,      b = a ^ 2,   ) %>%    show_query()   # неизвестные dplyr функции ----------------------------------------------- # любая неизветсная dplyr функция в запрос будет прокинута как есть mf %>%    mutate(z = foofify(x, y)) %>%    show_query()  mf %>%    mutate(z = FOOFIFY(x, y)) %>%    show_query()  # так же dbplyr умеет переводить инфиксные функции mf %>%    filter(x %LIKE% "%foo%") %>%    show_query()   # использовать самописный SQL --------------------------------------------- ## для вставки SQL кода используйте sql() mf %>%    transmute(factorial = sql("CAST(x AS FLOAT)")) %>%    show_query()  mf %>%    filter(x == sql("ANY VALUES(1, 2, 3)")) %>%    show_query() 

multidplyr

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

Варианты разбиения данных

multidplyr позволяет вам разбить данные двумя способами:

  • Разбиение уже существующей в памяти таблицы с помощью partition().

  • Разбиение таблицы на части в момент загрузки.

Синтаксис multidplyr

  • new_cluster() — создаёт кластер процессов;

  • partition() — разбивает существующий в памяти набор данных на части, что бы каждая отдельная часть обрабатывалась на своём узле кластера;

  • cluster_assign_partition() — разделяет вектор значений на части таким образом, что бы каждому узлу кластера досталась примерно одинаковое количество элементов;

  • cluster_send() — запускает выполнение вычислений на разны узлах кластера;

  • party_df() — создаёт секционированный дата фрейм;

  • collect() — возвращает секционированный дата фрейм к обычному режиму работы.

Примеры кода multidplyr

Пример вычислений с multidplyr
library(multidplyr)  # создаЄм кластер cluster <- new_cluster(4) cluster  # разбиение фрейма на кластера -------------------------------------------- library(nycflights13)  flights1 <- flights %>% group_by(dest) %>% partition(cluster) flights1  # выполн¤ем вычислени¤ в многопоточном режиме flight_dest %>%    summarise(delay = mean(dep_delay, na.rm = TRUE), n = n()) %>%    collect()  # чтение файлов разными кластерами ---------------------------------------- # создаЄм временную папку path <- tempfile() dir.create(path) # разбиваем файл по мес¤цам,  # сохран¤¤ данные каждого мес¤ца в отдельный файл flights %>%    group_by(month) %>%    group_walk(~ vroom::vroom_write(.x, sprintf("%s/month-%02i.csv", path, .y$month)))  # находим все файлы в директории,  # и делим их так, чтобы каждому воркеру досталось (примерно) одинаковое количество файлов files <- dir(path, full.names = TRUE) cluster_assign_partition(cluster, files = files)  # считываем файлы на каждом воркере  # и используем party_df() дл¤ создани¤ секционированного фрейма данных cluster_send(cluster, flights2 <- vroom::vroom(files)) flights2 <- party_df(cluster, "flights2") flights2   # глаголы dplyr ----------------------------------------------------------- flights1 %>%    summarise(dep_delay = mean(dep_delay, na.rm = TRUE)) %>%    collect()

Какой бекенд выбрать

  • На данных среднего объёма предпочтительнее использовать dtplyr, накладные расходы на конвертацию кода минимальны, при высокой скорости вычислений.

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

  • К dbplyr стоит прибегать в случае, когда данные с которыми вы работаете хранятся в базах данных. Данный бекенд направлен не столько на повышение производительности вычислений, сколько на то, что бы ваш R код не перемешивался с SQL запросами.

Заключение

В ходе этого урока, мы разобрались с тем, как ускорить вычисления реализованные с помощью dplyr глаголов на данных большого объёма, а так же как работать с данными хранящимися в базах данных, не засоряя при этом код громоздкими SQL запросами.

Надеюсь материал предоставленный в этом видео уроке был вам полезен!


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