img

Полное руководство по пакету NumPy для научных расчетов в Python

 

NumPy (произносится как «numb pie») – это один из самых важных пакетов, который нужно усвоить при изучении Python. 

Пакет известен за счет своей удобной структуры данных, которая именуется массивом NumPy. NumPy также позволяет разработчикам Python быстро выполнять самые разные числовые вычисления. 

Это руководство научит вас основам NumPy, которые вы сможете использовать для вычислений в Python. 

Введение в NumPy

В этом разделе мы познакомимся с библиотекой NumPy.

Что такое NumPy?

NumPy – это бибилиотека Python для реализации научных расчетов. NumPy расшифровывается как Numerical Python, то есть «Числовой Python». Вот официальное описание библиотеки с ее сайта:

«NumPy – это основной пакет для выполнения научных расчетов с помощью Python. Кроме прочего, он содержит:

  • объект N-мерного массива, который обладает большими преимуществами;
  • сложные (распределительные) функции;
  • инструменты для интеграции кода C/C++ и Fortran;
  • поддержку линейной алгебры, преобразования Фурье и случайных чисел.

Кроме того, что NumPy имеет научное применение, его также можно использовать в качестве эффективного многомерного контейнера общих данных. Можно определить произвольные типы данных. Это позволяет NumPy легко и быстро интегрироваться с различными базами данных. 

NumPy лицензируется в соответствии с BSD, что позволяет использовать его где угодно, но с небольшими ограничениями».

NumPy – настолько важная библиотека в Python, что есть такие библиотеки (в том числе pandas), которые были полностью созданы на основе NumPy.

Главное преимущество NumPy

Главное преимущество NumPy заключается в том, что он позволяет очень быстро генерировать и обрабатывать данные. NumPy имеет свою собственную встроенную структуру данных под названием «массив», которая чем-то похожа на обычный список Python, но в нем можно хранить данные, и при этом работать с ними можно будет куда проще. 

Что мы будем изучать о NumPy

Продвинутые специалисты по Python гораздо больше работают с pandas, нежели с NumPy. И тем не менее, с учетом того, что pandas построен на NumPy, важно знать и понимать наиболее важные аспекты библиотеки NumPy.

 Далее мы рассмотрим следующие сведения о NumPy:

  • Массивы NumPy
  • Индексация и присваивание в NumPy
  • Методы и операции в NumPy

Едем дальше

Давайте перейдем к изучению массивов NumPy. Это основная структура данных, с которой должен быть знаком каждый специалист-практик. 

Массивы NumPy

В этом разделе мы поговорим с вами о NumPy-массивах.

Что такое массивы NumPy?

Массивы NumPy – это основной способ хранения данных с помощью библиотеки NumPy. Они похожи на обычные списки в Python, но имеют серьезное преимущество – они работают быстрее и имеют больше встроенных методов. 

Массивы NumPy создаются с помощью вызова метода array() из библиотеки NumPy. Внутри метода передается список. 

Ниже приведен пример самого простого NumPy-массива. Отмечу, что в этом фрагменте кода я запускаю оператор import numpy as np, но дальше для краткости я не буду его писать. 

import numpy as np

sample_list = [1, 2, 3]

np.array(sample_list)

Последняя строка данного фрагмента кода выведет следующее:

array([1,2,3])

Обёртка array() указывает на то, что это больше не обычный список Python. Теперь это массив NumPy.

Два вида массивов NumPy

Есть два вида массивов NumPy: векторы и матрицы.

Векторы – это одномерные NumPy-массивы, и выглядят они так:

my_vector = np.array(['this', 'is', 'a', 'vector'])

Матрицы – это двумерные массивы, и они создаются путем передачи списка списков в метод np.array(). Ниже приведен пример:

my_matrix = [[1, 2, 3],[4, 5, 6],[7, 8, 9]]

np.array(my_matrix)

Вы можете расширить массивы NumPy для работы с трех-, четырех-, пяти-, шестимерными массивами и массивами более высокой размерности, но они встречаются крайне редко и в целом выходят за рамки этого курса (в конце концов, это курс по программированию на Python, а не по линейной алгебре). 

Массивы NumPy: встроенные методы

Массивы NumPy имеют ряд полезных встроенных методов. В оставшейся части этого раздела мы подробно разберем эти методы. 

Как получить диапазон чисел в Python с помощью NumPy

В NumPy есть полезный метод arange, который принимает  в качестве аргумента два числа и выдает массив целых чисел, который находятся в диапазоне от первого числа включительно и до второго числа не включительно. 

Ниже приведен пример метода arange

np.arange(0,5)

#Returns array([0, 1, 2, 3, 4])
{ #Возвращает array([0, 1, 2, 3, 4]) }

Методу arange можно передать и третий аргумент. Он будет задавать размер шага возвращаемого массива. Передав 2 в качестве третьей переменной, метод выведет каждое второе число в диапазоне, а передав 5 в качестве третьей переменной – каждое пятое. 

Ниже приведен пример с третьей переменной в методе arange

np.arange(1,11,2)

#Returns array([1, 3, 5, 7, 9])
{ #Возвращает array([1, 3, 5, 7, 9]) }

Как получить массивы из нулей и единиц в Python с помощью NumPy

Когда вы пишете программный код, вам может потребоваться создать массив из нулей или единиц. У NumPy есть встроенные методы, с помощью которых можно сгенерировать такие массивы. 

Массив из нулей можно создать с помощью метода zeros. В качестве аргумента функции вам необходимо передать число, которое будет определять размер массива. Ниже приведен пример.

np.zeros(4)

#Returns array([0, 0, 0, 0])
{ #Возвращает array([0, 0, 0, 0]) }

Аналогично можно создать двумерные массивы. Например, np.zeros(5, 5) создаст матрицу 5х5, которая состоит только из нулей.

По той же схеме можно создавать массивы из единиц с помощью метода ones. Ниже приведен пример. 

np.ones(5)

#Returns array([1, 1, 1, 1, 1])
{ #Возвращает array([1, 1, 1, 1, 1]) }

Как разделить диапазон чисел в Python на равные интервалы с помощью NumPy

Бывают ситуации, когда, у вас есть диапазон чисел и вам нужно разделить его на равные интервалы. Для того, чтобы это сделать вам понадобиться метод linspace. Этот метод принимает три аргумента:

  1. Начало интервала
  2. Конец интервала
  3. Количество подинтервалов, на которые вы хотите поделить исходный интервал

Ниже приведенпример использования метода linspace

np.linspace(0, 1, 10)

#Returns array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
{ #Возвращает array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) }

Как создать единичную матрицу в Python с помощью NumPy

Любой, кто когда-либо изучал линейную алгебру, знаком с понятием «единичная матрица». Единичная матрица – это квадратная матрица, в которой все диагональные значения равны 1, а остальные – 0. У NumPy есть встроенная функция для построения единичной матрицы, которая принимает один аргумент. Это функция eye.

Ниже приведен пример:

np.eye(1)

#Returns a 1x1 identity matrix
{ #Возвращает единичную матрицу размером 1х1 }
np.eye(2) 

#Returns a 2x2 identity matrix
{ #Возвращает единичную матрицу размером 2х2 }
np.eye(50)

#Returns a 50x50 identity matrix
{ #Возвращает единичную матрицу размером 50х50 }

Как сгенерировать случайные числа в Python с помощью NumPy

У NumPy есть целый ряд встроенных методов, с помощью которых можно создавать массивы случайных чисел. Каждый из этих методов начинается со слова random. Ниже приведены несколько примеров:

np.random.rand(sample_size)

#Returns a sample of random numbers between 0 and 1.
{ #Возвращает выборку случайных чисел между 0 и 1 }
#Sample size can either be one integer (for a one-dimensional array) or two integers separated by commas (for a two-dimensional array).
{ #Размер выборки (sample_size) может быть как одним целым числом (для одномерного массива), так и двумя целыми числами, записанными через запятую (для двумерного массива) }
np.random.randn(sample_size)

#Returns a sample of random numbers between 0 and 1, following the normal distribution.
{ #Возвращает выборку случайных чисел от 0 до 1 в соответствии с законом нормального распределения }
#Sample size can either be one integer (for a one-dimensional array) or two integers separated by commas (for a two-dimensional array).
{ #Размер выборки (sample_size) может быть как одним целым числом (для одномерного массива), так и двумя целыми числами, записанными через запятую (для двумерного массива) }
np.random.randint(low, high, sample_size)

#Returns a sample of integers that are greater than or equal to 'low' and less than 'high'
{ #Возвращает выборку целых чисел, которые больше или равны значению low и меньше значения high }

Как изменить массивы NumPy

Нередко массив определенного размера необходимо преобразовать в массив другой формы. Например, у вас есть одномерный массив из 10 элементов, а вы хотите переделать его в двумерный массив 2х5. 

Ниже приведен пример:

arr = np.array([0,1,2,3,4,5])

arr.reshape(2,3)

Результат будет следующий:

array([[0, 1, 2],

       [3, 4, 5]])

Имейте в виду, что для того, чтобы метод reshape можно было использовать, необходимо, чтобы исходный массив имел такое же количество элементов, что и массив, в который вы хотите его трансформировать. 

Если вы хотите узнать текущую форму массива NumPy, то вы можете это сделать с помощью атрибута shape. Ниже приведен пример вызова атрибута shape для нашей предыдущей структуры переменных arr:

arr = np.array([0,1,2,3,4,5])

arr.shape

#Returns (6,) - note that there is no second element since it is a one-dimensional array
{ #Возвращает (6,0). Здесь отсутствует второй элемент, так как это одномерный массив } 
arr = arr.reshape(2,3)

arr.shape

#Returns (2,3)
{ #Возвращает (2,3) }

Вы можете применить метод reshape и вызов атрибута shape одновременно в одной строке, как показано ниже:

arr.reshape(2,3).shape

#Returns (2,3)
{ #Возвращает (2,3) }

Как найти максимальное и минимальное значение NumPy-массива

В конце давайте разберем четыре полезных метода, которые позволят вам определить максимальные и минимальные значения NumPy-массивов. Мы будем работать со следующим массивом:

simple_array = [1, 2, 3, 4]

Для того, чтобы найти максимальное значение в NumPy-массиве, мы можем воспользоваться методом max. Ниже приведен пример:

simple_array.max()

#Returns 4
{ #Возвращает 4 }

Также мы можем воспользоваться методом argmax для того, чтобы найти индекс максимального значения в массиве NumPy. Это может оказаться полезным, когда вам необходимо найти позицию максимального элемента, но вам совершенно без разницы, что это за значение. 

Ниже приведен пример:

simple_array.argmax()

#Returns 3
{ #Возвращает 3 }

Аналогично с методами min и argmin. С их помощью можно найти минимальное значение или его индекс в массиве NumPy:

simple_array.min()

#Returns 1
{ #Возвращает 1 }
simple_array.argmin()

#Returns 0
{ #Возвращает 0 }

Двигаемся дальше

В этом разделе мы обсудили различные атрибуты и методы массивов NumPy. В следующем разделе мы разберем некоторые практические задачи с массивами NumPy.

Методы и операции в NumPy

В этом разделе мы разберем, как работают различные операции, которые есть в библиотеке NumPy.

Здесь мы будем предполагать, что команда import numpy as np уже запущена. 

Массив, с которым я буду работать в этом разделе

В этом разделе во всех примерах я буду работать с массивом длины 4, который будет создан с помощью np.arange.

Если вы хотите соотнести мой массив с выходными данными, которые я получил в этом разделе, то ниже я привел, как я создал и напечатал массив:

arr = np.arange(4)

arr

Ниже приведены значения массива:

array([0, 1, 2, 3])

Как выполнять арифметические действия с числами в Python

С помощью NumPy можно легко выполнять арифметические действия с массивами. Вы можете выполнять арифметические действия либо с массивом и числом, либо с двумя массивами.

Ниже мы разберем основные математические операции.

Сложение

Когда вы складываете число с NumPy-массивом, это число прибавляется к каждому элементу массива. Ниже приведен пример:

2 + arr

#Returns array([2, 3, 4, 5])
{ #Возвращает array([2, 3, 4, 5]) }

С помощью оператора + вы также можете сложить два NumPy-массива. Массивы будут складываться поэлементно (это значит, что первые элементы складываются друг с другом, потом вторые и т.д.).

Ниже приведен пример:

arr + arr

#Returns array([0, 2, 4, 6])
{ #Возвращает array([0, 2, 4, 6]) }

Вычитание

Как и сложение, вычитание выполняется поэлементно. Ниже приведены примеры как с вычитанием числа, так и с вычитанием другого NumPy-массива.

arr - 10

#Returns array([-10,  -9,  -8,  -7])
{ #Возвращает array([-10,  -9,  -8,  -7]) }
arr - arr

#Returns array([0, 0, 0, 0])
{ #Возвращает array([0, 0, 0, 0]) }

Умножение

Умножение также выполняется поэлементно. При этом неважно, умножаете вы массив на число или на другой массив.

Ниже приведены два примера:

6 * arr

#Returns array([ 0,  6, 12, 18])
{ #Возвращает array([ 0,  6, 12, 18]) }
arr * arr

#Returns array([0, 1, 4, 9])
{ #Возвращает array([0, 1, 4, 9]) }

Деление

Я, думаю, здесь вы не сильно удивитесь, если узнаете, что деление NumPy-массивов выполняется поэлементно. Ниже приведен пример деления arr на число:

arr / 2

#Returns array([0. , 0.5, 1. , 1.5])
{ #Возвращает array([0. , 0.5, 1. , 1.5]) }

Правда, если сравнивать деление с другими математическими операциями, которые мы здесь рассмотрели, у него есть одно несоответствие. Так как делить на ноль мы не можем, то в случае, если деление на ноль все же произойдет, это приведет к тому, что соответствующее поле заполнится значением nan, которое в Python является сокращением от «Not A Number». Jupiter Notebook напечатает следующее предупреждение:

RuntimeWarning: invalid value encountered in true_divide

Ниже приведен пример деления на ноль: 

arr / arr

#Returns array([nan,  1.,  1.,  1.])
{ #Возвращает array([nan,  1.,  1.,  1.]) }

Как обращаться со значениями nan мы узнаем чуть позже в этом же руководстве. 

Сложные операции с массивами NumPy

Многие операции нельзя выполнять, применяя к массиву NumPy обычный синтаксис. В этом разделе мы рассмотрим несколько математических операций, у которых есть встроенные методы в библиотеке NumPy. 

Как вычислить квадратные корни с помощью NumPy

Вы можете вычислить квадрантный корень каждого элемента массива с помощью метода np.sqrt:

np.sqrt(arr)

#Returns array([0., 1., 1.41421356, 1.73205081])
{ #Возвращает array([0., 1., 1.41421356, 1.73205081]) }

Ниже приведены еще примеры (отмечу, что в тестировании этих методов не будет, но на них полезно взглянуть, чтобы увидеть возможности NumPy):

np.exp(arr)

#Returns e^element for every element in the array 
{ #Возвращает e^element для каждого элемента (element) в массиве }
np.sin(arr)

#Calculate the trigonometric sine of every value in the array
{ #Вычисляет тригонометрическую функцию синуса от каждого значения в массиве }
np.cos(arr)

#Calculate the trigonometric cosine of every value in the array
{ #Вычисляет тригонометрическую функцию косинуса от каждого значения в массиве }
np.log(arr)

#Calculate the base-ten logarithm of every value in the array
{ #Вычисляет логарифм по основанию 10 от каждого значения в массиве }

Двигаемся дальше

В этом разделе мы рассмотрели различные методы и операции, которые есть в библиотеке NumPy в Python. Ваши знания об этих понятиях мы проверим с помощью практических задач, представленных далее. 

Индексация и присваивание в NumPy

В этом разделе мы разберем индексацию и присваивание в NumPy-массивах. 

Массив, который я буду использовать в этом разделе

Как и до этого, на протяжении всего раздела я буду использовать один массив. На этот раз он будет сгенерирован с помощью метода np.random.rand. Ниже я продемонстрировал, как я сгенерировал массив:

arr = np.random.rand(5)

А вот и сам массив:

array([0.69292946, 0.9365295 , 0.65682359, 0.72770856, 0.83268616])

Для того, чтобы его было проще воспринимать, я округлю каждый элемент массива до 2 знаков после запятой с помощью метода round:

arr = np.round(arr, 2)

И вот такой массив у меня получился:

array([0.69, 0.94, 0.66, 0.73, 0.83])

Как вернуть определенный элемент из массива NumPy

Вы можете выбрать (и вернуть) определенный элемент из NumPy-массива аналогично тому, как если вы бы это делали с обычным списком Python, то есть с помощью квадратных скобок.

Ниже приведен пример:

arr[0]

#Returns 0.69
{ #Возвращает 0.69 }

Мы также можем обращаться к нескольким элементам массива с помощью оператора двоеточия. Например, индекс [2:] выбирает все элементы, начиная с индекса 2 и далее. Индекс [:3] выбирает все элементы до элемента с индексом 3, исключая элемент с индексом 3. Индекс [2:4] возвращает все элементы от индекса 2 до индекса 4, исключая элемент с индексом 4. Конечная точка более высокого порядка всегда исключается. 

Ниже приведены несколько примеров индексации с помощью оператора двоеточия.

arr[:]

#Returns the entire array: array([0.69, 0.94, 0.66, 0.73, 0.83])
{ #Возвращает весь массив: array([0.69, 0.94, 0.66, 0.73, 0.83]) }
arr[1:]

#Returns array([0.94, 0.66, 0.73, 0.83])
{ #Возвращает array([0.94, 0.66, 0.73, 0.83]) }
arr[1:4] 

#Returns array([0.94, 0.66, 0.73])
{ #Возвращает array([0.94, 0.66, 0.73]) }

Присваивание элементов в массивах NumPy

Мы можем присваивать новые значения элементам массива NumPy с помощью оператора =. Все как и в обычных списках Python. Ниже приведены несколько примеров (отмечу, что все это один блок кода, а это значит, что присваивания выполняются от строчки к строчке).

array([0.12, 0.94, 0.66, 0.73, 0.83])

arr

#Returns array([0.12, 0.94, 0.66, 0.73, 0.83])
{ #Возвращает array([0.12, 0.94, 0.66, 0.73, 0.83]) }
arr[:] = 0

arr

#Returns array([0., 0., 0., 0., 0.])
{ #Возвращает array([0., 0., 0., 0., 0.]) }
arr[2:5] = 0.5

arr

#Returns array([0. , 0. , 0.5, 0.5, 0.5])
{ #Возвращает array([0. , 0. , 0.5, 0.5, 0.5]) }

Создание ссылок в NumPy

NumPy использует такую концепцию, как «создание ссылок», которая может сбивать с толку многих людей, которые только начали знакомиться с библиотекой. 

Для того, чтобы понять, что такое ссылки, давайте сначала рассмотрим пример:

new_array = np.array([6, 7, 8, 9])

second_new_array = new_array[0:2]

second_new_array

#Returns array([6, 7])
{ #Возвращает array([6, 7]) }
second_new_array[1] = 4

second_new_array 

#Returns array([6, 4]), as expected
{ #Возвращает array([6, 4]) (как и следовало ожидать) }
new_array 

#Returns array([6, 4, 8, 9]) 
{ #Возвращает array([6, 4, 8, 9]) }
#which is DIFFERENT from its original value of array([6, 7, 8, 9])
{ #который ОТЛИЧАЕТСЯ от исходного массива array([6, 7, 8, 9]) }
#What the heck?
{ #Что за дела? }

Как вы могли заметить, когда мы изменили массив second_new_array, это привело к тому, что изменились и значения массива new_array

Почему же?

По умолчанию, когда вы обращаетесь к исходной переменной с помощью оператора присваивания =, NumPy не создает копию массива. Вместо этого он просто указывает новой переменной на старую переменную. Из-за этого вторая переменная может повлиять на исходную, и даже не спросит вам об этом. 

Это может показаться ненормальным, но у этого есть логическое объяснение. Цель такого обращения к массиву – экономия вычислительной мощности. Когда вы работаете с большими наборами данных, то у вас может быстро закончится оперативная память, если вы будете создавать новый массив каждый раз, когда вам нужно работать с какой-то его частью.

К счастью, есть решение данной проблемы. Вы можете использовать метод copy для того, чтобы явно скопировать массив NumPy.

Ниже приведен пример. 

array_to_copy = np.array([1, 2, 3])

copied_array = array_to_copy.copy()

array_to_copy

#Returns array([1, 2, 3])
{ #Возвращает array([1, 2, 3]) }
copied_array

#Returns array([1, 2, 3])
{ #Возвращает array([1, 2, 3]) }

Ниже вы можете увидеть, что изменения, которые были внесены в скопированный массив, никак не повлияли на исходный. 

copied_array[0] = 9

copied_array

#Returns array([9, 2, 3])
{ #Возвращает array([9, 2, 3]) }
array_to_copy

#Returns array([1, 2, 3])
{ #Возвращает array([1, 2, 3]) }

До сих пор в этом разделе мы рассматривали, как обращаться к одномерным массивам NumPy. А теперь давайте перейдем к индексации двумерных массивов. 

Индексация двумерных массивов NumPy

Для начала давайте создадим двумерный массив NumPy и назовем его mat:

mat = np.array([[5, 10, 15],[20, 25, 30],[35, 40, 45]])

mat

"""

Returns:

array([[ 5, 10, 15],

       [20, 25, 30],

       [35, 40, 45]])

"""
{ “““
Возвращает:
array([[ 5, 10, 15],

       [20, 25, 30],

       [35, 40, 45]])
“““ }

Есть два способа, как можно обращаться к двумерному массиву NumPy через индексы:

  • mat[row, col]
  • mat[row][col]

Лично мне больше нравится второй вариант, так как его легче пошагово визуализировать. Например,

#First, let's get the first row:
{ #Для начала давайте получим первую строку: }
mat[0]

#Next, let's get the last element of the first row:
{ #А теперь, давайте получим последний элемент первой строки: }
mat[0][-1]

Вы также можете сгенерировать подматрицы из двумерного массива NumPy:

mat[1:][:2]

"""

Returns:

array([[20, 25, 30],

       [35, 40, 45]])

"""
{ “““
Возвращает:
array([[20, 25, 30],

       [35, 40, 45]])
“““ }

Для двумерных массивов понятие «ссылки» все также актуально, поэтому используйте метод copy, если вы хотите избежать случайного вмешательства в исходный массив после того, как сохранили его часть в новую переменную.

Условный выбор с помощью массивов NumPy

Массивы NumPy поддерживают функцию, которая называется conditional selection (условный выбор). Она позволяет создавать новый массив из логических значений, который указывает, удовлетворяют ли элементы в массиве определенному оператору if. Ниже приведен пример (я также снова создал наш исходный массив arr, так как мы давно с ним не работали):

arr = np.array([0.69, 0.94, 0.66, 0.73, 0.83])

arr > 0.7

#Returns array([False,  True, False,  True,  True])
{ #Возвращает array([False,  True, False,  True,  True]) }

Вы также можете сгенерировать новый массив, значения в котором будут удовлетворять условию, которое вы передадите в квадратных скобках (как при индексации).

Ниже приведен пример:

arr[arr > 0.7]

#Returns array([0.94, 0.73, 0.83])
{ #Возвращает array([0.94, 0.73, 0.83]) }

Условный выбор можно даже усложнить. Больше примеров мы рассмотрим в практических задачах этого раздела. 

Двигаемся дальше

В этом разделе мы подробно рассмотрели индексацию и присваивание массивов NumPy. Ваши знания по этой теме мы закрепим с помощью решения практических задач в следующем разделе. 

Заключительная мысль 

Благодарю за прочтение этой статьи о NumPy. Это один из моих любимых пакетов Python и библиотека, которая является обязательной для изучения для каждого разработчика Python. 

 

Ссылка
скопирована
Программирование
Скидка 25%
Python-программист с нуля
Стань разработчиком на одном из самых популярных языков программирования.
Получи бесплатный
вводный урок!
Пожалуйста, укажите корректный e-mail
отправили вводный урок на твой e-mail!
Получи все материалы в telegram и ускорь обучение!
img
Еще по теме:
img
Если вы хотите перейти с GitHub на какую-нибудь новую платформу, то вот вам несколько отличных альтернативных вариантов, где вы
img
Среди таких языков, как Java, JavaScript и Mocha, существует поразительно большое число языков программирования, которые были на
img
  Введение Docker – это популярная программа для контейнеризации, которая обеспечивает безопасность, ускоряет развертывание прил
img
Вот оно – слово, которое ненавидят все разработчики, -  конфликт . Если вы работаете с Git (или с какими-то другими системами ко
img
Пожалуй, каждый разработчик хочет, чтобы его код был структурированным, несложно запрограммированным и хорошо закомментированным
img
Это довольно распространенный вопрос, который волнует многих пользователей Linux. К тому же, этот вопрос частенько задают на экз
Комментарии
ЛЕТНИЕ СКИДКИ
40%
50%
60%
До конца акции: 30 дней 24 : 59 : 59