Далее поделюсь с вами моей коллекцией самых неожиданных, забавных и всё-таки валидных «заклинаний» программирования. По сути, использование этих особенностей поведения ЯП считается пагубным, поскольку ваш код никоим образом не должен быть непредсказуемым. Хорошо, что многие линтеры уже осведомлены и готовы посмеяться над вами, если попробуете какое-то из перечисленных дурачеств. Но как говорится, знание — сила, так что начнём.
Вражеское переназначение True в Python 2
Рифмуется с true, так что вы знаете, что это poo («какашка»).
>>> True = False
>>> True
False
К счастью, такой код выводит
SyntaxError
в версии Python 3, поскольку True, False и None теперь стали зарезервированными словами. Такая шалость всё-таки далека от подлости в C++, когда вы вставляете #define true false
в стандартный заголовочный файл на рабочей машине коллеги.Призрачное взаимодействие с объектом в Java и Python
Семантика
==
часто озадачивает начинающих Java-программистов, но ещё более усложняет ситуацию непостоянство оператора даже в тривиальных ситуациях, пусть это и сделано для производительности.Integer a = 100;
Integer b = 100;
System.out.print(a == b); // prints true
Integer c = 200;
Integer d = 200;
System.out.print(c == d); // prints false
JVM использует однотипный справочник для значений в диапазоне
[-128, 127]
. Что ещё более странно, так это соответствующее поведение Python.>>> x = 256
>>> y = 256
>>> x is y
True
>>> x = 257
>>> y = 257
>>> x is y
False
Пока ничего слишком удивительного.
>>> x = -5
>>> y = -5
>>> x is y
True
>>> x = -6
>>> y = -6
>>> x is y
False
Похоже, нижний предел для интерпретатора Python такой же…
-5
. Целые числа в диапазоне [-5, 256]
получают одинаковые ID. Но всё равно это работает как-то странно.>>> x = -10
>>> y = -10
>>> x is y
False
>>> x, y = [-10, -10]
>>> x is y
True
Видимо, применение деструктурирующего присваивания сразу меняет правила. Я не уверен, почему так происходит, и даже задал вопрос на Stack Overflow в попытке разобраться. Может быть, повторяющиеся значения в списке указывают на тот же объект для экономии памяти.
Обратная запись с индексом в C
Обратная запись с индексом мгновенно доставляет головную боль любому разработчику.
int x[1] = { 0xdeadbeef };
printf("%x\n", 0[x]); // prints deadbeef
Причина работы такого кода в том, что
array[index]
на самом деле просто синтаксический сахар для *(array + index)
. Благодаря коммутативному свойству сложения можно поменять их местами и получить тот же результат.Оператор «перехода» в C
На первый взгляд оператор
-->
выглядит как синтаксическая ошибка. Но когда вы понимаете, что он нормально компилируется, то начинаете думать, что это недокументированная функция языка. К счастью, это ни то, ни другое.for (x = 3; x --> 0;) {
printf("%d ", x); // prints 2 1 0
}
«Оператор»
-->
— это на самом деле два оператора, которые в этом контексте разбираются как (x--) > 0
. Известно, что такая штука вызывает немалую путаницу при использовании в продакшне — чистое зло.Оператор sizeof
в C
Оператор
sizeof
обрабатывается в процессе компиляции, что даёт ему интересные свойства.int x = 0;
sizeof(x += 1);
if (x == 0) {
printf("wtf?"); // this will be printed
}
Поскольку объекты оператора
sizeof
анализируются в процессе компиляции, то выражение (x += 1)
никогда не будет запущено. Также любопытно: исследования показывают, что printf("wtf?")
— самая популярная строчка кода, которая никогда не поступает в продакшн.Начало индексов с единицы в Lua, Smalltalk, MATLAB и др…
На форумах /r/programminghumor полно мемов об «индексах, которые начинаются с единицы». Поразительно, но немало языков программирования в реальности используют 1-индексированные массивы. Более полный список см. здесь.
0 соответствует true
в Ruby
…и только в Ruby. *
if 0 then print 'thanks, ruby' end # prints thanks, ruby
* правка: В обсуждении Reddit мне указали, что такое справедливо также для Lua, Lisp и Erlang.
Триграфы, диграфы и токены в C
По историческим причинам в C остались альтернативные варианты написания для нецифробуквенных символов.
Триграф | Символ | Диграф | Символ | Токен | Символ | ||
---|---|---|---|---|---|---|---|
??= |
# |
<: |
[ |
%:%: |
## |
||
??/ |
\ |
:> |
] |
compl |
~ |
||
??' |
^ |
<% |
{ |
not |
! |
||
??( |
[ |
%> |
} |
bitand |
& |
||
??) |
] |
%: |
# |
bitor |
| |
||
??! |
| |
and |
&& |
||||
??< |
{ |
or |
|| |
||||
??> |
} |
xor |
^ |
||||
??- |
~ |
and_eq |
&= |
||||
or_eq |
|= |
||||||
xor_eq |
^= |
||||||
not_eq |
!= |
if (true and true) { // same as if (true && true)
printf("thanks, c");
}
Некоторое чужеродное оборудование вроде IBM 3270 не позволяло набрать некоторые часто используемые символы в C/C++, так что ввели использование диагрфы, триграфы и токены, чтобы сохранить совместимость с определёнными кодировками.
Надеюсь, статья была интересной. Можете почитать обсуждение на Reddit.
Комментарии (56)
Koyanisqatsi
27.12.2017 11:54-2Оператор sizeof обрабатывается в процессе компиляции, что даёт ему интересные свойства
Вы в примере неправильно применили sizeof. Он нужен для переносимости программ с одной машины на другую, т.к. не везде количество байт в каком-то типе переменной одинаково. Какой смысл вызывать sizeof(i++), если переменная i будет иметь всегда одно и тоже количество байт?iig
27.12.2017 17:14sizeof нужен, чтобы узнать количество байт. И его можно использовать с разными целями. В том числе и для реализации переносимости между разными архитектурами, и для стрельбы в ногу.
Koyanisqatsi
27.12.2017 18:23Ну вы сказали то же самое, что и я. Не пойму где я не прав.
И его можно использовать с разными целями.
У него одна цель — сколько байт занимает тот или иной тип данных. Зачем в него пихать выражение?iig
27.12.2017 18:57Он нужен для переносимости программ с одной машины на другую
Его можно использовать в том числе и для этого.
Зачем в него пихать выражение?
sizeof(str)-1
sizeof(str-1)
Бывают и просто очипятки.
Aeroapplabs
27.12.2017 11:54По поводу === в JS: он выглядит как синтаксическая ошибка и вводит в ступор начинающих программистов. === означает сравнивание не только по значению, а по типам.
Zibx
27.12.2017 14:24Наоборот. === проще — это просто сравнение. Т.е. сравнение по ссылки, а в случае примитивов — по значению. А вот == — приведение к общему типу и последующее сравнение. Приведение осуществляется методами toString или valueOf.
fireSparrow
27.12.2017 12:21+1В питоне «is» и не предназначен для сравнения значения переменных, для этой цели служит вполне стандартное "=="
А «is» проверяет, указывают ли две переменный на один и тот же объект в памяти. Это достаточно специальная операция, и её нужно применять только в тех редких случаях, когда вы чётко понимаете, зачем вам нужна именно такая проверка, а не простая проверка на равенство значений.
Единственный вариант, когда «is» можно использовать просто так — это для сравнения с None.
Dim0v
27.12.2017 12:43+1Касательно Python.
>>> a = [-10, -10] >>> a[0] is a[1] True >>> b = [-10] >>> b.append(-10) >>> b[0] is b[1] False >>> b[0] is a[0] False
Причина — в оптимизациях. В интерактивном режиме единицей трансляции является строка. Соответственно, интерпретатор выполняет (и оптимизирует) каждую строку независимо от других. Если в этой одной строчке несколько раз используется одна и та же константа, то оптимизатор способен это заметить и создать единственный объект этой константы под нужды всей строки. Если же одна и та же константа используется в нескольких строках, то интерпретатор уже не в силах оптимизировать такой вариант.
К слову, если код выше поместить в файл и выполнить его, то вывод станет
True True True
Потому что в этом случае единицей трансляции будет уже весь файл и оптимизатор сможет использовать один объект для константы "-10" в рамках всего файла, а не только строки.
alix_ginger
27.12.2017 15:45В любом случае, сложно придумать кейс, при котором будет иметь значение, какой результат возвращает оператор is, примененный к двум примитивам.
Dim0v
27.12.2017 16:52Конечно, с этим сложно поспорить. Просто немного прояснил природу явления для тех, кому интересно.
Yngvie
27.12.2017 17:57Видимо это применимо к Python2. Там если записать в одну строку, то результат поменяется
>>> b = [-10]; b.append(-10); b[0] is b[1] True
Но в Python 3 эта же строка вернет False. Более того, даже для создания списка сразу из двух элементов будет False
>>> b = [-10, -10] >>> b[0] is b[1] False
Даже если положить это в файл.
Dim0v
27.12.2017 19:37На Python 3 «-10» воспринимается не как отдельная константа, а как операция унарного минуса, применённая к константе 10. Замените «-10» на 500 и поведение в третьем питоне станет аналогичным поведению во втором.
rraderio
27.12.2017 12:55Начало индексов с единицы в Lua, Smalltalk, MATLAB и др…
А почему это плохо? Первый элемент — первый индекс.Zibx
27.12.2017 14:27В языках которые умеют манипулировать указателями — очень удобно вычислять адрес в памяти домножая индекс на размер элемента + смещение. В языках где нет указателей — это не важно, но у истинных разработчиков которые начинали с низкоуровневых языков резонно бомбит от такой нумерации.
Akdmeh
27.12.2017 12:56Ни одной шутки про PHP.
Со спецификации языка (то есть, документированное поведение, не баг!), которое мне на днях попортило нервы как наследие от предыдущего программиста:
<?php count(false) == 1 //true
cher11
27.12.2017 14:00Ну так а зачем пытаться считать количество элементов в false?
тут все логично.count(null) // 0 count([]) // 0
Akdmeh
27.12.2017 14:02Функция может возвращать [], если количество результатов равно нолю или false, если возникла ошибка при запросе.
Да, логичнее бросать Exception, но о них не все PHP-программисты знают.
ivan386
27.12.2017 12:59Недавно узнал что можно делать так:
BOOL CTigerTree::IsZeroBlock(uint32 nBlock) const { static const uint64 ZeroHash[37][3] = { ... }; ... CTigerNode* pBase = m_pNode + m_nNodeCount - m_nNodeBase + nBlock; return memcmp( ZeroHash[ m_nActualHeight - m_nHeight ], pBase->value, sizeof( pBase->value ) ) == 0;
Если не указывать второй индекс то возвращается адрес.
devalone
27.12.2017 15:27>>> x = -5 >>> y = -5 >>> x is y True >>> x = -6 >>> y = -6 >>> x is y False
А что странного? В документации указано, что is проверяет, одинаковые ли это объекты(не значения), никто не гарантирует, какие будут одинаковыми, а какие нет. Лучше было бы показать неочевидное поведение параметров по умолчанию в функции, например:
>>> def append_to_array(value, array=[]): ... array.append(value) ... return array >>> print(append_to_array(10)) [10] >>> print(append_to_array(11)) [10, 11] >>> print(append_to_array(12))
Это происходит потому что параметры по умолчанию создаются во время объявления функции(например при импорте модуля) и array всегда указывает на один и тот же массив.alix_ginger
27.12.2017 15:43Вот это на самом деле странно. Согласно заголовку функции, значение array по умолчанию — пустой массив, а во втором случае функция вызывается с параметрами (11, [10]) вместо (11, []) как должно быть
fireSparrow
27.12.2017 16:19Согласно заголовку функции, значение по умолчанию — вовсе не пустой массив, а вот этот вот конкретный массив, который только на данный момент пуст, но может измениться в будущем.
Но вы правы, это действительно один из самых неочевидных нюансов питона, на котором спотыкаются многие начинающие питонисты.
Dim0v
27.12.2017 17:39Параметры по умолчанию инициализируются один раз в момент инициализации функции. Поэтому с мутабельными объектами (вроде списков) в качестве дефолтных параметров и получается вот такая беда. И как уже выше написали, у многих начинающих с этим проблемы, т.к. поведение действительно очень неочевидное, если знаком с питоном поверхностно.
Собственно, решение простое — использовать immutable значения в качестве параметров по умолчанию. Как правило используют None с последующей инициализацией в теле функции:
def my_func(my_arg=None): if my_arg is None: my_arg = [] ...
В некоторых случаях также можно использовать пустой кортеж, он во многом аналогичен пустому списку. Но тут уже зависит от самой функции.
Gryphon88
27.12.2017 15:35Кто-то еще пишет сишный код с триграфами? Кстати, даже с ними нельзя конструировать строку дефайна в макросе.
olegy
27.12.2017 16:21Когда то нарвался в С когда переменные назывались 'and' и 'or': не мог понять почему выпадают ошибки
fireSparrow
27.12.2017 16:25Кстати, раз уж речь зашла о логических переменных в питоне, то можно было бы вспомнить и эту хохму:
False ** False == True # Результат этого сравнения - True
Alcor
27.12.2017 17:06А почему должно быть иначе?
0^0 = 1fireSparrow
27.12.2017 17:22Для тех, кто уже знает, что bool наследуется от int, всё действительно очевидно. А вот остальным это вполне может взорвать мозг.
Dim0v
27.12.2017 17:42Ну так-то, 0^0 — это неопределенность, а не единица. Так что в любом случае сложно назвать это очевидным.
Хотя если знать, что с интами питон ловко возвращает 1 для 0**0, вместо выбрасывания ошибки, то действительно все встает на свои места)fireSparrow
27.12.2017 17:55А это как раз уже не особенность питона, в математике в ряде случаев принимают 0^0 за единицу. Так же, как и факториал нуля, который строго говоря, тоже должен быть неопределён.
Dim0v
27.12.2017 19:47В математике единице равен предел x^x при x стремящемся к нулю справа. А именно 0^0 — это неопределенность, как не крути. И то, что ее принимают за единицу — это именно особенность языка. И факториал нуля тут совершенно не к месту. Он равен единице как «empty product» (хз, есть ли устоявшийся перевод на русский) https://en.m.wikipedia.org/wiki/Empty_product.
fireSparrow
27.12.2017 19:59Тогда уж посмотрите и эту статью:
en.wikipedia.org/wiki/Zero_to_the_power_of_zero
Вы не поверите, но там тоже упоминается empty product.
И, кстати, устоявшийся перевод есть — «пустое произведение»
mayorovp
28.12.2017 12:37Принимать 00 за единицу рекомендовал еще как минимум Кнут. Потому что с таким определением многие формулы упрощаются лишаясь особых случаев.
Например, известная формула (x+y)n = ?k Ckn xk yn-k была бы неприменима при x=0 или y=0 если бы 00 было бы определено как-то кроме 1.
Rsa97
27.12.2017 17:31Erlang:
> if 0 -> true; true -> false end. false
true и false в Erlang — это отдельные атомы, ни с нулём, ни с единицей, ни с каким-либо другим числом не связанные.
Yngvie
27.12.2017 18:13А мне в Python нравился трюк с изменением tuple
>>> t = ([1, 2], [3, 4]) >>> t[0].append(10) >>> t # сработало, кортеж содержит тот же список, но в нем новый елемент ([1, 2, 10], [3, 4]) >>> t[0] = t[0] + [20] # TypeError: 'tuple' object does not support item assignment >>> t[0] += [30] # Сокращенная запись, тот же TypeError, но... >>> t ([1, 2, 10, 30], [3, 4])
И пока я искал этот кусочек кода наткнулся на wtfpython @ Github с подборкой таких моментов. Кажется на Хабре даже был перевод той статьи. Вот с chained operations по ссылке мне понравился
fireSparrow
27.12.2017 23:34А в чём трюк-то?
В строчке, где происходит ошибка, вы говорите питону «Запиши в нулевой элемент тупла результат выражения». Естественно питон отказывается, потому что в тупл нельзя ничего записывать.
В следующей строчке вы говорите питону «Возьми нулевой элемент тупла, и проделай с ним вот такую манипуляцию». И здесь всё нормально.Yngvie
28.12.2017 00:02Да я знаю что происходит. Интересно тут выглядит третья попытка, когда вылетает ошибка, но при этом у нас все равно желаемый результат.
Потому как
list.__iadd__
не создает новый список, в отличии отlist.__add__
. Так что + в+=
отрабатывает, а вот = выдает ошибку.
Может слово трюк не подходит, но "wtf tuple изменился" реакцию у некоторых вызывает
Hazactam
27.12.2017 21:59Хватает в языках весёлухи:
Неопределённое поведение
В качестве еще одного примера неопределенного поведения можно привести код:
int i = 5;
i = ++i + ++i;
При его выполнении переменная i может принять значения 13 или 14 для C/C++, 13 для Java, PHP и C#, 12 при реализации на LISP.
Hazactam
27.12.2017 22:09Ну и лурк можно вспомнить :)
lurkmore.to/++i_+_++i
У меня основной рабочий язык — Delphi, там подобных чудес, к счастью, минимум. За что и нравится, среди прочего.
master1312
28.12.2017 12:29Питон мне вообще мозг вынес, учитывая то, что еще в комментах понаписали.
0 соответствует true в Ruby
Еще в линуксовых шеллах во всех.mayorovp
28.12.2017 12:42Ну, в шеллах-то все логично. Там 0 соответствует выполнению без ошибки (true), а любое другое число — выполнению с ошибкой (false).
Это так не только в линуксовых шеллах, кстати. Виндовый cmd.exe тоже так считает…master1312
28.12.2017 13:22А в Ruby и прочих зачем так сделано? Там же наверное тоже какая-то логика была.
ivan386
28.12.2017 16:34Ну в Lua например всё просто. Есть false и nil остальное всё воспринимается как true. При этом false не равен nil. Nil это отсутсвие значения.
master1312
28.12.2017 12:58sizeof(x+=1);
Кстати, компилятор люто паникует.
предупреждение: statement has no effect [-Wunused-value]
sizeof(x+=1);
Так что, не забывайте посматривать на ворнинги компилятора.
aragaer
28.12.2017 16:03В том С, который есть у меня под рукой (gcc 5.4.0) нет диграфов и «слов» типа bitand — только что проверил. Но оно есть в С++. В С только триграфы.
Foxeed
29.12.2017 11:14Начиная с gcc 5.1 и clang 3.1 триграфов уже нет, но диграфы еще остались. Забавно, но IBM использует триграфы (Appendix A).
mayorovp
А в языке Perl существует "истинный ноль" — как число он равен 0, но как булево значение он равен истине. Кажется, он записывается как "0 but true".
Idot
А чему во всех этих языках равен False?
deril
В ruby false это nil(аналог null) и false. Всё остальное true.