Deep Engineering

Подготовка к собеседованию

Один механизм — один урок. В линии Python сначала основа — от классов и коллекций до тестов, потоков и окружений, — дальше в порядке частоты вопроса: декораторы, дескрипторы, итераторы и генераторы. В линии Go тоже сначала основа — от структур и указателей до модулей и паттернов конкурентности. Каждый урок разбирает механизм по документации и исходникам — с замерами, а не по памяти.

Разделы
3
Статьи
71
ИСТОЧНИКОВ
340
Сложность
71 урок · подходит

Разделы

Статьи

СреднийОпубликованоКанал в Go: значение мимо буфера, select без приоритетов и цена, которую платят парковкойСобеседование по каналам идёт лестницей: что лежит в переменной — куда попадает значение — что будет с закрытым и nil-каналом — как выбирает select — во что обходится буфер — где утекают горутины. Урок проходит её целиком: почему при ждущем получателе значение идёт мимо буфера, почему у ветки отмены в select нет приоритета и почему канал без буфера дороже втрое — но не потому, что «канал медленный».gochannelsselectgoroutinesconcurrency25 МИН3 ИСТОЧНИКОВЧитать →СреднийОпубликованоФункции и замыкания в Go: что захватывается и когдаСобеседование по замыканиям идёт лестницей: можно ли сравнить две функции — что такое замыкание — что напечатает счётчик — что увидят замыкания, созданные в цикле — когда вычисляются аргументы defer и что сохраняет значение метода — передаёт ли f(s...) копию — во что обходится замыкание. Прогон отвечает на каждый шаг: замыкание читает присваивание, сделанное после его создания; один и тот же цикл дал c c c в модуле с go 1.21 и a b c с go 1.22; f(s...) записал в срез вызывающего; а вызов замыкания стоил столько же, сколько прямой вызов, — дорого только его создание.gofunctions-closuresclosuresloop-variablevariadic55 МИН8 ИСТОЧНИКОВЧитать →СреднийОпубликованоПаттерны конкурентности в Go: пул воркеров, конвейер, fan-inСобеседование по паттернам идёт лестницей: напишите пул воркеров — сколько воркеров взять — в каком порядке придут результаты — кто закрывает канал — как слить несколько каналов в один — как ограничить параллелизм — как остановить конвейер, если потребитель ушёл. Прогон отвечает на каждый шаг: на двух ядрах восемь воркеров считают не быстрее двух, а на задаче со сном — в восемь раз быстрее одного; стадия, забывшая close, роняет маленькую программу и молча вешает ту, где жив хоть один таймер; а потребитель, ушедший без отмены, оставляет две горутины навсегда.goconcurrency-patternsworker-poolpipelinefan-in65 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликованоContext в Go: отмена только вниз, cancel, который не про отмену, и Value, который дорожает с глубинойСобеседование идёт лестницей: зачем контекст — почему первым аргументом — что делает cancel и почему его вызывают всегда — чем Canceled отличается от DeadlineExceeded — что класть в Value. Замерено: забытый cancel оставляет 123 байта на потомка навсегда, а Value на двадцати слоях дороже в 18 раз.gocontextcancellationtimeoutconcurrency30 МИН4 ИСТОЧНИКОВЧитать →ПродвинутыйОпубликованоГонки данных в Go: что ломается, что видит -race и чего он не видитСобеседование по гонкам идёт лестницей: что такое гонка данных — чем она отличается от состояния гонки — как её найти — найдёт ли -race все гонки — можно ли включить его в продакшене. Прогон отвечает на каждый шаг: структура из двух слов без синхронизации отдаёт пары, которых никто не записывал; -race находит гонку, не испортившую результат, и не находит ту, что не выполнилась; а проверка и списание под разными захватами мьютекса уводят баланс в минус в 936 случаях из 1000 при нуле отчётов.godata-racesrace-detectormemory-modelconcurrency35 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликованоВстраивание структур в Go: композиция, которую путают с наследованиемСобеседование по встраиванию идёт лестницей: есть ли в Go наследование — что будет, если внешний тип объявит тот же метод — почему структура со встроенным типом не реализует интерфейс — что делать с двумя одинаковыми именами — зачем встраивать интерфейс. Прогон отвечает на каждый шаг: поднятый метод вызывает метод своего типа, а не внешнего; два одноимённых поля молча исчезают из JSON; а поднятый вызов в замере от прямого не отличается.goembeddingcompositionmethod-setsjson30 МИН3 ИСТОЧНИКОВЧитать →СреднийОпубликованоОшибки в Go: одна буква, которая рвёт цепочку, и утверждение типа, которое перестанет работатьСобеседование идёт лестницей: что такое error — чем %w отличается от %v — почему == не работает после обёртки — чем Is отличается от As — что делает Join — и сколько всё это стоит. Замерено: errors.Is на цепочке из двадцати обёрток дороже в 8,2 раза, а сама обёртка — это два выделения и 162 нс на пути ошибки.goerrorswrappingerrors-iserrors-as25 МИН4 ИСТОЧНИКОВЧитать →ПродвинутыйОпубликованоСтек, куча и escape-анализ: почему указатель ещё ничего не значитГде живёт значение, в Go решает не программист и не наличие & — решает компилятор, и вопрос у него один: переживёт ли значение свой кадр. Замерено: указатель без ухода наружу даёт ноль выделений, а структура без единого указателя — одно; уход в кучу стоит ×12,8, и цена растёт вместе с размером значения.gomemoryescape-analysisperformancecompiler30 МИН3 ИСТОЧНИКОВЧитать →ПродвинутыйОпубликованоСборщик мусора: почему пауза не растёт вместе с кучей, а GOGC ничего не ускоряетСборщик Go конкурентный и неперемещающий, и из этого выводится всё остальное: пауза короткая и от размера кучи не зависит, а GOGC — не ускоритель, а ручка обмена времени на память. Замерено: куча выросла в шестнадцать раз, медианная пауза — в 1,22, и даже эта разница меньше разброса между двумя повторами одного замера; GOGC=400 даёт вчетверо меньше циклов и в два с половиной раза больший пик памяти.gogcmemoryperformanceruntime30 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликованоHTTP-сервер в Go: что net/http делает сам и чего не делает, пока не попросятСобеседование по HTTP-серверу идёт лестницей: как сделать веб-сервер — как он обслуживает конкурентные запросы — какие таймауты задать — что делать, когда клиент ушёл, — как остановить сервер, не оборвав запросы. Прогон отвечает на каждый шаг: на каждое соединение — своя горутина и никакого встроенного предела; у http.Server{} все четыре таймаута нулевые, и молчащий клиент держит соединение сколько угодно; контекст запроса отменяется, а работа — нет; и Serve возвращается сразу после вызова Shutdown, пока запросы ещё в полёте.gonet-httphttp-servergraceful-shutdowntimeouts30 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликованоИнтерфейс в Go: два слова, nil, который не nil, и вызов дороже в четыре разаСобеседование по интерфейсам идёт лестницей: что лежит в переменной — почему nil бывает не nil — когда упаковка выделяет память — во что обходится вызов через интерфейс — как устроено утверждение типа. Урок проходит её целиком, и всё выводится из одного: слов два, а не одно.gointerfacesniltype-assertionruntime35 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликованоio.Reader и io.Writer: контракт из двух методовСобеседование по io идёт лестницей: что такое Reader — как написать цикл чтения — почему Read вернул меньше, чем просили, — как копирует io.Copy и сколько берёт памяти — зачем bufio и что будет без Flush — что не так с defer f.Close(). Прогон отвечает на каждый шаг: цикл, который смотрит на ошибку раньше байтов, теряет последний кусок; io.Copy гонит 100 000 байт одним Write, если у источника есть WriterTo; а функция с bufio и defer f.Close() возвращает nil и оставляет на диске пустой файл.goio-reader-writeriobufioio-copy55 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликованоJSON в Go: теги, omitempty и что encoding/json делает молчаСобеседование по encoding/json (v1) идёт лестницей: какие поля попадают в JSON — что считается пустым для omitempty — почему ключ NAME заполнил поле name — куда делся лишний ключ — почему id из базы приехал на единицу меньше — как написать свой MarshalJSON и не уронить процесс. Прогон отвечает на каждый шаг: пустая структура и нулевое time.Time проходят сквозь omitempty; Unmarshal не смотрит на регистр и молча пропускает лишнее; 9007199254740993 в any становится 9007199254740992; MarshalJSON на указателе не вызывается у значения без адреса; а в замере на go1.24.7, linux/amd64, Marshal структуры оказался дороже ручной сборки той же строки меньше чем вдвое — дорогой стороной был Unmarshal.gojson-encodingencoding-jsonstruct-tagsomitempty65 МИН8 ИСТОЧНИКОВЧитать →СреднийОпубликованоКарта в Go: группа из восьми, порог 7/8 и промах, который дороже попаданияСобеседование по картам идёт лестницей: что лежит в переменной — как устроен поиск — как она расширяется — какая асимптотика — почему порядок обхода «случайный» — что будет при конкурентном доступе. Урок проходит её целиком, и на каждой ступени показывает механизм: почему восьмую часть слотов карта держит пустой намеренно, почему расширение происходит при 7/8, а не при заполнении, и почему промах ищется дольше попадания.gomapsswiss-tablehashruntime40 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликованоПолучатель метода в Go: копия, которой не видно, и «указатель быстрее», которое неверноСобеседование по получателю идёт лестницей: что такое получатель — почему изменения не видны — почему тип «не реализует» интерфейс — когда компилятор берёт адрес сам, а когда отказывается — и во что обходится копия. Урок проходит её целиком: замер показывает, что до восьми слов разницы нет вовсе, а на килобайте она десятикратна.gomethodsreceiversmethod-setsperformance25 МИН3 ИСТОЧНИКОВЧитать →СреднийОпубликованоМодули и зависимости в Go: go.mod, go.sum и минимальная версияСобеседование по модулям идёт лестницей: что такое модуль — какую версию возьмёт go, если две библиотеки требуют разные — что делают go get и go mod tidy — зачем go.sum — почему импорт /v2 — как подменить зависимость локальной копией — что значит строка go в go.mod. Прогон отвечает на каждый шаг: a просил c v1.1.0, b — v1.3.0, в прокси лежала v1.9.0, а собралась v1.3.0 — и для a тоже; подменённый в прокси архив той же версии go не собрал, напечатав SECURITY ERROR; а replace в go.mod зависимости не сработал вовсе — каталога, на который он указывал, не существовало, и это не стало ошибкой.gogo-modulesgo-modgo-summvs60 МИН12 ИСТОЧНИКОВЧитать →СреднийОпубликованоПакеты в Go: видимость, init и структура проектаСобеседование по пакетам идёт лестницей: как сделать имя приватным — что такое пакет и при чём тут каталог — зачем internal — в каком порядке выполняются init — зачем импорт с подчёркиванием — как разложить проект. Прогон отвечает на каждый шаг: переменная, объявленная первой, инициализируется последней; пакет alpha инициализируется раньше zeta, хотя main импортирует zeta первым; импорт из internal запрещает не компилятор, а команда go; а у одного и того же нарушения видимости два разных сообщения компилятора.gopackagespackages-layoutexported-namesinit50 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликованоУказатели в Go: всё передаётся по значению, даже указательСобеседование по указателям идёт лестницей: что такое указатель — передаётся ли в Go что-нибудь по ссылке — что будет после p = nil внутри функции — можно ли вызвать метод на nil — почему у элемента карты нет адреса — можно ли вернуть указатель на локальную переменную — есть ли арифметика указателей. Прогон отвечает на каждый шаг: присваивание параметру-указателю снаружи не видно, метод на nil-получателе отработал, указатель на элемент среза после append пишет в старый массив, а функция, вернувшая указатель на свою переменную, в замере на go1.24.7 обошлась дороже копии в килобайт.gopointerspass-by-valueniladdressability55 МИН10 ИСТОЧНИКОВЧитать →ПродвинутыйОпубликованоПрофилирование в Go: что показывает pprof и чего он не показывает по умолчаниюСобеседование по профилированию идёт лестницей: как найти, что тормозит, — как снять профиль — чем flat отличается от cum — как искать утечку памяти — почему профиль блокировок пустой — можно ли профилировать в продакшене. Прогон отвечает на каждый шаг: профиль CPU — это 100 сэмплов в секунду; функция, выделившая почти 600 МБ, в профиле удерживаемой памяти отсутствует; профили блокировок и мьютексов пусты при настоящей конкуренции, пока их не включить; а цена профиля CPU — в пределах шума.goprofilingpprofperformancegoroutine-leaks30 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликованоРефлексия в Go: reflect.Type, reflect.Value и правило изменяемостиСобеседование по рефлексии идёт лестницей: что такое reflect.Type и reflect.Value — чем Type отличается от Kind — почему SetInt паникует — можно ли изменить приватное поле — как encoding/json узнаёт имена из тегов — чем DeepEqual отличается от == — сколько всё это стоит. Прогон отвечает на каждый шаг: ValueOf видит копию, и изменить её нельзя; у Celsius Type — main.Celsius, а Kind — int; неэкспортируемое поле читается, но не меняется; nil-срез не равен пустому даже для DeepEqual; а в замере на go1.24.7 поиск поля по имени оказался дороже записи по готовому индексу больше чем в десять раз.goreflectionreflectstruct-tagsdeepequal50 МИН6 ИСТОЧНИКОВЧитать →ПродвинутыйОпубликованоГорутины и планировщик Go: две тысячи байт, вытеснение и GOMAXPROCS, который ограничивает не то, что думаютСобеседование идёт лестницей: чем горутина отличается от потока — сколько она стоит — что такое GMP — что ограничивает GOMAXPROCS — вытесняющий ли планировщик — и когда горутина уходит с процессора. Замерено: 2000 байт стека плюс 500 байт структур, запуск с ожиданием дороже вызова в 82 раза, а уступка процессора стоит 106 нс.gogoroutinesschedulergmpgomaxprocs30 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликованоСрез в Go: окно над чужим массивом, два пути append и правило роста, которое все помнят неверноСрез не владеет данными: это окно над чужим массивом, и одна запись через одно окно видна во втором. Урок идёт от этой модели к механизму — два пути append, настоящая формула роста, что стоит за словами «амортизированно O(1)» и почему «×2 до 1024, дальше ×1,25» неверно в обоих числах.goslicesappendmemoryruntime35 МИН3 ИСТОЧНИКОВЧитать →СреднийОпубликованоСтроки, руны и байты: почему len не считает символы, а подстрока держит мегабайтыСтрока в Go — неизменяемая последовательность байтов, и корректный UTF-8 в ней не обязателен. Отсюда len даёт байты, s[i] даёт байт, а range декодирует UTF-8 в руны с байтовыми смещениями. Дальше — реализация: в gc строка — указатель и длина, подстрока не копирует и потому удерживает весь исходный массив. Замерено: десять байт держат восемь мегабайт, а склейка тысячи кусков через += дороже Builder в 111,8 раза.gostringsunicodeutf-8memory30 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликованоСтруктуры и методы в Go: значение, которое копируется целикомСобеседование по структурам идёт лестницей: нужен ли структуре конструктор — что будет после b := a — можно ли сравнить две структуры и сделать структуру ключом карты — сколько она весит — на каком типе можно объявить метод. Прогон отвечает на каждый шаг: после b := a правка в поле-срезе видна в a, а в поле-массиве — нет; == на структуре со срезом не собирается, а за интерфейсом — паникует; struct{ a bool; b int64; c bool } на linux/amd64 весит 24 байта, а с int64 первым — 16; и в замере на go1.24.7 множество map[string]struct{} на миллион ключей весит столько же, сколько map[string]bool, — 56 МБ.gostructs-methodsstructszero-valuememory-layout50 МИН7 ИСТОЧНИКОВЧитать →ПродвинутыйОпубликованоsync и atomic в Go: два совета, которые разваливаются при замереСобеседование идёт лестницей: что не так с обычным инкрементом — чем atomic отличается от мьютекса — когда брать RWMutex — что делает Once — почему копию мьютекса ловит go vet. Замерено: без конкуренции мьютекс дороже atomic вдвое, но с ростом числа претендентов он дорожает в 9,6 раза, а atomic — в 1,0; RWMutex же на короткой секции проигрывает обычному Mutex.gosyncatomicmutexconcurrency30 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликованоТестирование в Go: что на самом деле делают t.Run, t.Parallel и бенчмаркСобеседование по тестам идёт лестницей: что такое табличный тест — как запустить один случай — зачем t.Parallel и что с ним ломается — почему повторный go test не запускает тесты — что не так с бенчмарком. Прогон отвечает на каждый шаг: defer родителя закрывает ресурс раньше, чем начнутся параллельные подтесты; в модуле с go 1.21 все подтесты видят последний случай таблицы; четыре спящих подтеста идут парами, потому что -parallel равен GOMAXPROCS; а бенчмарк, выбросивший результат чистой встроенной функции, замерил пустой цикл.gotestingtable-driven-testst-parallelbenchmarks35 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликованоИнструменты go: build, vet, fmt и что они проверяютСобеседование по инструментам идёт лестницей: чем go run отличается от go build — почему вторая сборка мгновенная — зачем gofmt — что ловит go vet, если код компилируется — как собрать бинарник под Windows на Linux — как вшить в него версию. Прогон отвечает на каждый шаг: холодная сборка вызвала компилятор 55 раз, повторная — ни разу; Printf с неверным глаголом и копия sync.Mutex собрались без единой ошибки, а go vet поймал оба; go test пропустил копию мьютекса, потому что запускает только часть проверок vet; а -ldflags -X молча проигнорировал опечатку в имени переменной.gotoolinggo-vetgofmtbuild-cache45 МИН11 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14*args и **kwargs: две звёздочки, которые стоят по-разному — и одна из них делает неизвестные имена допустимыми*args — кортеж, **kwargs — словарь, и на этом сходство кончается. На передаче звёздочка почти бесплатна, а две дороже вчетверо. А **kwargs, дописанный «для гибкости», превращает опечатку в имени аргумента из TypeError в молча принятое значение по умолчанию — и заодно выбрасывает подсказку, которой интерпретатор научился в 3.13.functionsargumentscpython-internals30 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14async/await: вызов ничего не выполняет — и отсюда три ошибки, которые не падаютВызов async-функции не запускает её тело: он создаёт объект и возвращает управление. Из этого следует забытый await, который превращает любую проверку в истину; блокирующий вызов, который останавливает не одну задачу, а весь цикл событий вместе со всеми его задачами; и gather, у которого второе исключение остаётся при своей задаче, а вызывающему не достаётся.asyncioasync-awaitconcurrencycpython-internals45 МИН8 ИСТОЧНИКОВЧитать →СреднийОпубликованоdefer, panic и recover в Go: три момента вместо одного и recover, который молча не работаетСобеседование идёт лестницей: когда выполняется отложенный вызов — когда вычисляются его аргументы — в каком порядке — почему отложенная функция может изменить результат — что recover ловит и чего не ловит — и во что всё это обходится. Замерено: открытый defer 4,68 нс против 16,8 нс на виток в цикле, а паника против возврата ошибки — в 122 раза.godeferpanicrecoverruntime35 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Встроенные коллекции: list, tuple, dict и set — что когда братьВопрос «что выбрать — список, кортеж, словарь или множество» звучит как вопрос вкуса, а решается тремя проверками: нужен ли порядок, нужны ли повторы и как вы будете искать. Прогон отвечает числами: проверка x in по списку из тысячи элементов медленнее, чем по множеству, в 216 раз, а на десяти — всего в 3,4 (CPython 3.13.7, одна машина); словарь хранит порядок вставки, а множество строк печатается по-разному при разных PYTHONHASHSEED; и множество на тысячу чисел в 4,1 раза больше списка.choosing-collectionscollections-modulehashing30 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Классы и ООП: self — это аргумент, а атрибут класса — один на всехПервые вопросы по ООП звучат просто: что такое self, чем атрибут класса отличается от атрибута экземпляра, зачем classmethod и staticmethod, что делает __init__. Прогон отвечает на каждый: a.hello() и Greeter.hello(a) — один и тот же вызов; список, объявленный в теле класса, — один на все экземпляры; __init__ получает уже созданный объект; а класс с __eq__, но без __hash__, перестаёт быть ключом словаря и элементом множества.oop-inheritancemethods-selfdunder-methodsclassesself30 МИН5 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.12 · 3.13 · 3.14Замыкания: захвачена переменная, а не значение — и цепочка областей проходит мимо тела классаЗамыкание держит ячейку, а не копию: значение читается в момент вызова, и одна ячейка достаётся всем внутренним функциям сразу. Привычная цепочка «локальная → объемлющая → глобальная → встроенные» умалчивает о двух вещах, на которых и ошибаются: тело класса в неё не входит, а присваивание где угодно в теле делает имя локальным для всего тела.closuresscopenonlocalcpython-internals35 МИН5 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Сложность операций: O-нотация, посчитанная на встроенных типах«Оцените сложность этого кода» — вопрос, на который отвечают буквой O, а проверяют счётом. Прогон считает сравнения напрямую: x in по списку из ста тысяч элементов сделал сто тысяч сравнений, а по множеству без коллизий хешей — одно, с одним хешем на всех — порядка n; bisect — семнадцать; на сто тысяч append список перевыделил память 66 раз; генератор списка с x in по другому списку на тысяче элементов сделал миллион сравнений; а sorted() на уже отсортированных данных — ровно n − 1.algorithms-complexitybig-osortingbisect25 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Включения: граница области видимости проходит внутри квадратных скобок, а не по нимПеременная включения наружу не течёт — это знают все. Но первый итерируемый вычисляется снаружи, а остальные части внутри, и в теле класса это даёт NameError на имя, написанное строкой выше. Наружу при этом всё-таки течёт одна вещь: морж.comprehensionsscopewalruscpython-internals35 МИН5 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Контекстные менеджеры: два метода, одна возвращаемая истина и одна тихая потеря данныхПротокол — ровно два метода на типе. Но судьбу исключения после успешного входа определяет истинность одного значения: того, что вернул __exit__. Вернул истину — исключение исчезло вместе с остатком блока, и в логе не осталось ничего.context-managerswith-statementcontextlibcpython-internals40 МИН9 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Декораторы: выражения сверху вниз, применение снизу вверхДекоратор — обычный вызов, который случается один раз, когда выполняется def. Выражения при этом читаются сверху вниз, а применяются снизу вверх — из этой пары растёт всё остальное: и зачем нужен functools.wraps, и почему @classmethod под своим декоратором ломается молча.decoratorsfunctoolsdescriptorscpython-internals50 МИН9 ИСТОЧНИКОВЧитать →ПродвинутыйОпубликовано3.11 · 3.12 · 3.13 · 3.14Дескрипторы: одно правило приоритета, из которого растут property, методы и слотыДескриптор — это объект в классе, у которого есть __get__, __set__ или __delete__. Всё остальное — следствие одной строчки: дескриптор данных сильнее словаря экземпляра, дескриптор не-данных — слабее. Отсюда и работающий cached_property, и падающий @classmethod под чужим декоратором.descriptorsattributespropertycached-propertycpython-internals35 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Исключения и finally: одно слово, после которого функция перестаёт падатьСтоимость try, который не сработал, — около двух наносекунд, пять процентов, и байт-код объясняет почему. Дорого стоит другое: return в finally, который молча уничтожает исключение и превращает функцию в такую, из которой не выходит ни одна ошибка.exceptions-finallyexception-groupscpython-internals30 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Сборка мусора: почему объект жив, хотя ссылок на него нетВ обычной сборке CPython объект умирает в тот момент, когда на него пропадает последняя ссылка, — сборщик для этого не нужен. Сборщик нужен для циклов, и только для них. А объект, который «не умирает», может держать не мусорный цикл, а ссылка от живого объекта, которой не видно в коде: трассировка исключения, ключ кеша, метод в списке подписчиков.garbage-collectionreference-countinggcweakrefmemory-leaks25 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14is против ==: одно спрашивает про тождество, второе вызывает метод — и половина известных сюрпризов вообще не про них«is сравнивает объекты, == сравнивает значения» — верно и бесполезно: на вопрос «почему у меня 1000 is 1000 даёт True» это не отвечает. Ответов там три разных: кеш малых чисел, объединение констант компилятором и интернирование строк. Опираться в коде нельзя ни на один из трёх.identityequalityinterningnancpython-internals30 МИН7 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Итераторы и генераторы: один метод разницы и одна тихая ошибкаИтерируемое умеет отдать итератор, итератор умеет отдать следующий элемент — вся разница в одном методе. Но именно из неё растёт ошибка, которая не падает: функция, дважды прошедшая по генератору, получает во второй раз пустоту и возвращает неверный ответ молча.iterators-vs-iterablesgeneratorsitertoolscpython-internals40 МИН8 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14lambda: то же самое, что def, кроме двух вещей — и знаменитая ошибка с циклом не из их числаУ lambda и def один тип, один байт-код тела и неотличимое время вызова. Разница в имени и в том, что тело — выражение. А знаменитая ошибка с циклом к lambda вообще не относится: обычный def ломается точно так же.lambdacpython-internals20 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Список против кортежа: 16 байт, которые получаются только при одном сравнении из трёх, и две операции, в которых разницы нет«Кортеж легче на 16 байт» верно, только если сравнивать его со списком, построенным повторением. Против выросшего через append та же разница — 72 байта, и getsizeof показывает её честно. А «кортеж быстрее» верно ровно для одной операции из пяти, и не потому, что он кортеж.listtupledata-structurescpython-internals40 МИН6 ИСТОЧНИКОВЧитать →ПродвинутыйОпубликовано3.11 · 3.12 · 3.13 · 3.14Метаклассы: шесть шагов создания класса, из которых видно, что делать метаклассом, а что уже не надоМетакласс — это тип класса, и весь его протокол следует из одного этого. Порядок шагов рассыпан по трём разделам документации; собранный в колонку, он отвечает на все практические вопросы разом — включая тот, ради которого метаклассы обычно и не нужны.metaclasstypeinit-subclassclass-creationcpython-internals25 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Модули и импорт: модуль выполняется один раз, а from копирует ссылкуВопросы про импорт кажутся служебными, пока не спросят, что именно выполняется при import. Прогон отвечает: тело модуля выполнилось один раз на тысячу и три импорта — и снова, когда запись удалили из sys.modules; после from settings import DEBUG присваивание settings.DEBUG = True новое значение не показало; циклический импорт упал на from и заработал, когда обращение к модулю ушло внутрь функции; а свой файл random.py рядом со скриптом заслонил стандартный модуль.import-systemmodulespackagessys-modulescircular-import35 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14MRO и множественное наследование: C3, ромб и куда на самом деле ведёт super()Порядок поиска метода — не «слева направо, вглубь», а линеаризация C3, и выводится она из одного правила слияния. Из того же правила следуют и ромб, в котором общий предок уходит в хвост, и TypeError, которым Python отказывается создавать класс. А super() ведёт не к родителю, а к следующему в MRO экземпляра — и один и тот же метод зовёт разных соседей в разных иерархиях.mrosupermultiple-inheritancec3-linearizationcooperative-inheritance25 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Изменяемый аргумент по умолчанию: значение лежит в поле функции, и оно одно на все вызовыПравило «не пиши список в аргумент по умолчанию» запоминают все и всё равно нарушают, потому что оно выглядит произвольным запретом. Оно перестаёт быть произвольным, как только видно, где это значение лежит: в __defaults__ функции, созданное один раз при выполнении def.defaultsmutabilityfunctionsdataclasses25 МИН5 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Копирование: присваивание не копирует ничего, copy — один уровень, deepcopy — граф целикомТри операции, которые путают между собой, потому что на плоском списке они дают одинаковый результат. Разница видна только на вложенности — и на ней же выясняется, что глубокая копия не «копирует всё подряд»: общий объект остаётся общим, цикл остаётся циклом, а __init__ не вызывается ни разу.copydeepcopymutabilityreferencescpython-internals30 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Строки и форматирование: неизменяемость, срезы, f-строки и цена склейкиВопросы про строки проверяют, понимаете ли вы, что строка неизменяема и что из этого следует. Прогон отвечает: s[0] = 'П' даёт TypeError; после t = s; s += '!' имя t по-прежнему видит старую строку; границы среза урезаются до длины строки — 'Python'[10:20] пустая, 'Python'[:100] целая, а индекс за пределами — IndexError; f'{2.5:.0f}' печатает 2, потому что округление идёт к чётному; 'привет' — шесть кодовых точек и двенадцать байт; а быстрый путь CPython для s += x срабатывает не всегда: его выключили лишняя ссылка, посчитанный хеш, подкласс str, символ вне ASCII и — на 3.13 и 3.14 — второй оператор на той же строке кода. Собирать строку надёжно через join.strings-formattingf-stringsstr-bytes35 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Потоки и процессы: что выбрать для вычислений, а что — для ожидания«Потоки или процессы?» — вопрос, на который отвечают правилом, а проверяют пониманием причины. Прогон на CPython 3.13.7 (обычная сборка с GIL, linux, 2 ядра) отвечает: четыре вычислительные задачи в четырёх потоках заняли 0,478 с против 0,502 последовательно, в четырёх процессах — 0,262; четыре ожидания по 0,2 с в потоках — 0,201 с; поток пишет в общий список, а процесс — в свою копию; лямбду в пул процессов не передать; пул процессов создают в main() и запускают файлом; и на 3.14 под Linux процессы по умолчанию запускаются способом forkserver, а не fork.threads-vs-processesthreadingmultiprocessingconcurrent-futures25 МИН6 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Тестирование: unittest, pytest и mock — что на самом деле проверяетсяВопросы про тесты проверяют не знание синтаксиса, а понимание того, что тест проверяет на самом деле. Прогон отвечает: упавшая проверка и исключение в тесте — разные исходы; patch('time.time') не подменяет время в модуле, который сделал from time import time; Mock без спецификации принимает вызов с неверным числом аргументов и метод с опечаткой в имени; опечатка в имени проверки m.called_once() на 3.11 проходит молча, а с 3.12 — ошибка; и unittest запускает тесты по алфавиту, а не в порядке объявления.testingunittestpytestmock40 МИН3 ИСТОЧНИКОВЧитать →СреднийОпубликовано3.11 · 3.12 · 3.13 · 3.14Окружения и зависимости: что делает venv, а что — pipВопросы про виртуальные окружения кажутся бытовыми, пока не спросят, как интерпретатор понимает, что он в окружении. Прогон отвечает: окружение — это папка со своим bin/python и файлом pyvenv.cfg; env/bin/python ставит sys.prefix на окружение без всякой активации; activate — скрипт оболочки, который ставит env/bin первым в PATH и задаёт VIRTUAL_ENV и VIRTUAL_ENV_PROMPT; пакет, поставленный в окружение, базовому интерпретатору не виден; pip freeze печатает снимок установленного, а не lock-файл; в свежем окружении на 3.11 два пакета — pip и setuptools, — а с 3.12 один pip; а с 3.14 sys.prefix окружения ставит инициализация путей, и python -S его уже не сбрасывает.venv-dependenciesvenvpiprequirements25 МИН7 ИСТОЧНИКОВЧитать →СреднийОпубликованоОчередь accept: соединение установлено, а сервер о нём не знаетКлиент подключился, отправил запрос и ждёт. Сервер при этом не вызвал `accept` ни разу и о клиенте не подозревает. Между ними стоит очередь ядра — и почти всё, что выглядит как «сеть подтормаживает», на самом деле происходит в ней.srelinuxtcpbacklogaccept25 МИН3 ИСТОЧНИКОВЧитать →СреднийОпубликованоcgroups и OOM-killer: кого убьют, почему в логе пусто и что на самом деле значит 137Контейнер исчез, в логе приложения — ни строчки, в отчёте оркестратора — код 137. Это не загадка, а нормальная работа двух механизмов: учёт памяти по группе процессов и убийство сигналом, который нельзя перехватить. Урок разбирает оба и показывает прогоном, что потребление дошло до лимита и не превысило его: убивают не за превышение, а за то, что освобождать внутри группы стало нечего.srelinuxcgroupsmemoryoom25 МИН5 ИСТОЧНИКОВЧитать →СреднийОпубликованоКонсистентное хеширование: сколько ключей переезжает при смене состава«Хеш по модулю числа узлов» работает ровно до первого изменения состава. Посчитано точно: добавление одного узла к восьми переносит 88,9 % ключей — в восемь раз больше необходимого. Кольцо переносит 11,5 %, а при удалении узла не трогает ни одного чужого ключа. Цена — перекос, который без виртуальных узлов достигает 90 крат.sreshardinghashingdistributedload-balancing25 МИН1 ИСТОЧНИКЧитать →СреднийОпубликованоToo many open files: чей это лимит, почему он не общий и что считается дескрипторомОшибку «Too many open files» (пер.: «слишком много открытых файлов») чинят перезапуском и правкой конфига, редко разбираясь, чей именно лимит кончился. Урок показывает механизм целиком: два лимита вместо одного, наследование при запуске, и то, что дескриптор — это не файл, а любой открытый объект, включая каждое сетевое соединение.srelinuxrlimitfile-descriptorsemfile25 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликованоGraceful shutdown: откуда берутся пятисотки при каждой выкатке«При деплое немного ошибок» — не свойство природы, а следствие одного решения: что делает процесс, получив SIGTERM. Урок измеряет обе ветки на настоящем сервере под нагрузкой: немедленный выход обрывает все запросы в работе, дренаж не обрывает ни одного и стоит остаток начатой работы.sreshutdowndeploysignalsavailability20 МИН2 ИСТОЧНИКОВЧитать →СреднийОпубликованоHealth-checks: три разные проверки, которые зовут одним словомLiveness спрашивает «жив ли ты», readiness — «готов ли ты принимать», startup — «успел ли ты подняться». Их путают, и цена путаницы измерима: проба, завязанная на общую базу, превращает одну аварию базы в перезапуск всех реплик сразу.srehealth-checkslivenessreadinesscascade20 МИН1 ИСТОЧНИКЧитать →СреднийОпубликованоИдемпотентность: «ровно один раз» бывает в эффекте, а не в доставкеПовтор запроса — не решение, а способ сделать эффект дважды. Измерено: четыре повтора без ключа дали четыре эффекта, с ключом — один. Между эффектом и ответом есть окно, которое закрыть нельзя: измерено, как клиент видит отказ там, где на сервере уже всё случилось. А порядок работы с ключом решает всё: занять до работы — один эффект, записать после — два.sreidempotencyretriesdistributedreliability25 МИН1 ИСТОЧНИКЧитать →СреднийОпубликованоБалансировка: почему случайный выбор двух бьёт круговой переборКруговой перебор раздаёт запросы поровну — и именно поэтому проигрывает, как только запросы перестают быть одинаковыми. В модели он дал хвост в 582 мс там, где опрос двух случайных серверов дал 123,6, а опрос всех шестнадцати — 42,9. Урок про то, откуда берётся эта разница и почему на однородной нагрузке её не видно.sreload-balancingqueueinglatencynetworking25 МИН1 ИСТОЧНИКЧитать →СреднийОпубликованоРазрешение имени: приложение зовёт не DNS, а getaddrinfoМежду строкой «медленный DNS» в постмортеме и настоящим сервером имён стоит библиотека и три файла. Почти всё, за что ругают сеть, решается в них: ответ может прийти вообще без сети, кеша в процессе нет, а имя, которого не существует, стоит секунд, посчитанных из настроек, — и таймаут, поставленный на сокет, их не покрывает.srelinuxdnsgetaddrinforesolver30 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликованоPage cache и fsync: почему «записали» и «не потеряется» — разные утверждения`write` возвращается до того, как данные окажутся на диске: они попадают в кеш ядра. Отсюда всё остальное — почему записанное переживает падение процесса, но при потере питания может быть потеряно, почему сброс дороже записи в десятки раз и почему база данных фиксирует транзакции пачками.srelinuxfsyncpage-cachedurability25 МИН3 ИСТОЧНИКОВЧитать →СреднийОпубликованоОчередь и хвост: почему рост нагрузки на 19 % даёт рост задержки вчетвероЗагрузка выросла с 0,80 до 0,95 — это плюс девятнадцать процентов. Время в системе выросло вчетверо. Урок разбирает, откуда берётся эта непропорциональность, и заодно опровергает ходячее «хвост растёт быстрее среднего»: в чистой очереди это не так, а причина расхождения хвоста и среднего — другая.srequeueinglatencypercentilescapacity30 МИН1 ИСТОЧНИКЧитать →СреднийОпубликованоРетраи и джиттер: почему сервис падает во второй раз в момент восстановленияПовтор запроса — самая дешёвая на вид мера и самый простой способ добить сервис: он добавляет нагрузку ровно тогда, когда её труднее всего принять, а одинаковое расписание повторов собирает тысячу клиентов в один миг. Урок разбирает механизм на модели: что с этим делает выдержка, что делает джиттер и чем за него приходится платить.sreretriesbackoffjitteroverload25 МИН1 ИСТОЧНИКЧитать →СреднийОпубликованоВыкатка и откат: почему канарейка не помогает без нужного детектораЧетыре стратегии выкатки на одном и том же дефекте — и неожиданный результат: одна и та же канарейка задевает в двадцать два раза меньше запросов, если тревога поднимается по доле ошибок. С детектором, который ждёт двухсот ошибок, она проигрывает переключению всего трафика разом.sredeploycanaryrollbackrelease25 МИН1 ИСТОЧНИКЧитать →СреднийОпубликованоСигналы, зомби и PID 1: почему контейнер не останавливается по SIGTERMРазговор идёт лестницей: что такое зомби — кому достаётся сирота — почему контейнер не останавливается сразу — что означает код 137. Первые две ступени держатся на том, что смерть процесса состоит из двух событий; последние две — на правиле ядра, которое почти никогда не называют вслух: у процесса с номером 1 в своём пространстве имён умолчательные действия сигналов не выполняются.srelinuxsignalsprocessescontainers25 МИН4 ИСТОЧНИКОВЧитать →СреднийОпубликованоSLO и бюджет ошибок: почему «99,9 %» без окна не значит ничегоОдна и та же цель означает 43 минуты в месяц и полторы минуты в сутки. Урок разбирает арифметику окна, показывает, что незаметный инцидент съедает бюджета больше заметного, и объясняет, почему выбор между календарным и скользящим окном — это выбор правила, по которому кончается заморозка выкаток.sresloerror-budgetreliabilitymetrics25 МИН1 ИСТОЧНИКЧитать →СреднийОпубликованоTIME_WAIT и эфемерные порты: клиент упирается раньше сервераДесятки тысяч сокетов в TIME_WAIT выглядят как утечка, а это нормальное состояние — и лечат его обычно не тем. Измерено: при обычном закрытии сокеты достаются стороне, которая закрывает активно; состояние живёт минуту; а упирается клиент не в память, а в номера портов, которых конечное число. Настройки, которые крутят вместо лечения, описаны в tcp(7) — и ни одна из них не про длительность.srelinuxtcpnetworkingtime-wait25 МИН3 ИСТОЧНИКОВЧитать →СреднийОпубликованоTLS-рукопожатие: за что платит первое соединение«Возобновление сессии экономит круг» — фраза про TLS 1.2, которую продолжают говорить про 1.3. Измерено на звене с известной задержкой: полное рукопожатие 1.3 стоит один круг, 1.2 — два, а возобновление в 1.3 не экономит ни одного. Экономит оно другое, и это видно в байтах.sretlsnetworkinglatencyhandshake25 МИН2 ИСТОЧНИКОВЧитать →СреднийОпубликованоТаймаут и дедлайн: почему клиент ушёл, а работа продолжаетсяТаймаут ограничивает ожидание того, кто ждёт, и ничего не сообщает тому, кто работает. Отсюда две вещи, которые обычно обнаруживают в проде: цепочка из трёх сервисов с секундным таймаутом на каждом шаге может работать три секунды, а после ухода клиента вся цепочка продолжает занимать соединения и обработчики.sretimeoutsdeadlinesnetworkingcancellation25 МИН2 ИСТОЧНИКОВЧитать →

Разобрано статьями

Эти механизмы спрашивают не реже, но разобраны они статьёй — целиком, с замерами и источниками.

GIL: не «Python не умеет в потоки», а «поток ждёт столько, сколько вы попросили»статьяИнтервал переключения — настройка, которая работает: медиана ожидания следует за ней и на 5 и 50 мс отличается на единицы процентов. Пока не наступает случай, в котором она не работает вовсе — и он измеряется в сотнях миллисекунд.__slots__: что меняет в объектной модели и сколько это стоит в байтахстатьяСначала механизм: перечисленные имена получают дескрипторы данных на классе и места по фиксированным смещениям, а __dict__ и __weakref__ перестают создаваться сами. Всё это — контракт языка, одинаковый на 3.11–3.14. И только потом цена, которая контрактом не является: 40 байт на экземпляр, 42 %, причём sys.getsizeof показывает обратное, basicsize молчит про __weakref__, а сумма двух getsizeof отвечает по-разному в начале программы и потом.Словари Python изнутри: хеш-таблица CPythonстатьяХеширование, открытая адресация и переписывание словаря в 2016 году, которое сделало порядок вставки побочным эффектом реализации — а через один релиз сделало его гарантией языка.Дженерики в Go: одиннадцать типов, шесть тел кода и метод, до которого всё равно идут через словарьстатьяСначала язык: ограничение задаёт множество типов, множество типов задаёт операции, а сохранить связь между типом входа и типом выхода не умеет ни один интерфейс. Потом компилятор: Go порождает версию не под каждый тип, а под форму — одиннадцать типов дали шесть тел кода, три разных указателя одно. И только потом замер: дженерик по ограничению с методами прямого вызова не даёт и отстаёт вчетверо, но четвёрка эта на четыре пятых состоит из потерянного встраивания, а не из косвенности, и это здесь измерено, а не предположено.

Граф знаний

НЕЛИНЕЙНО · С УЧЁТОМ ПРЕДПОСЫЛОК

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

30 вершин · 30 связей

Без связей: maps

Связи списком · 29