HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-02-21
1 <p><a>#статьи</a></p>
1 <p><a>#статьи</a></p>
2 <ul><li>20 сен 2021</li>
2 <ul><li>20 сен 2021</li>
3 <li>0</li>
3 <li>0</li>
4 </ul><h2>Придумают же! Самые крутые фишки языков программирования</h2>
4 </ul><h2>Придумают же! Самые крутые фишки языков программирования</h2>
5 <p>Всё самое интересное: от расширений до постепенной типизации.</p>
5 <p>Всё самое интересное: от расширений до постепенной типизации.</p>
6 <p>Альберто Блинчиков для Skillbox Media</p>
6 <p>Альберто Блинчиков для Skillbox Media</p>
7 <p>Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.</p>
7 <p>Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.</p>
8 <p><strong><strong>об авторе</strong></strong></p>
8 <p><strong><strong>об авторе</strong></strong></p>
9 <p>Программист-энтузиаст. Получает докторскую степень в области инженерных наук, хочет стать преподавателем. Любит писать о коде. Сайт автора:<a>The Renegade Coder</a>.</p>
9 <p>Программист-энтузиаст. Получает докторскую степень в области инженерных наук, хочет стать преподавателем. Любит писать о коде. Сайт автора:<a>The Renegade Coder</a>.</p>
10 <p>Одна из самых прикольных фич, которые я для себя открыл, - это расширения. Впервые я столкнулся с ними, когда кодил<a>Hello World на Kotlin</a>. Конечно, в такой простой программе они не пригодились, зато я узнал, что они существуют.</p>
10 <p>Одна из самых прикольных фич, которые я для себя открыл, - это расширения. Впервые я столкнулся с ними, когда кодил<a>Hello World на Kotlin</a>. Конечно, в такой простой программе они не пригодились, зато я узнал, что они существуют.</p>
11 <p>Расширения позволяют добавлять новые методы к существующим классам, не расширяя сами классы.</p>
11 <p>Расширения позволяют добавлять новые методы к существующим классам, не расширяя сами классы.</p>
12 <p>Представьте, что нам очень нравится класс String в Java, но мы хотим сделать его ещё лучше, добавив новый метод. Единственный способ сделать это - создать свой класс, который расширяет String:</p>
12 <p>Представьте, что нам очень нравится класс String в Java, но мы хотим сделать его ещё лучше, добавив новый метод. Единственный способ сделать это - создать свой класс, который расширяет String:</p>
13 public class StringPlusMutation extends String { public String mutate() { // добавляем код для изменений } }<p><strong>Примечание переводчика</strong></p>
13 public class StringPlusMutation extends String { public String mutate() { // добавляем код для изменений } }<p><strong>Примечание переводчика</strong></p>
14 <p>Создать такой класс не получится, потому что String в Java - финальный класс, его нельзя расширять.</p>
14 <p>Создать такой класс не получится, потому что String в Java - финальный класс, его нельзя расширять.</p>
15 <p>Между тем в Kotlin достаточно написать метод, который напрямую расширяет класс String:</p>
15 <p>Между тем в Kotlin достаточно написать метод, который напрямую расширяет класс String:</p>
16 fun String.mutate(){ // добавляем код для изменений }<p>Теперь каждый раз, когда мы создаём экземпляр String, мы можем обратиться к его методу mutate, как будто это обычный публичный метод класса String.</p>
16 fun String.mutate(){ // добавляем код для изменений }<p>Теперь каждый раз, когда мы создаём экземпляр String, мы можем обратиться к его методу mutate, как будто это обычный публичный метод класса String.</p>
17 <p>Конечно, у расширений есть и недостатки. Представьте, что произойдёт, если к стандартным методам класса String добавят метод с тем же именем mutate. Наверняка в вашей программе возникнет какая-нибудь хитрая ошибка. Но не думаю, что такие совпадения случаются часто.</p>
17 <p>Конечно, у расширений есть и недостатки. Представьте, что произойдёт, если к стандартным методам класса String добавят метод с тем же именем mutate. Наверняка в вашей программе возникнет какая-нибудь хитрая ошибка. Но не думаю, что такие совпадения случаются часто.</p>
18 <p>Так или иначе, я не придумал ничего лучше, чем использовать расширения для быстрого прототипирования. Расскажите мне, если у вас есть идеи получше.</p>
18 <p>Так или иначе, я не придумал ничего лучше, чем использовать расширения для быстрого прототипирования. Расскажите мне, если у вас есть идеи получше.</p>
19 <p>Другая классная фишка языков программирования - макросы. Я столкнулся с ними, когда писал<a>Hello World на Rust</a>. Ведь вывод в консоль на Rust реализован именно в виде макроса.</p>
19 <p>Другая классная фишка языков программирования - макросы. Я столкнулся с ними, когда писал<a>Hello World на Rust</a>. Ведь вывод в консоль на Rust реализован именно в виде макроса.</p>
20 <p>Вообще, макросы - это понятие из области метапрограммирования. Они позволяют расширять язык напрямую - добавляя правила к <a>дереву абстрактного синтаксиса</a>. Такие правила создаются с помощью сопоставления с образцом (pattern matching).</p>
20 <p>Вообще, макросы - это понятие из области метапрограммирования. Они позволяют расширять язык напрямую - добавляя правила к <a>дереву абстрактного синтаксиса</a>. Такие правила создаются с помощью сопоставления с образцом (pattern matching).</p>
21 <p>Объяснение получилось довольно мутным - поэтому рассмотрим пример на Rust:</p>
21 <p>Объяснение получилось довольно мутным - поэтому рассмотрим пример на Rust:</p>
22 macro_rules! print { ($($arg:tt)*) =&gt; ($crate::io::_print(format_args!($($arg)*))); }<p>Этот код взят из <a>исходников самого Rust</a>. Как видите, в макросе print используется одно сопоставление с образцом, которое принимает любое число аргументов и пытается перед выводом привести их к подходящему виду.</p>
22 macro_rules! print { ($($arg:tt)*) =&gt; ($crate::io::_print(format_args!($($arg)*))); }<p>Этот код взят из <a>исходников самого Rust</a>. Как видите, в макросе print используется одно сопоставление с образцом, которое принимает любое число аргументов и пытается перед выводом привести их к подходящему виду.</p>
23 <p>Если сопоставление с образцом даётся вам трудновато, возможно, стоит получше изучить<a>регулярные выражения</a> - у них похожий синтаксис.</p>
23 <p>Если сопоставление с образцом даётся вам трудновато, возможно, стоит получше изучить<a>регулярные выражения</a> - у них похожий синтаксис.</p>
24 <p>Вы наверняка заметили, что работать с макросами не так-то просто. Их трудно писать и так же сложно отлаживать. В документации по Rust макросы и вовсе называют<a><strong>крайним средством</strong></a>, использовать которое можно редко и только по рецепту :)</p>
24 <p>Вы наверняка заметили, что работать с макросами не так-то просто. Их трудно писать и так же сложно отлаживать. В документации по Rust макросы и вовсе называют<a><strong>крайним средством</strong></a>, использовать которое можно редко и только по рецепту :)</p>
25 <p>Я узнал о них, когда разбирался с C# (см. статью "<a>Hello World на C#</a>").</p>
25 <p>Я узнал о них, когда разбирался с C# (см. статью "<a>Hello World на C#</a>").</p>
26 <p>Автоматические свойства (automatic properties) - это, по сути, сокращения для<strong>геттеров</strong>и <strong>сеттеров</strong>в объектно-ориентированных языках, то есть синтаксический сахар.</p>
26 <p>Автоматические свойства (automatic properties) - это, по сути, сокращения для<strong>геттеров</strong>и <strong>сеттеров</strong>в объектно-ориентированных языках, то есть синтаксический сахар.</p>
27 <p>Допустим, у нас есть класс Person и мы хотим добавить в него поле name: у людей есть имена, так что всё логично. Вот как это будет выглядеть на Java:</p>
27 <p>Допустим, у нас есть класс Person и мы хотим добавить в него поле name: у людей есть имена, так что всё логично. Вот как это будет выглядеть на Java:</p>
28 public class Person { private String name; }<p>Теперь, если мы захотим заполнить имя, то нам, вероятно, придётся написать общедоступный метод (с модификатором public). Тогда мы сможем обновить приватное поле. Именно такие методы чаще всего называют сеттерами - ведь они устанавливают (set) свойство объекта. Однако официально они называются<a>мутаторами</a>(mutator methods).</p>
28 public class Person { private String name; }<p>Теперь, если мы захотим заполнить имя, то нам, вероятно, придётся написать общедоступный метод (с модификатором public). Тогда мы сможем обновить приватное поле. Именно такие методы чаще всего называют сеттерами - ведь они устанавливают (set) свойство объекта. Однако официально они называются<a>мутаторами</a>(mutator methods).</p>
29 <p>На Java код для создания мутатора выглядит так:</p>
29 <p>На Java код для создания мутатора выглядит так:</p>
30 public setName(String name) { this.name = name; }<p>Мы написали уже шесть строк кода, но даже не можем получить значение переменной name вне класса. Чтобы это сделать, нужно написать геттер, или метод доступа:</p>
30 public setName(String name) { this.name = name; }<p>Мы написали уже шесть строк кода, но даже не можем получить значение переменной name вне класса. Чтобы это сделать, нужно написать геттер, или метод доступа:</p>
31 public getName() { return this.name; }<p>В языках с поддержкой автоматических свойств можно просто выкинуть эти шесть строк бойлерплейта. Вот полный аналог нашего джавишного класса на C#:</p>
31 public getName() { return this.name; }<p>В языках с поддержкой автоматических свойств можно просто выкинуть эти шесть строк бойлерплейта. Вот полный аналог нашего джавишного класса на C#:</p>
32 public class Person { public string Name { get; set; } }<p>С автоматическими свойствами мы пишем всего одну строку кода для каждого поля, которое хотим открыть другим классам, - и это здорово! Без такой фичи нам пришлось написать шесть строк.</p>
32 public class Person { public string Name { get; set; } }<p>С автоматическими свойствами мы пишем всего одну строку кода для каждого поля, которое хотим открыть другим классам, - и это здорово! Без такой фичи нам пришлось написать шесть строк.</p>
33 <p>Наш новый герой - необязательное связывание, или цепочки опциональных вызовов (optional chaining). Я впервые столкнулся с ними, когда писал<a>Hello World на Swift</a>. Для приветствия миру эта фича не пригодилась, но познакомиться с ней было интересно.</p>
33 <p>Наш новый герой - необязательное связывание, или цепочки опциональных вызовов (optional chaining). Я впервые столкнулся с ними, когда писал<a>Hello World на Swift</a>. Для приветствия миру эта фича не пригодилась, но познакомиться с ней было интересно.</p>
34 <p>Понять, что такое цепочки опциональных вызовов, нам помогут необязательные переменные. Для начала разберёмся с ними.</p>
34 <p>Понять, что такое цепочки опциональных вызовов, нам помогут необязательные переменные. Для начала разберёмся с ними.</p>
35 <p>В Swift переменные не могут быть пустыми. Иными словами, они не могут хранить значение NIL - по крайней мере, напрямую. И это отлично, потому что так мы уверены, что любая переменная содержит какое-то значение.</p>
35 <p>В Swift переменные не могут быть пустыми. Иными словами, они не могут хранить значение NIL - по крайней мере, напрямую. И это отлично, потому что так мы уверены, что любая переменная содержит какое-то значение.</p>
36 <p>Конечно, иногда нам необходимо передать в переменную NIL. К счастью, Swift позволяет это сделать с помощью необязательных переменных. Они оборачивают реальное значение (в том числе NIL) в контейнер, из которого это значение потом можно будет извлечь:</p>
36 <p>Конечно, иногда нам необходимо передать в переменную NIL. К счастью, Swift позволяет это сделать с помощью необязательных переменных. Они оборачивают реальное значение (в том числе NIL) в контейнер, из которого это значение потом можно будет извлечь:</p>
37 var printString: String? printString = "Hello, World!" print(printString!)<p>В этом примере мы объявляем необязательную строковую переменную и присваиваем ей значение "Hello, World!". Мы знаем, что в переменной лежит строка, и можем безо всяких проверок на NIL извлечь её и вывести на печать.</p>
37 var printString: String? printString = "Hello, World!" print(printString!)<p>В этом примере мы объявляем необязательную строковую переменную и присваиваем ей значение "Hello, World!". Мы знаем, что в переменной лежит строка, и можем безо всяких проверок на NIL извлечь её и вывести на печать.</p>
38 <p><strong>Примечание переводчика</strong></p>
38 <p><strong>Примечание переводчика</strong></p>
39 <p>С помощью символа ? после типа String мы поясняем компилятору, что printString - не просто строковая переменная, а опциональная (необязательная). То есть в ней может лежать строковое значение или пустое значение (NIL).</p>
39 <p>С помощью символа ? после типа String мы поясняем компилятору, что printString - не просто строковая переменная, а опциональная (необязательная). То есть в ней может лежать строковое значение или пустое значение (NIL).</p>
40 <p>С помощью символа ! мы извлекаем значение из такой переменной и выводим его на печать.</p>
40 <p>С помощью символа ! мы извлекаем значение из такой переменной и выводим его на печать.</p>
41 <p>Конечно, извлекать значение вот так, безо всяких проверок, - плохая практика. Здесь я пренебрёг этим правилом, чтобы не усложнять пример.</p>
41 <p>Конечно, извлекать значение вот так, безо всяких проверок, - плохая практика. Здесь я пренебрёг этим правилом, чтобы не усложнять пример.</p>
42 <p>Концепцию необязательных значений также применяют к вызовам методов и полям. В этом случае речь как раз идёт о цепочках опциональных вызовов. Представьте, что у нас есть длинная цепочка вызовов методов:</p>
42 <p>Концепцию необязательных значений также применяют к вызовам методов и полям. В этом случае речь как раз идёт о цепочках опциональных вызовов. Представьте, что у нас есть длинная цепочка вызовов методов:</p>
43 important_char = commandline_input.split('-').get(5).charAt(7)<p>В этом примере мы получаем из командной строки какое-то строковое значение и делим его на отрезки, ограниченные дефисами (-). Потом берём пятый из этих отрезков-строк и получаем из него седьмой по счёту символ. Если вызов хотя бы одного из трёх методов завершится неудачно, вся наша программа обрушится.</p>
43 important_char = commandline_input.split('-').get(5).charAt(7)<p>В этом примере мы получаем из командной строки какое-то строковое значение и делим его на отрезки, ограниченные дефисами (-). Потом берём пятый из этих отрезков-строк и получаем из него седьмой по счёту символ. Если вызов хотя бы одного из трёх методов завершится неудачно, вся наша программа обрушится.</p>
44 <p>С необязательным связыванием мы можем перехватить значение NIL в любом месте цепочки и обработать его. Тогда вместо сбоя мы получим значение important_char, равное NIL. Как по мне, это гораздо лучше, чем иметь дело с <a>пирамидой смерти (the pyramid of doom)</a>.</p>
44 <p>С необязательным связыванием мы можем перехватить значение NIL в любом месте цепочки и обработать его. Тогда вместо сбоя мы получим значение important_char, равное NIL. Как по мне, это гораздо лучше, чем иметь дело с <a>пирамидой смерти (the pyramid of doom)</a>.</p>
45 <p><strong>Примечание переводчика</strong></p>
45 <p><strong>Примечание переводчика</strong></p>
46 <p>Когда аргументами функций становятся другие функции, внутри которых тоже есть функции, может получиться что-то такое (пример на псевдокоде):</p>
46 <p>Когда аргументами функций становятся другие функции, внутри которых тоже есть функции, может получиться что-то такое (пример на псевдокоде):</p>
47 функция1(аргумент1, function (аргумент2, аргумент3) { функция2(аргумент4, function (аргумент5, аргумент6) { функция3(аргумент7, function (аргумент8, аргумент9) { }); }); });<p>Код функций принято записывать со смещением вправо - так проще понять, какой текст к какой функции относится. В итоге код действительно напоминает пирамиду.</p>
47 функция1(аргумент1, function (аргумент2, аргумент3) { функция2(аргумент4, function (аргумент5, аргумент6) { функция3(аргумент7, function (аргумент8, аргумент9) { }); }); });<p>Код функций принято записывать со смещением вправо - так проще понять, какой текст к какой функции относится. В итоге код действительно напоминает пирамиду.</p>
48 <p>Если уровней вложенности очень много, то вершина пирамиды уезжает далеко вправо. Читать, а тем более поддерживать такой код - смерти подобно :)</p>
48 <p>Если уровней вложенности очень много, то вершина пирамиды уезжает далеко вправо. Читать, а тем более поддерживать такой код - смерти подобно :)</p>
49 <p>Без лямбда-выражений этот список был бы неполным. Лямбда-выражения - не новая концепция (смотрите "<a>Hello World на Lisp</a>"), они даже старше компьютеров. И всё же их продолжают добавлять в современные языки программирования - даже в такие проработанные и устоявшиеся, как Java (в Java лямбды поддерживаются с 8-й версии; она вышла в 2014 году. - Пер.).</p>
49 <p>Без лямбда-выражений этот список был бы неполным. Лямбда-выражения - не новая концепция (смотрите "<a>Hello World на Lisp</a>"), они даже старше компьютеров. И всё же их продолжают добавлять в современные языки программирования - даже в такие проработанные и устоявшиеся, как Java (в Java лямбды поддерживаются с 8-й версии; она вышла в 2014 году. - Пер.).</p>
50 <p>Честно говоря, сам я впервые услышал про лямбда-выражения три или четыре года назад, когда изучал Java. Тогда я толком не понял, что в них интересного, - да и не особенно пытался узнать.</p>
50 <p>Честно говоря, сам я впервые услышал про лямбда-выражения три или четыре года назад, когда изучал Java. Тогда я толком не понял, что в них интересного, - да и не особенно пытался узнать.</p>
51 <p>Однако пару лет спустя я начал писать на Python, а в нём куча библиотек с открытым исходным кодом, которые вовсю используют лямбда-выражения. Так что в какой-то момент мне всё-таки пришлось с ними подружиться.</p>
51 <p>Однако пару лет спустя я начал писать на Python, а в нём куча библиотек с открытым исходным кодом, которые вовсю используют лямбда-выражения. Так что в какой-то момент мне всё-таки пришлось с ними подружиться.</p>
52 <p>Если вы не слышали о лямбдах, то, возможно, знаете хотя бы об анонимных функциях. Лямбды - это почти то же самое, но с одним отличием: их можно использовать как данные. А точнее - упаковать лямбда-выражение в переменную, а потом обращаться с ним как с обычными данными. Например:</p>
52 <p>Если вы не слышали о лямбдах, то, возможно, знаете хотя бы об анонимных функциях. Лямбды - это почти то же самое, но с одним отличием: их можно использовать как данные. А точнее - упаковать лямбда-выражение в переменную, а потом обращаться с ним как с обычными данными. Например:</p>
53 increment = lambda x: x + 1 increment(5) # вернёт 6<p>Тут мы создали функцию, сохранили её в переменную и вызвали, как любую другую обычную функцию. Фактически можно даже создать функцию, которая будет возвращать другие функции - то есть<a>динамически генерировать функции</a>.</p>
53 increment = lambda x: x + 1 increment(5) # вернёт 6<p>Тут мы создали функцию, сохранили её в переменную и вызвали, как любую другую обычную функцию. Фактически можно даже создать функцию, которая будет возвращать другие функции - то есть<a>динамически генерировать функции</a>.</p>
54 def make_incrementor(n): return lambda x: x + n addFive = make_incrementor(5) addFive(10) # вернёт 15<p>Круто!</p>
54 def make_incrementor(n): return lambda x: x + n addFive = make_incrementor(5) addFive(10) # вернёт 15<p>Круто!</p>
55 <p>Если вы хоть немного программировали, то, вероятно, знакомы с двумя основными видами типизации: статической и динамической. Только не путайте эти понятия с явной и неявной типизацией, а ещё с сильной и слабой типизацией. Это всё абсолютно разные вещи.</p>
55 <p>Если вы хоть немного программировали, то, вероятно, знакомы с двумя основными видами типизации: статической и динамической. Только не путайте эти понятия с явной и неявной типизацией, а ещё с сильной и слабой типизацией. Это всё абсолютно разные вещи.</p>
56 <p><strong>Примечание переводчика</strong></p>
56 <p><strong>Примечание переводчика</strong></p>
57 <p>При<strong>статической типизации</strong>тип переменной известен и проверяется во время компиляции, а при<strong>динамической</strong> - определяется во время выполнения в зависимости от значения переменной.</p>
57 <p>При<strong>статической типизации</strong>тип переменной известен и проверяется во время компиляции, а при<strong>динамической</strong> - определяется во время выполнения в зависимости от значения переменной.</p>
58 <p>При<strong>явной типизации</strong>задаётся тип каждой переменной. Это частный случай статической типизации. При<strong>неявной типизации</strong>типы указывать не обязательно.</p>
58 <p>При<strong>явной типизации</strong>задаётся тип каждой переменной. Это частный случай статической типизации. При<strong>неявной типизации</strong>типы указывать не обязательно.</p>
59 <p>При<strong>сильной типизации</strong>преобразуются один в другой только совместимые между собой типы. Например, в сильно типизированном языке нельзя без явных преобразований передать в подпрограмму строку, если параметр подпрограммы объявлен с числовым типом.</p>
59 <p>При<strong>сильной типизации</strong>преобразуются один в другой только совместимые между собой типы. Например, в сильно типизированном языке нельзя без явных преобразований передать в подпрограмму строку, если параметр подпрограммы объявлен с числовым типом.</p>
60 <p><strong>Слабо типизированные языки</strong>относятся к совместимости типов более свободно. Например, в JavaScript можно складывать строку с числом. Ошибок не будет, просто число преобразуется в строку.</p>
60 <p><strong>Слабо типизированные языки</strong>относятся к совместимости типов более свободно. Например, в JavaScript можно складывать строку с числом. Ошибок не будет, просто число преобразуется в строку.</p>
61 <p>Пересечение между статической и динамической типизацией называется постепенной (gradual) типизацией. И для меня это одна из самых крутых фишек в языках программирования. Пользователь сам указывает, когда ему нужна статическая типизация, а по умолчанию действует динамическая.</p>
61 <p>Пересечение между статической и динамической типизацией называется постепенной (gradual) типизацией. И для меня это одна из самых крутых фишек в языках программирования. Пользователь сам указывает, когда ему нужна статическая типизация, а по умолчанию действует динамическая.</p>
62 <p>Во многих языках постепенная типизация реализуется через объявления типов (как в явно типизированных языках):</p>
62 <p>Во многих языках постепенная типизация реализуется через объявления типов (как в явно типизированных языках):</p>
63 def divide(dividend: float, divisor: float) -&gt; float: return dividend / divisor<p>Это функция на Python, у которой явно указаны типы входного и выходного параметров (у обоих тип float. - Пер.). Но можно обойтись и без объявления типов:</p>
63 def divide(dividend: float, divisor: float) -&gt; float: return dividend / divisor<p>Это функция на Python, у которой явно указаны типы входного и выходного параметров (у обоих тип float. - Пер.). Но можно обойтись и без объявления типов:</p>
64 def divide(dividend, divisor): return dividend / divisor<p>Теперь ничто не помешает передать в эту функцию вообще всё что угодно. Однако первый вариант реализации позволяет статически проверять типы - встроенными в IDE инструментами статического анализа или какими-то внешними утилитами. Я бы сказал, что это беспроигрышный вариант.</p>
64 def divide(dividend, divisor): return dividend / divisor<p>Теперь ничто не помешает передать в эту функцию вообще всё что угодно. Однако первый вариант реализации позволяет статически проверять типы - встроенными в IDE инструментами статического анализа или какими-то внешними утилитами. Я бы сказал, что это беспроигрышный вариант.</p>
65 <p>Хоть я и выбрал для иллюстрации Python, но впервые столкнулся с постепенной типизацией, когда писал<a>Hello World на языке Hack</a>. Похоже, Facebook* и правда решил улучшить систему типов в PHP.</p>
65 <p>Хоть я и выбрал для иллюстрации Python, но впервые столкнулся с постепенной типизацией, когда писал<a>Hello World на языке Hack</a>. Похоже, Facebook* и правда решил улучшить систему типов в PHP.</p>
66 <p>* Решением суда запрещена "деятельность компании Meta Platforms Inc. по реализации продуктов - социальных сетей Facebook* и Instagram* на территории Российской Федерации по основаниям осуществления экстремистской деятельности".</p>
66 <p>* Решением суда запрещена "деятельность компании Meta Platforms Inc. по реализации продуктов - социальных сетей Facebook* и Instagram* на территории Российской Федерации по основаниям осуществления экстремистской деятельности".</p>
67 <a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>
67 <a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>