Мой друг Алексей ищет работу и ходит на собеседования. После которых интересуется, как бы я ответил на некоторые из заданных вопросов.
Отвечая на один такой вопрос, я слегка увлёкся, и материала набралось на целую статью. Впрочем, небольшую и несерьёзную - пятничного формата.
Хотите немного развлечься? Вопрос лёгкий. Надеюсь, вы попытаетесь ответить на него самостоятельно, прежде чем читать дальше. Итак:
"Сложить два целых числа (от 1 до 99) без использования оператора 'плюс'. Дайте пять разных ответов"
Ну как? Придумали пять ответов? Давайте сравним. Если будет что-то такое, до чего я не додумался - добавляйте в комментарии.
Дальше - художественно обработанная "стенограмма" моего общения с другом.
Первое, что приходит в голову - "минус на минус даёт плюс":
plus1 = lambda a,b: a - (-b)
>>> plus1(22,6)
28
Видишь ошибку? Она здесь есть. И на интервью её заметят. Но не будем пока отвлекаться - в конце объясню.
Второй вариант, думаю, очевиден:
import math
plus2 = lambda a,b: int(math.log10(10**a * 10**b))
Пояснение для читателей
Используется равенство an+m = an * am
Соответственно, логарифм от an+m равен n+m
С помощью модуля operator:
import operator
plus3 = lambda a,b: operator.add(a,b)
Но можно и напрямую, без этого модуля:
plus4 = lambda a,b: a.__add__(b)
Впрочем, есть готовая встроенная функция:
plus5 = lambda a,b: sum([a,b])
И даже вот так можно:
plus6 = lambda a,b: list(range(a, 200, b))[1]
Или через длину строки:
plus7 = lambda a,b: len(''.join([a*'#', b*'*']))
Python вообще богат на варианты:
plus8 = lambda a,b: eval('a + b')
Тут мой товарищ возмутился, что видит плюс, а плюс использовать нельзя. Спорный вопрос. В условии говорится про оператор '+', а здесь он просто символ. Хотя eval его, конечно, исполняет как оператор.
Впрочем, не буду спорить с другом:
plus9 = lambda a,b: eval('a \N{PLUS SIGN} b')
Пояснение для читателей
Используется символ плюса через его название в Unicode
Друг: "Мне кажется, меня тут пытаются обмануть. Что это ещё за PLUS SIGN?"
Ладно! Сейчас не будет никаких плюсов:
plus10 = lambda a,b: eval("".join(map(chr, [97, 32, 43, 32, 98])))
Пояснение для читателей
С помощью join из отдельных символов собирается строка 'a + b'
"Так! Никаких больше eval!"
Хорошо. Кстати, я тут придумал ещё пару вариантов. Правда, с плюсом, но Python даже не будет этот плюс исполнять. Вариант первый:
import sqlite3
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()
plus11 = lambda a,b: cursor.execute('select ? + ?', (a,b)).fetchone()[0]
Вариант второй (для Linux, FreeBSD и т.п.):
import os
plus12 = lambda a,b: int(os.popen(f'expr {a} + {b}').read().strip())
"Э-э-э, нет... Давай вот без этого. Только встроенными средствами Питона. А то так можно в каком-нибудь онлайн-калькуляторе два числа сложить, а потом распарсить ответ"
Эх... А я только собирался предложить что-нибудь эдакое. Что-ж... Придётся вспомнить детство. Складываем "в столбик" двоичные представления чисел:
def plus13(aa,bb):
a = f'{aa:0>8b}'
b = f'{bb:0>8b}'
result = ['0'] * 8
carry_bit = '0'
for i in range(7, -1, -1):
if a[i]=='1' and b[i]=='1':
result[i] = carry_bit
carry_bit = '1'
elif (a[i]=='1' and b[i]=='0') or (a[i]=='0' and b[i]=='1'):
if carry_bit == '0':
result[i] = '1'
else:
if carry_bit == '1':
result[i] = '1'
carry_bit = '0'
return int(''.join(result),2)
Пояснение на примере
22 + 6 = 10110 + 00110 (считаем справа налево, всего пять шагов)
1 | 2 | 3 | 4 | 5
| Ў1 | Ў1 | |
10110 | 10110 | 10110 | 10110 | 10110
00110 | 00110 | 00110 | 00110 | 00110
----- | ----- | ----- | ----- | -----
0 | 00 | 100 | 1100 | 11100 = 28
Шаг 2) 1 + 1 = 10. Что не вмещается в двоичный разряд. Поэтому 0 пишем, а не вместившийся бит (бит переноса, carry bit) переходит в следующий разряд.
Шаг 3) 1 + 1 = 10 плюс бит переноса = 11. Пишем один и один переносим.
А собственно... Что это я в бирюльки играюсь? Можно обрабатывать все разряды одновременно:
def plus14(a, b):
while b != 0:
carry_bits = a & b
a = a ^ b
b = carry_bits << 1
return a
Пояснение для читателей
Сначала используем битовое И (&). Так мы узнаем разряды, в которых появится переполнение. Соответственно, на разряд левее нужно будет добавить биты переноса. Для этого сдвигаем полученное число на бит влево (00110 << 1 = 01100). И получаем первое слагаемое для следующего цикла. Или выходим из цикла, если битов переноса нет (одно из слагаемых стало равно нулю, значит вычисление завершено).
10110
00110
----- &
00110 << 1 = 01100
С помощью исключающего ИЛИ (^) устанавливаем в 0 переполненные разряды и оставляем неизменными непереполненные. Это будет второе слагаемое для следующего цикла или конечный результат, если вычисления завершены.
10110
00110
----- ^
10000
Можно даже сделать рекурсивный вариант:
def plus15(a, b):
if b == 0:
return a
else:
return plus15(a ^ b, (a & b) << 1)
А теперь - внимание! Барабанная дробь... Смертельный номер! Закат Солнца вручную:
import types
co = types.CodeType(2, 0, 0, 2, 0, 0, b'|\x00|\x01\x17\x00S\x00', (), (),
('a','b'), '', '', 1, b'')
plus16 = types.FunctionType(co, globals())
"Так... Секундочку... Что это сейчас было?"
Ты-же в курсе, что внутри функции есть CodeObject, который состоит из байт-кода Питона и нескольких параметров (определение переменных, размер стека и т.п.). Этот объект можно сгенерировать вручную и получить из него работающую функцию.
"Ну да. Ты ещё скажи, что в голове питоновские программы в байт-код компилируешь :)"
Нет, конечно. Просто я это пару дней назад смотрел и пока ещё помню.
На самом деле там несложно
>>> import dis
>>> dis.dis(co)
1 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
То есть, это обычное сложение через стек.
Вообще, к байткоду функции легко добраться:
>>> bytecode = plus16.__code__.co_code
>>> bytecode
b'|\x00|\x01\x17\x00S\x00'
>>> list(bytecode)
[124, 0, 124, 1, 23, 0, 83, 0]
Видно, что операции в байткоде состоят из кодов команд (opcode) и аргументов (oparg). Вот команды:
124 - LOAD_FAST # |
23 - BINARY_ADD # \x17
83 - RETURN_VALUE # S
С помощью dis.opmap и dis.opname их можно преобразовывать туда-сюда:
>>> dis.opname[124]
'LOAD_FAST'
>>> dis.opmap('LOAD_CONST')
100
Аргумент операции - это номер в списке переменных. В нашем случае список состоит из двух переменных a и b, которые загоняются в стек и складываются.
Примечание: если Питон версии ниже 3.8, то там перед байт-кодом пять параметров, а не шесть (в 3.8 добавились "только позиционные аргументы").
Кстати, без модуля types можно обойтись. Переменные типа "функция" и "объект кода" можно клонировать из других объектов:
f = lambda: ...
function = type(f)
code = type(f.__code__)
co = code(2, 0, 0, 2, 0, 0, b'|\x00|\x01\x17\x00S\x00', (), (),
('a','b'), '', '', 1, b'')
plus17 = function(co, globals() )
"Три точки в первой строке - это Ellipsis?"
Да. Объект-заполнитель, который здесь используется вместо pass. Появился в последних версиях.
О! Насчёт последних версий. Если у тебя Python версии 3.8+, там есть замена кодового объекта:
def plus18(a,b): ...
plus18.__code__ = plus18.__code__.replace(
co_code=b'|\x00|\x01\x17\x00S\x00')
Видал, какая чёрная магия? Весь "обвес" остаётся от исходной функции, а меняется только нужная часть (в этом примере - байт-код).
Вообще, там не обязательно должен быть байткод в явном виде. Можно не заморачиваться с преобразованием и писать вот так:
plus18.__code__ = plus18.__code__.replace(
co_code=(lambda a,b: a + b).__code__.co_code)
И, раз уж я полез во внутренности, можно задействовать подсчёт ссылок:
def plus19(a,b):
lst = []
value = 0
before = sys.getrefcount(value)
for i in range(a):
lst.append(value)
for i in range(b):
lst.append(value)
return sys.getrefcount(value) - before
Вот, как-то так...
. . .
{прошло несколько минут}
. . .
"Что молчишь? Нет больше вариантов?"
Один ещё есть. Только я формулу забыл. Пришлось в интернете посмотреть. Через разложение косинуса суммы углов:
from math import cos, sin, acos
def plus20(a,b):
a = a / 200
b = b / 200
result = acos(cos(a)*cos(b) - sin(a)*sin(b)) * 200
return int(round(result, 0))
Пояснение для читателей
Используется формула cos(a+b) = cos(a)*cos(b) - sin(a)*sin(b)
Максимально возможная сумма в этой задаче - 198. А тригонометрия считается в радианах. Чтобы исключить неоднозначность и гарантированно остаться в пределах четверти круга (около полутора радиан) - я просто делю и умножаю на 200.
А пока я мысленно представлял транспортир, вспомнилась ещё и функция enumerate, которая нумерует элементы:
plus21 = lambda a,b: list(enumerate([*range(-1,a), *range(b)]))[-1][0]
Вот на этом, пожалуй, всё... Сходу больше ничего в голову не приходит. Разве что на регулярных выражениях выкрутить. Но это ты уже сам сделай в качестве домашнего задания. А мне пока выдай ещё какой-нибудь каверзный вопрос.
"Вопрос я выдам. Не вопрос. Что там с первым ответом? Где ошибка?"
Ошибка в том, что по PEP 8 не рекомендуется присваивать лямбды. Надо использовать обычное определение функции через def, т.к. это "more useful for tracebacks and string representations".
То есть, неправильно писать
fun1 = lambda a: a**2
надо использовать так:
def fun2(a): return a**2
Потому что лямбды делались именно для того, чтобы оставаться безымянными и никуда не присваиваться. Смотри:
>>> fun1
<function <lambda> at 0x0000024FEE36F1F0>
>>> fun3 = lambda a: 2 * a
>>> fun3
<function <lambda> at 0x000001A7571BA550>
>>> fun2
<function fun2 at 0x0000024FEE36F3A0>
Видишь? У всех лямбд одинаковое имя - <lambda>. Когда я тебе однострочные примеры пишу - это неважно. А на собеседовании лучше делать так, как рекомендуют.
"А можно лямбде имя присвоить?"
Да без проблем!
>>> fun1.__qualname__ = 'fun1'
>>> fun1
<function fun1 at 0x0000024FEE36F1F0>
Только зачем такие сложности, если можно сразу через def объявить. Кроме того, всё ещё видно, что это лямбда:
>>> fun1.__code__.co_name
'<lambda>'
В отличие от
>>> fun2.__code__.co_name
'fun2'
При этом параметр co_name - readonly, т.е. напрямую имя не поменять. Надо использовать __code__.replace (но это только в Python 3.8+):
>>> fun1.__code__ = fun1.__code__.replace(co_name='fun1')
И ещё в одном месте:
>>> fun1.__name__ = 'fun1'
Теперь она не отличается от обычной функции:
>>> fun1.__code__.co_name
'fun1'
>>> fun1.__qualname__
'fun1'
>>> fun1.__name__
'fun1'
>>> fun1
<function fun1 at 0x0000024FEE36F1F0>
Реально проще использовать def.
И я всё ещё жду новый вопрос...
Впрочем... Забавно у вас там на собеседованиях. Самому, что-ли, сходить? Ни разу не был.
Akuma
Бежать надо от таких вопросов и работодателей.
Хотите разузнать о моих математических навыках? Спросите, а не делайте спектакль безумия.
Заметил, что только в ИТ сфере есть такие дебильные собеседования.
— Как вы доставите пиццу, если у вас откажут ноги?
— Как вы почините кран, если у вас будет шесть пальцев?
— Как провести трахеотомию при помощи пвх трубы и зажигалки?
— У вас есть пластилин, кинетический песок и жвачка. Постройте 9-этажный дом. Не забудьте про СНИПы.
tumbler
Забавно, что иногда такие задачи в ИТ встречаются: например, как на лету прочитать дамп ffmpeg vstats при условии, что он пишется исключительно в файл (в стандартный выхлоп не умеет).
И первой мыслью в ответе на вопрос статьи у меня было "никак" (довольно сложно совершить переход от неприятия идиотизма задачи к первому решению, хотя дальше уже ход мысли понятен).
И да, в других сферах жизнидеятельности вряд ли найдется человек, который удалит гланды с помощью разложения синуса суммы углов)
Akuma
Ну так предлагайте реальные задачи, а не «не используйте операцию сложения». Нет в природе ситуаций, когда нельзя использовать сложение.
Само-собой, бывает, что нужно провести операцию в лифте, отрезать ногу в поле, замотать трубу тряпкой в эпоксидке, отбиться от гопников перед подъездом клиента. Но никто не нападает на кандидатов перед собеседованием и не запирает в лифте с больным. Когда-то давно были только люки от гугла, теперь вот пошло в массы.
AllexIn
Конечно же в природе есть ситуации, когда нельзя использовать сложение.
Например, когда ваша задача реализовать сложение в архитектуре процессора который вы разрабатываете.
Akuma
На питоне? :)
AllexIn
В природе.
saboteur_kiev
При разработке архитектуры, наверное будет невыгодно реализовывать сложение через логарифмы или вычитание. Иначе это будет очень очень медленный и плохой процессор.
zuko3d
На объяснение «реальной задачи» может уйти 10 минут. И если я уже знаю, что «реальная задача» сводится к «найти наибольший элемент в массиве без использования оператора сравнения» — то я сэкономлю 10 минут и дам сразу задачу в сжатом виде. Потому что на собесе время ограничено. И если кандидат успеет быстро решить задачу — то поясню, откуда она такая взялась и как она относится к нашей деятельности. Но только если времени хватит.
Nalivai
Если вы не можете придумать реальную задачу, которую можно объяснить быстрее чем за 10 минут, это не проблема задач.
Если ваша реальная задача сводится к тому, чтобы заниматься таким вот извращением, возможно вам стоит отойти на шаг подальше, и понять где именно вы свернули не туда, а не собеседования устраивать.
zuko3d
Я вижу тут какое-то противоречие: если вы придумываете задачу — она уже по определению не реальная.
Кажется, вы забываете о том, что не всё программирование делается по шаблону. И иногда приходится обрабатывать терабайты неструктурированных данных для которых оператор сравнения не определён (точки на графе, например).
ZuOverture
zuko3d
Я согласен, что декомпозицию так не проверить. Но когда я провожу собеседование — я сначала проверяю, что человек тянет хотя бы на джуна, а потом уже даю мидловские/сениорские задачи.
albartash
Если собеседовать «по матрице», то подход верен. А если на конкретную позицию, требующую левел, то я бы сразу переходил к вопросам повыше (слегка странно на сеньора слышать вопросы а-ля «как создать класс исключения и потом использовать его в коде», причем кейс — реальный). Максимум — это вопросы, тянущие на полгрейда ниже — все-таки резюме интервьюер перед собесом видит. Так и время сэкономится, и качество отбора повыше будет. Имхо.
avkor2021
Не всё то правда, что в резюме. Не каждый претендент на сеньора — джун. А вообще проще надо всем быть. А то одни изображают из себя супер профессионалов и считают, что алгоритмические задачи недостойны того, чтобы их решать, я ведь не джун какой-то. А потом выясняется что решить то эту «задачку» не так-то просто. А другие придумавают такие «задачки», которые чтобы решить надо быть Вассерманом в программировании, а потом выясняется, что на работе надо домашний страницы делать. В общем баланс должен быть везде и гармония :)
albartash
Конечно, в резюме много чего можно встретить. Но я немного не о том.
Идея в том, чтобы на собесе на сеньора сразу спросить вопросы, по ответам на которые человек либо явно сеньор, либо скорее мидл, либо точно джун. Эдакие маячки. Вряд ли по ответу на вопрос «что такое декоратор» можно такое предполагать (ну разве что кандидат, увлекшись, начнет декоратор сразу в байткоде набрасывать :) ).
А баланса, да, побольше бы. :)
zuko3d
Да, тут локальная специфика — я примерно никогда не собеседую людей на конкретную позицию. Очень часто провожу предварительные секции, на которых нужно оценить общий уровень кандидата и дальше с ним уже будут общаться по конкретным позициям.
В моей практике были соискатели, которые неплохо отвечали на сениорские вопросы по архитектуре, но при этом не могли написать джуновский код. Поэтому я в любом случае прошу сделать какую-то минимальную задачку с кодом даже если кандидат идёт на высокую позицию. Почти все умелые кандидаты эту задачу делают за 5-7 минут.
zuek
Из решений "в лоб" — присваивать каждому значению элемент массива с индексом его значения и выводить "последний элемент", но с кучей ограничений — как минимум, значения целочисленные, а интепретатор языка располагает элементы массива в порядке возрастания индекса (оба упомянутых ограничения — весьма трудновыполнимы, в общем случае).
Aquahawk
Ну вообще-то, такая программа как tail именно это и делает, читает в свой stdout всё что валится в файл.
tumbler
А еще есть именованные каналы
mx-yh
Он не не умеет, а сделано это целенаправленно.
ffmpeg в stdout может выводить видео или аудио поток, и тогда этот поток можно принимать другой программой обработки.
Именно по этому, ffmpeg делает текстовый вывод в stderr. Но ни кто не мешает stderr перенаправить в stdout средствами операционной системы:
fmpeg -i input.mp3 2>&1
tumbler
Почитайте документацию, посмотрите код. Vstats пишется исключительно в файл.
mrise
ИМХО, самое ужасное в этих вопросах — зачастую очень плохо задаются условия задачи и ограничения.
В статье это тоже есть. Язык — только в тегах, можно ли использовать библиотеки, которые используют "+" под капотом — не сказано.
Иногда из-за этого получается забавный эффект — «правильно» задачку могут решить только те, кто знают одни вещи, и не знают другие. Потому что если знаешь слишком много — отвергнешь «правильное» решение.
sukhe Автор
Что ещё плохо — непонятно, что именно от тебя хотят. Проверить знание математики? Алгоритмов? Стандартной библиотеки? Встроенных возможностей языка? Или просто ищут повод, чтобы отказать чем-то не понравившемуся кандидату.
A114n
Обычно если прямо спросить — ответят «мы хотим увидеть, как вы мыслите». Но объяснить, что же именно в этом мышлении они анализируют, обычно не могут.
То есть у них самих нет готового алгоритма типа «если кандидат использовал библиотеку — то он относится к группе людей 1, если кандидат сформировал плюс из символов — то он относится к группе людей 2».
Но даже если эта группировка и есть — не факт, что она адекватная. Например, я рассказывал, как мне на собеседовании сказали, что при сложении обычных дробей гуманитарии всегда переводят в десятичные, а технари оставляют дробями.
Так что по факту это просто ИБД — имитация бурной деятельности. С тем же успехом человека можно посадить проходить тест Люшера.
mrise
Кстати да. Я вполне могу представить ситуацию, когда кандидат уже подходит, но по процессам компании собеседование всё равно нужно провести. Спрашивать элементарщину тоже вариант, но придумывать свои задачки-головоломки тупо интереснее, а слышать в 20 раз о том, как работает хеш-таблица — тупо скучнее.
wadeg
А на практике из 20 раз услышать один-два вменяемых ответа — вполне норма. А уж как работает хеш джойн — и того куда реже.
albartash
Если кандидат подходит, а собес нужно для галочки, то проблема в процессах, ведь, пока человека ищут/собеседуют, компания теряет время, а время=деньги. В конце-концов, просто сократить время собеса до 15 минут и поговорить с человеком о задачах, которые ему действительно придется решать на проекте — так и интересно, и с пользой. А все эти задачи вроде «сосчитать до бесконечности» или «сколько жирафов поместится в лондонский автобус» — это либо пустая трата времени, либо садизм по отношению к кандидату (и себе, если делается без удовольствия).
З.Ы: особенно ярко проявляется в конторах, как из этого примера.
zuek
К сожалению, даже я, практически не имеющий опыта "боевой" разработки, сталкивался с ситуациями, когда кандидату нельзя показывать боевые примеры кодинга. Безопасность через неясность — всё ещё популярный механизм. Увы.
albartash
Так код и нельзя показывать, а то «придут люди, умеющие убеждать» — злой дядя Начальник-Безопасности и неумолимая тетя NDA.
Рассказать о стэке, с которым придется работать, и задачах можно и без раскрытия всех карт. «Есть зоопарк Postgres+MySQL+Firebird» или «Ядро — легаси, писанное индусами на Коболе», ну или «система писалась в режиме стартапа, поэтому есть много неоднозначных мест, которые надо сделать однозначными. Например, стабилизировать модуль отчетов, настроить автодеплой или починить бота-заказчика картошки оптом». И без приукрашиваний, и конфиденциальность соблюдена. :)
sets
Мне кажется, в данном случае проверялось, нет ли у собеседуемого склонности к решению задач "на отвали" через максимально формальную трактовку ТЗ.
Если 5 разных способов на самом деле разные, то ок.
Если 5 способов это почти копии одного (5 вычислений через минусы со сменой знака в конце, например), то не ок.
Ну и промежуточные вариации.
WinPooh73
«Нам угрожает начинённый атомными бомбами инопланетный космический корабль. У нас есть транспортир» — Нил Стивенсон, «Анафем»
StjarnornasFred
Правильный ответ на вопрос "как?" не может быть дан при неимении ответа на вопрос "зачем?".
ole325
Задача очень даже интересная, если бы собеседовал разработчиков вероятно тоже бы задавал.
Хотя по мне интереснее слушать как бы кандидат написал алгоритм поиска решения для игры в 15ки. Еще надо понимать на какой грейд берут специалиста.
enkryptor
"Задача - удалить гланды. Условие - через рот нельзя."
albartash
Ответ выше был: «трахеотомия с помощью пвх-трубы и зажигалки».