Комментарии 78
Имхо, конечно. И разумеется это не перечеркивает некоторые плюсы питона.
А то, что это ошибка в дизайне — это да. Любой язык, которым пользуются люди несовершенен, потому что если вы будете менять язык несовместимым образом каждый раз, когда такая «бяка» будет обнаруживаться — то быстро потеряла всех разработчиков…
Надо сказать, что Python2 к Python3 всё равно поменялся несовместимым образом, так что возможность "со второго раза сделать правильно" была, но не использована.
Пример с генератором лямбд — он самый неочевидный для меня. В лямбде, значит, i
связывается по ссылке. А в выражении for i in range(5)
— по значению?! Почему тогда [[i] for i in range(3)]
возвращает [[0], [1], [2]]
, а не [[2], [2], [2]]
?
А, хотя в лямбде же лексическое связывание, а не по ссылке и не по значению.
А ещё с одной стороны — вопрос семантики и того, что в питоне тело цикла не создаёт новую область видимости.
В Julia:
function create_multipliers_1(n::Integer)
let m=[]
let itr=0:n-1
_next = iterate(itr)
while _next ≢ nothing
i = _next[1]
push!(m, x->i*x)
_next = iterate(itr, _next[2])
end
end
m
end
end
function create_multipliers_2(n::Integer)
let m=[]
let itr=0:n-1, i=nothing
_next = iterate(itr)
while _next ≢ nothing
i = _next[1]
push!(m, x->i*x)
_next = iterate(itr, _next[2])
end
end
m
end
end
foreach(m->print(m(2), " "), create_multipliers_1(5)) # 0 2 4 6 8
foreach(m->print(m(2), " "), create_multipliers_2(5)) # 8 8 8 8 8
Выражение for i in itr
в Julia семантически эквивалентно тому, как развёрнуто в первой функции.
А ещё с одной стороны — вопрос семантики и того, что в питоне тело цикла не создаёт новую область видимости.
Возможно из-за того, что у цикла может быть секция else. Могу ошибаться.
Тут, можно сказать, есть более глубокая проблема (или не совсем проблема, это кто как видит): в питоне вообще единственный способ создать вложенную область видимости — это написать функцию. Никаких там let
, фигурных скобочек или чего-то ещё. Честно — понятия не имею, создаёт это проблемы питонистам или нет. В Julia удобнее, когда каждый блок создаёт локальную область видимости, но больше с точки зрения оптимизации кода компилятором.
До меня дошло, что не так с лямбдами. Проблема больше в том, что Гвидо давным-давно не упёрся и не сказал "не бывать тут лямбдам". В итоге они есть, но со списковыми включениями неправильно работают.
Суть: включения позиционируются как замена map. Но [<expr> for x in collection]
не соответствует семантически map(lambda x: <expr>, collection)
, когда <expr>
— это лямбда! Включение захватит x
лексически, а map
— по значению.
>> type(range(3))
<class 'range'>
А при итерировании этот класс возвращает значение.
Про лямбду сами выше ответили :)
А если не список, а множество (круглые скобочки вместо квадратных), то всё будет ок.
Это не так, множество изменяемо, поэтому будет ровно та же самая проблема.
И это чуть ли не в самом начале освоения языка, который все называют очень простым и понятным, что даже гуманитарии из маркетинга осваивают его.Мне это тоже всегда казалось странным, но, практика показывает, что освоить функциональные языки, где всех этих проблем нету, для «гуманитариев из маркетинга» сложнее, чем Python.
По той же причине, по которой подавляющее большинство бухгалтеров успешно зазубривают многостраничные инструкции (что приводит меня в трепет, так как я на это неспособен), но категорически неспособны (или, возможно, не хотят) просто взять и разобраться с теми или иными простыми вещами.
Для меня, как для человека с математическим образованием, всё это выглядит, мягко скажем, дико… но вот почему-то имеено так устроен этот мир… я просто принял это к сведению, так как понять — почему он именно так устроен, я не в состоянии…
>>> def foo(bar=[]): # bar - это необязательный аргумент
# и по умолчанию равен пустому списку.
... bar.append("baz") # эта строка может стать проблемой...
... return bar
Смотрим в гугль: «append» (англ.) — «присоединять» (ну или тут python list).
Ну вроде логчно, было бы там что-то типа define, set, initialize и тп, тогда да.
Тут скорее не ошибка, а не желание новичка хотя бы мануал почитать.
Нет, ошибка не очевидна, т.к. bar является аргументом метода, а ведёт себя как локальная статическая переменная.
В документации все это описано, значит не ошибка, а проблема грамотности разработчика.
Ну вот минусующий показал свой уровень непонимания Python.
Активно минисующие и думающие, что это ошибка, хотят чтобы аргументы функций каждый раз инициализировались при каждом вызове функции. А это лишний оверхед. В Python аргументы функции инициализируются только один раз при первом запуске, все последние запуски происходят с уже инициализированными аргументами. По моему, Гвидо придумал простое и изящное решение!
Если что-то вы не понимаете в языке, это не значит что язык плох, это значит что вы не уделили достаточного времени для чтения и изучения документации языка.
Принцип наименьшего удивления никто не отменял.
Просто в большинстве других языков сделано наоборот.
Если сделано по другому, это не значит что ошибка.
Принцип наименьшего удивления никто не отменял.
Это относится к психологии и никак не к языкам программирования.
Это относится к психологии и никак не к языкам программирования.Образованность она такая…
The principle of least astonishment (POLA), also called the principle of least surprise (alternatively a «law» or «rule»)[1][2] applies to user interface and software design.[3]Линк
…
[1]The Art of Unix Programming.
Не благодари, заработал.
За что я вас должен благодарить?
Активно минисующие и думающие, что это ошибка, хотят чтобы аргументы функций каждый раз инициализировались при каждом вызове функции. А это лишний оверхед.
И поэтому мы просто будем в каждом учебнике писать "в реальных проектах никогда не ставьте значением по умолчанию изменяемый объект" и рекомендовать закат солнца вручную:
def foo(bar = None):
bar = [] if bar is None else bar
...
Вот уж воистину, просто и элегантно. А главное, добавляет в питон уникальную идиому, которой нет больше ни в одном языке.
Даже в Common Lisp, который был стандартизован в годы, когда питон только-только был представлен публике, выражение для значения по умолчанию вычисляется при каждом вызове. И это прописано в стандарте.
CL-USER> (defun foo (&optional (bar (make-array 0 :fill-pointer t)))
(vector-push-extend 'baz bar)
bar)
FOO
CL-USER> (foo)
#(BAZ)
CL-USER> (foo)
#(BAZ)
Вот уж воистину, просто и элегантно. А главное, добавляет в питон уникальную идиому, которой нет больше ни в одном языке.
А что языки программирования должны походить друг на друга? И приходить к пониманию даже для человеку ни разу не читающего руководство по языку?
Именно с мутабельными аргументами по умолчанию — можно было их вообще запретить, когда обратную совместимость ломали. Благо неизменяемых типов не так уж много, легко можно проверить.
Нет, я не против, когда в языке оставляют возможность выстрелить себе в ногу, потому что иногда надо. Но тут слишком близко к поверхности, и описание этого в руководстве не отменяет того, что это не очень хорошее решение в плане дизайна языка.
Хотя, спору нет, таких вот скользких мест на поверхности гораздо меньше, чем в Си, например, и в целом написать корректно работающую (пускай и медленную) программу не в пример проще.
Благо неизменяемых типов не так уж много, легко можно проверить.Собственно код для этой проверки уже существует, так как мутабельные типы не могут использоваться в качестве ключей.
Нет, я не против, когда в языке оставляют возможность выстрелить себе в ногу, потому что иногда надо. Но тут слишком близко к поверхности, и описание этого в руководстве не отменяет того, что это не очень хорошее решение в плане дизайна языка.
Отстреливаю себе ноги в основном те кто не читая спецификаций языка начинают программировать, а современный мир разработки уж точно не подстраивается на начинающих и джуньоров.
Дизайн языка это компромисс в многих местах. Еще почему сделали единичную инициализацию аргументов по умолчанию, это из-за такого мощного свойства языка Python как развертываний args и kwargs в аргументах функций. Если бы при каждом вызове аргументы инициализировались, то про развертывание аргументов можно было бы забыть.
Я уже не первый год слышу речи на собеседовании подобные вашей. Джунторы поголовно не читают спецификации, из мидлов половина читала. Естественно ни один из нечитающих вряд ли пройдет собеседование.
Сейчас в IT лезет много народа, который хочет только получать деньги, при этом люди совершенно не пытается стать профессионалами хотя бы в том языке на котором они пишут.
Я уже не первый год слышу речи на собеседовании подобные вашей. Джунторы поголовно не читают спецификации, из мидлов половина читала.А вам не кажется что уже этот факт является достаточным для того, чтобы сказать что «современный мир разработки именно что подстраивается под этих людей» — иначе как бы они смогли работать хотя бы джунами или, тем более, дорасти до миддлов?
Естественно ни один из нечитающих вряд ли пройдет собеседование.Ну если вашей компании важнее, чтобы человек вызубрил описание языка (а будет ли он при этом уметь что-то ещё — уже не так важно), то это ведь ваш выбор.
Того факта, что огромное количество компаний (Facebook, Google, Microsoft и прочие монстры… о мелких стартапах я уж и не говорю) этого не требут — достаточен для того, чтобы сказать подо что подстраивается «современный мир разработки».
А вам не кажется что уже этот факт является достаточным для того, чтобы сказать что «современный мир разработки именно что подстраивается под этих людей» — иначе как бы они смогли работать хотя бы джунами или, тем более, дорасти до миддлов?
В моей памяти с десяток примеров когда джун за пару лет превращался в сеньора, а через год уходил в Яндекс, IBM и т.д. А секрет из просто: отличное знание спецификации языка, паттерны, алгоритмы по Кнуту, а для вишенки от
приличный профиль на сайте типа HackerRank. И что странно, ни один из этих людей не жаловался на язык, что он работает как-то не так или что в нем ошибки.
Ну если вашей компании важнее, чтобы человек вызубрил описание языка (а будет ли он при этом уметь что-то ещё — уже не так важно), то это ведь ваш выбор.
Да, это мой выбор, так как я провожу собеседования соискателей. И я не хочу тратить время, чтобы потом вбивать людям терминологию которая принята в языке. Где я работаю, не такие богатые как Google и Microsoft, поэтому тратить деньги на проходимцев никто не будет. Нам нужны люди решающие задачи и проблемы, могущие легко объяснить суть коллеге, а не те кто себя практиковал на нестандартном мышлении.
Google и Microsoft, как бы, не всегда такими богатыми были, напоминаю. И если они смогли заработать достаточно денег для того, чтобы «тратить деньги на проходимцев» — а у вас их до сих пор недостаточно, то, может быть, проблема не в «проходимцах».
В моей памяти с десяток примеров когда джун за пару лет превращался в сеньора, а через год уходил в Яндекс, IBM и т.д.
Да, это мой выбор, так как я провожу собеседования соискателейИ то и то — липа. Время расставит нубов по местам, либо они научатся
Возьмите язык широкого применения такой как Rust или GoА что ты знаешь про Го? Он создавался как язык с низким порогом вхождения. И на этом преуспел (в т.ч.)
И у меня на питоне не получается писать код, хоть убейте. Я не понимаю, как этот язык можно назвать дружественным к новичку.Ровно по той причине, по которой он кажется странным вам. Его можно понять только разобравшись в реализации. И да, это сложно и мало кому нужно.
Но большинство людей не хотят и не любят разбираться вообще ни в чём! Они стремятся к получению набора готовых рецептов — ну и последующему выбору из них. Как эти рецепты связаны между собой — они даже не хотят задумываться.
И для вот такого подхода — Python неплохо подходит. Ну а те, кому хочется или нужно таки понимать — могут разобраться с реализацией… И у них тоже будет всё хорошо.
По той же причине «взлетел» Git, кстати. Им тоже невозможно пользоваться, не имея некоторого представления о том, как он устроен «внутри» (хотя там есть забавное читерство: есть некоторое «правдоподобное объяснение» о том, как Git устроен внутри и без него ничего понять невозможно, но оно уже давным-давно не описывает «реальный» Git… точное же описание того, что у Git'а внутри бывает нужно очень-очень редко).
Можно пояснить, что именно вы понимаете под "развёртыванием аргументов", и почему это конфликтует с инициализацией аргументов при вызове?
А то я как бы с Julia прихожу посмотреть, там есть и splat
, т.е. f(x, rest...)
принимает произвольное число аргументов, и инициализация аргументов по умолчанию при каждом вызове. Однократная инициализация, кстати, тоже ограничивает — выражение для значения по умолчанию не может использовать значения предыдущих аргументов, вроде f(x, y=sin(x)) = x + y
. Точнее, может, но только если на момент объявления функции x
будет в глобальной области видимости, и результат будет немного не тот...
Но, конечно, настоящий разработчик™ знает весь стандарт назубок и таких ошибок допускать не будет, поэтому всё нормально.
def test(*args, foo=[1]):
print(args, foo)
Т.е. сколько аргументов не передавай, foo всегда будет проинициализированна, а это тоже самое что вот такой код:
def test(*args):
foo=[1]
print(args, foo)
Т.е. какая-то не нужная бессмыслица. Поэтому сейчас выдается SyntaxError.
Ну я не знаю, Julia c этим как-то живёт, без ошибок синтаксиса и без эквивалентности первой и второй функции:
julia> test(args...; foo=[1]) = println(args, " ", foo)
test (generic function with 1 method)
julia> test()
() [1]
julia> test(42)
(42,) [1]
julia> test(42, foo=42)
(42,) 42
Но там позиционные аргументы строго отделены от именованных (;
в списке параметров). Но и в питоне можно понимать первую запись как то, что все аргументы после *args
могут быть только именованными — вполне логичная семантика, как по мне.
Но фиг с ним, собственно, нельзя так нельзя. Почему нельзя для значения по умолчанию проверить тип и точно так же сказать "нельзя", если он оказался мутабельным? Ведь даже в документации признаётся, что это, скорее всего, не то, что программист хотел сказать. Чтобы, если уж хочется стрелять себе в ногу — это нужно было явно задекларировать:
_default_bar_in_foo = []
def foo(bar=None):
bar = _default_bar_in_foo if bar is None else bar
bar.append("baz")
return bar
И, как я писал, — конечно, сейчас уже поздно это менять. Но Python3 всё равно ломал обратную совместимость, чёрт подери! Можно было тогда и сделать.
В документации все это описано, значит не ошибкаОт создателей «это не баг, это фича».
На сколько я понимаю bar=[] создается во время импорта модуля с этой функцией и соотвественном выделение памяти под bar происходит 1 раз, нужно ли для «условной очевидности» выделять память каждый раз при вызове foo с дефолтными аргументами?
Да в Питоне, вроде бы, выделение памяти не считается за криминал. Сборщик мусора же чистит эту память, если она больше не используется.
Еще причина почему сделали инициализацию при первом проходе, это из-за того что декоратором можно просто поменять поведение функции, даже сменить ее сигнатуру. При постоянной инициализации аргументов по умолчанию изменить поведение было бы проблематично. Пришлось бы тогда при каждом вызове вызывать декоратор, а не то что он возвращает, а возвращает он измененную функцию.
Народ во сидит здесь и удивляется, что это ошибка, поверхностно судя и не вникая в суть почему это так работает.
При постоянной инициализации аргументов по умолчанию изменить поведение было бы проблематичноПочему?
Пришлось бы тогда при каждом вызове вызывать декоратор,Зачем?
а не то что он возвращает, а возвращает он измененную функцию.Ну и чем существование лямбды, инициализующей опциональный аргумент в случае необходимости, ему бы помешало?
Народ во сидит здесь и удивляется, что это ошибка, поверхностно судя и не вникая в суть почему это так работает.А зачем «вникать в суть» неудобного и небезопасного поведения? Ну вот возьмите Ситроен с опцией торможения пассажиром. Почему вместо того, чтобы «вникнуть в суть того, как это работает» его отозвали? Ну потому что неудобно и небезопасно, блин.
Однако в случае с Питоном, почему-то, вместо того, чтобы назвать вещи своими именами баг пытаются объявить фичей и всех людей, которым это не нравится записывают в ретрограды.
Почему LISP полвека назад или C++ четверть века назад это делали ожидаемым и естественным способом, а Python — не может?
Ну Ok, совместимость, к этому уже все привыкли… могу понять — достаточно веская причина.
Но вот рассказы про декораторы и прочее — оставьте для людей, не умеющих в программирование. Люди, понимающие как это всё устроено как раз легко могут вам предложить несколько вариантов, которые могли бы проблему решить. Вопрос тут не во внутреннем устройстве а как раз исключительно «в человеческом факторе»: неясно — стоит ли исправление этой проблемы на третьем десятке жизни языка возможного хаоса или нет. Всё. Никакиз других веских причин для этого нет.
Почему?
Потому что декоратор изменяет функцию один раз.
Зачем?
Чтобы проинициализировать аргументы.
Однако в случае с Питоном, почему-то, вместо того, чтобы назвать вещи своими именами баг пытаются объявить фичей и всех людей, которым это не нравится записывают в ретрограды.
Вы прочитайте про передачу аргументов через * или **, возможно тогда поймете почему так, а не иначе. Странное желание видеть все языки одинаковыми. И пожалуйста, не начинайте демагогию, если вы совершенно не разбираетесь в Python.
Вы прочитайте про передачу аргументов через * или **, возможно тогда поймете почему так, а не иначе.Какое имеет отношение передача аргументов через * и ** к их инициализации?
Странное желание видеть все языки одинаковыми.Не одинаковыми. Логичными. То что одинаково выглядящие выражения «a = []», записанные в заголовке функции и в её теле работают по разному — ну вот ни разу не логично. И некрасиво. Да, можно пытаться это объяснять соображениями эффективности… (хотя «эффективность» и «питон» в одном предложении уже «делают мне смешно»), но вот никаких неразрешимых проблем с передачей параметров или декораторами отказ от этой «фичи» не вызывает.
И пожалуйста, не начинайте демагогию, если вы совершенно не разбираетесь в Python.Ну демагогию тут разводите вы, пытаясь «натянуть сову на глобус» и доказать, что этот дурдом в Python появился не из-за того, что так было проще сделать, а по каким-то глубинным соображениям.
Да, действительно, Python так устроен, что он вначале вычисляет все аргументы, а потом только передаёт этот список в декораторы и делает другие манипуляции. Ок, пусть будет так. Однако… метод «откладывания» вычислений всем, как бы, хорошо известен: лямбда. Храните в этом списке не готовые аргументы, а лямбды, которые эти аргументы вычислят в нужный момент — и всё. Вот совсем всё: декораторы вам больше не нужно вызывать 100500 раз, и никаких трюков с * или ** вам тоже не нужно. Эффективности не хватает? Ну, введите «псевдолямбду» в реализации, одна проверка одного бита для языка, который и так, при вызове функций, выполняет сотни машинных инструкций — это немного.
Ну или просто запретите модифицируемые типы данных использовать в качестве аргументов по умолчанию — запрещены же они в качестве ключей? Для этого вообще ничего менять не нужно.
На крайний случай хотя бы честно напишите в документации «по историческим причинам есть такая бяка» (собственно сейчас так и сделано).
Только не нужно обосновывать свои недоработки «глубинными материями» — глупо выглядит.
В Лиспе тоже есть, конечно, некоторые заморочки:
CL-USER> (defun foo (&optional (bar '(spam)))
(nconc bar '(spam))
bar)
; WARNING:
; Destructive function NCONC called on constant data: (SPAM)
; See also:
; The ANSI Standard, Special Operator QUOTE
; The ANSI Standard, Section 3.2.2.3
;
; compilation unit finished
; caught 1 WARNING condition
FOO
CL-USER> (foo)
(SPAM SPAM)
CL-USER> (foo)
; бесконечный цикл
Но SBCL вот сразу ругается и отправляет читать стандарт, согласно которому литералы в аргументах по умолчанию привязываются по ссылке, а не вычисляются заново во время вызова.
Правильно будет везде вместо литералов писать явный вызов функции:
CL-USER> (defun oof (&optional (bar (list 'spam)))
(nconc bar (list 'spam))
bar)
OOF
CL-USER> (oof)
(SPAM SPAM)
CL-USER> (oof)
(SPAM SPAM)
Если оставить вызов в аргументе функции, но оставить литерал в теле, то тоже будут весёлые побочные эффекты:
CL-USER> (defun ofo (&optional (bar (list 'spam)))
(nconc bar '(spam))
bar)
OOF
CL-USER> (ofo)
(SPAM SPAM)
CL-USER> (ofo)
(SPAM SPAM) ; всё хорошо? как бы не так!
CL-USER> (let ((spam (ofo)))
(setf (nth 1 spam) 'ham)
spam)
(SPAM HAM)
CL-USER> (ofo)
(SPAM HAM)
Ещё, говорят, реализация вольна внутри одного определения функции два одинаковых литерала по одной ссылке связывать, но в SBCL я такого не видел.
Советую ознакомиться, что он делает с аргументами функции.
Ну и как бы оценить, типичный подход, подход в Питоне и подход в Расте.
Решение этой распространенной проблемы с Python будет таким:
def create_multipliers(): return [lambda x, i=i : i * x for i in range(5)]
Фи, второй параметр добавлять. Правильное решение — это
def create_multipliers():
return [(lambda k: lambda x: k * x)(i) for i in range(5)]
И то, что в данный конкретный момент каждый конкретный ты-я-они-все помнят про вот такое и вот эдакое поведение — вовсе не значит, что оно будет помнится и завтра, когда случатся магнитная буря, телефонный спам от банков, пробки по дороге и перемена настроения маркетинга «теперь нам нужно вот так».
>>> def foo(bar=None):
… if bar is None: # or if not bar:
… bar = []
… bar.append(«baz»)
… return bar
Как новичку в Питоне, этот пример был наименее понятным, более того, не смог врубиться даже переспав с этой мыслью.
При первом вхождении должно быть всё отлично, однако же, было бы великолепно, если бы кто-нибудь из присутствующих смог мне пояснить, почему при последующих вхождениях в функцию выходит, что переменная bar снова становится None?
Ведь будучи пустым массивом, переменная хранила своё состояние при выходе из функции, а будучи None, превращённым в массив, вдруг стала его терять.
Потому что в Питоне происходит не присваивание, а связывание, а типы данных бывают изменяемые и неизменяемые. "Значением" изменяемого типа является ссылка на данные, а неизменяемого — сами данные.
Расшифровка определения функции:
- При создании: Определить функцию
foo
от одного аргументаbar
; вычислить выражение справа отbar=
(вычисляется вNone
) и, если при вызовеbar
не дан явным аргументом, связатьbar
с получившимся объектом. - При вызове: если
bar
связано с объектомNone
, создать пустой список и связатьbar
с этим списком.
Кстати, # or if not bar
— неверный комментарий, т.к. not []
вычисляется в True
— т.е. если вместо if bar is None
будет if not bar
— то при вызове foo()
на пустом списке "baz" добавится в новый список, а не в уже имеющийся.
Потому что в Питоне происходит не присваивание, а связывание, а типы данных бывают изменяемые и неизменяемые.
Поэтому в нем нет переменных, а есть индентификаторы. Хотя многие по пивычке называют их переменными, хотя это неправильно. На собеседованиях на сеньора это спрашивают, чтобы понять владеет ли собеседуемый терминологией Python.
На собеседованиях на сеньора это спрашивают, чтобы понять владеет ли собеседуемый терминологией Python.А не подскажите — на собеседованиях в какую именно компанию такие вещи спрашивать принято?
Ну просто чтобы чтобы не тратить время на общение с ней: последнее чего я хотел бы на работе — так это общаться с людьми, которые, вместо того, чтобы обсудлать эффктивность решения или его расширяемость — будут придираться к терминам и заниматься другими видами bikeshedding'а.
Ну тогда понятно почему от вас, по вашим же словам, через два-три года люди уходят ибо вы не можете обеспечить им достойную зарплату… продолжайте в том же духе!
>>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all
Зачем здесь срез [:]. Почему бы в данном примере не сделать просто numbers =?
Потому что здесь происходит замена данных, а не присваивание (связывание). Рассмотрим пример с обычным присваиванием:
numbers = [1, 2, 3]
other_number = numbers
numbers = [4, 5, 6]
# В данной ситуации, значение `other_numbers` не меняется, т.к. эта переменная продолжает ссылаться на область в памяти, к которой она была привязана изначально
print(other_numbers)
>>> [1, 2, 3]
Теперь с [:] = [...]
:
numbers = [1, 2, 3]
other_number = numbers
numbers[:] = [4, 5, 6]
# Значение `other_numbers` меняется, т.к. обе переменные ссылаются на одну и ту же область памяти
print(other_numbers)
>>> [4, 5, 6]
Глючный код на Python: 10 самых распространенных ошибок, которые допускают разработчики