Kodduu Python
1.08K subscribers
311 photos
28 videos
186 links
Научись программировать на Python на интересных примерах

Самый быстрый курс https://stepik.org/a/187914
Самый нескучный курс https://stepik.org/a/185238

Во вопросам сотрудничества: @AlexErf
Download Telegram
if hit_info:
hit_point, normal, material = hit_info
color = np.zeros(3)
for light in self.lights:
light_dir = light.position - hit_point
light_dir = light_dir / np.linalg.norm(light_dir)
# Проверка на тени
shadow_ray = Ray(hit_point + 1e-5 * normal, light_dir)
in_shadow = False
for obj in self.objects:
shadow_result = obj.intersect(shadow_ray)
if shadow_result and shadow_result[0] > 1e-5:
in_shadow = True
break
if not in_shadow:
color += material.shade(hit_point, normal, light_dir) * light.intensity
return np.clip(color, 0, 1)
else:
return np.array([0.2, 0.7, 1.0]) # Цвет фона

# Создаем сцену
scene = Scene()

# Добавляем объекты
material_red = LambertianMaterial(np.array([1, 0, 0]))
material_green = LambertianMaterial(np.array([0, 1, 0]))
material_blue = LambertianMaterial(np.array([0, 0, 1]))
material_yellow = LambertianMaterial(np.array([1, 1, 0]))

sphere1 = Sphere(np.array([-0.6, 0, -3]), 0.5, material_red)
sphere2 = Sphere(np.array([0.6, 0, -3]), 0.5, material_green)
sphere3 = Sphere(np.array([0, -1000.5, -3]), 1000, material_blue) # Плоскость
sphere4 = Sphere(np.array([0, 0.75, -3]), 0.5, material_yellow)

scene.add_object(sphere1)
scene.add_object(sphere2)
scene.add_object(sphere3)
scene.add_object(sphere4)

# Добавляем источник света
light1 = Light(np.array([5, 5, -5]), 1.2)
light2 = Light(np.array([-5, 5, -5]), 1.0)
scene.add_light(light1)
scene.add_light(light2)

# Рендерим сцену
scene.render(400, 300)


Описание кода:

1. Импортируем необходимые модули:
- numpy для работы с векторами и матрицами.
- PIL (Python Imaging Library) для создания и отображения изображения.
- abc для создания абстрактных классов.

2. Определяем класс `Ray`:
- Представляет луч с origin (начало) и direction (направление).

3. Создаем абстрактный класс `Material`:
- Имеет абстрактный метод shade, который должен быть реализован в подклассах.

4. Класс `LambertianMaterial`:
- Наследует от Material.
- Реализует диффузное освещение по модели Ламберта.

5. Класс `Sphere`:
- Представляет сферу в 3D-пространстве.
- Метод intersect вычисляет пересечение луча со сферой и возвращает информацию о пересечении.

6. Класс `Light`:
- Представляет точечный источник света с позицией и интенсивностью.

7. Класс `Scene`:
- Содержит объекты и источники света.
- Метод render создает изображение, трассируя лучи из камеры в сцену.
- Метод ray_trace выполняет трассировку луча и вычисляет цвет пикселя.

8. Создаем сцену и добавляем объекты:
- Четыре сферы с разными материалами (красный, зеленый, синий, желтый).
- Большая сфера, представляющая плоскость (землю).

9. Добавляем источники света:
- Два точечных источника света для более сложного освещения сцены.

10. Рендерим сцену:
- Вызываем scene.render с заданными шириной и высотой изображения.

Как это работает:

- Трассировка лучей:
- Для каждого пикселя генерируется луч из камеры через плоскость проекции.
- Луч пересекается со всеми объектами сцены, выбирается ближайшее пересечение.
- Если есть пересечение, вычисляется цвет точки с учетом освещения и теней.
- Если пересечений нет, пикселю присваивается цвет фона.

- Освещение и тени:
- Используется модель освещения Ламберта для расчета диффузного освещения.
- Для каждого источника света проверяется, не находится ли точка в тени других объектов.

Основные концепции ООП, используемые в коде:
- Инкапсуляция: Классы Ray, Sphere, Light, Scene и другие скрывают детали реализации и предоставляют интерфейсы для взаимодействия.
- Наследование: LambertianMaterial наследует от абстрактного класса Material.
- Полиморфизм: Метод shade может быть переопределен в разных материалах для создания различных эффектов освещения.
- Абстрактные классы и методы: Использование ABC и abstractmethod для определения интерфейса материалов.

Как запустить код:

1. Установите необходимые библиотеки:


pip install numpy pillow


2. Запустите скрипт.

Что вы увидите:

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

Дальнейшие улучшения:

- Добавить отражения и преломления для материалов.
- Реализовать дополнительные модели освещения.
- Добавить новые примитивы (плоскости, треугольники).
- Оптимизировать пересечения и добавить ускоряющие структуры данных.

Вывод:

Этот пример демонстрирует, как можно использовать ООП для создания сложных программ, таких как трассировщик лучей. Классы и наследование помогают структурировать код, а полиморфизм позволяет легко расширять функциональность.

Подпишись 👉🏻 @KodduuPython 🤖
Курс Python Data Science: самый быстрый курс готов 😎😎😎

Подпишись 👉🏻 @KodduuPython 🤖
Давайте разработаем сложную программу на Python с использованием объектно-ориентированного программирования (ООП). Мы создадим простую версию интерпретатора языка программирования, который будет включать парсинг, интерпретацию и выполнение кода на нашем собственном мини-языке.


import re
from abc import ABC, abstractmethod

# Определение абстрактного синтаксического узла
class ASTNode(ABC):
@abstractmethod
def evaluate(self, context):
pass

# Узел для числовых значений
class NumberNode(ASTNode):
def __init__(self, value):
self.value = value

def evaluate(self, context):
return self.value

# Узел для переменных
class VariableNode(ASTNode):
def __init__(self, name):
self.name = name

def evaluate(self, context):
return context.get(self.name, 0)

# Узел для бинарных операций
class BinaryOperationNode(ASTNode):
def __init__(self, left, operator, right):
self.left = left
self.operator = operator
self.right = right

def evaluate(self, context):
left_val = self.left.evaluate(context)
right_val = self.right.evaluate(context)
if self.operator == '+':
return left_val + right_val
elif self.operator == '-':
return left_val - right_val
elif self.operator == '*':
return left_val * right_val
elif self.operator == '/':
return left_val / right_val

# Узел для присваивания
class AssignmentNode(ASTNode):
def __init__(self, variable_name, expression):
self.variable_name = variable_name
self.expression = expression

def evaluate(self, context):
value = self.expression.evaluate(context)
context[self.variable_name] = value
return value

# Узел для вывода на экран
class PrintNode(ASTNode):
def __init__(self, expression):
self.expression = expression

def evaluate(self, context):
value = self.expression.evaluate(context)
print(value)
return value

# Парсер кода
class Parser:
def __init__(self, code):
self.tokens = re.findall(r'\w+|[\+\-\*/=()]', code)
self.position = 0

def parse(self):
nodes = []
while self.position < len(self.tokens):
node = self.parse_statement()
nodes.append(node)
return nodes

def parse_statement(self):
if self.tokens[self.position] == 'print':
self.position += 1
expr = self.parse_expression()
return PrintNode(expr)
else:
return self.parse_assignment()

def parse_assignment(self):
var_name = self.tokens[self.position]
self.position += 1
if self.tokens[self.position] == '=':
self.position += 1
expr = self.parse_expression()
return AssignmentNode(var_name, expr)
else:
raise SyntaxError('Ожидалось "="')

def parse_expression(self):
node = self.parse_term()
while self.position < len(self.tokens) and self.tokens[self.position] in ('+', '-'):
op = self.tokens[self.position]
self.position += 1
right = self.parse_term()
node = BinaryOperationNode(node, op, right)
return node

def parse_term(self):
node = self.parse_factor()
while self.position < len(self.tokens) and self.tokens[self.position] in ('*', '/'):
op = self.tokens[self.position]
self.position += 1
right = self.parse_factor()
node = BinaryOperationNode(node, op, right)
return node
def parse_factor(self):
token = self.tokens[self.position]
if token.isdigit():
self.position += 1
return NumberNode(int(token))
elif token.isalpha():
self.position += 1
return VariableNode(token)
elif token == '(':
self.position += 1
node = self.parse_expression()
if self.tokens[self.position] == ')':
self.position += 1
return node
else:
raise SyntaxError('Ожидалось ")"')
else:
raise SyntaxError(f'Неожиданный токен: {token}')

# Класс интерпретатора
class Interpreter:
def __init__(self):
self.context = {}

def execute(self, nodes):
for node in nodes:
node.evaluate(self.context)

# Пример кода на нашем мини-языке
code = """
x = 10
y = 20
result = x * y + (x - y) / 2
print result
"""

# Парсим и выполняем код
parser = Parser(code)
ast_nodes = parser.parse()
interpreter = Interpreter()
interpreter.execute(ast_nodes)


Описание кода:

1. Импортируем необходимые модули:
- re для разбивки кода на токены.
- abc для создания абстрактных классов.

2. Создаем абстрактный класс `ASTNode`:
- Определяет интерфейс для всех узлов синтаксического дерева.
- Метод evaluate выполняет узел в заданном контексте переменных.

3. Реализуем различные типы узлов:
- NumberNode для числовых значений.
- VariableNode для переменных.
- BinaryOperationNode для бинарных операций (`+`, -, *, `/`).
- AssignmentNode для операций присваивания.
- PrintNode для вывода значений на экран.

4. Создаем класс `Parser`:
- Инициализируется кодом и разбивает его на токены.
- Метод parse анализирует код и строит список узлов AST.
- Методы parse_statement, parse_assignment, parse_expression, parse_term, parse_factor реализуют разбор различных частей кода.

5. Создаем класс `Interpreter`:
- Содержит контекст переменных (`self.context`).
- Метод execute выполняет список узлов AST.

6. Пример кода на нашем мини-языке:
- Присваиваем значения переменным x и y.
- Вычисляем result с использованием арифметических операций и скобок.
- Выводим result на экран.

7. Парсим и выполняем код:
- Создаем экземпляр Parser и парсим код, получая AST.
- Создаем Interpreter и выполняем AST.

Как это работает:

- Парсинг кода:
- Код разбивается на токены (слова, числа, операторы).
- Парсер строит абстрактное синтаксическое дерево (AST), представляющее структуру кода.

- Выполнение кода:
- Интерпретатор проходит по узлам AST и выполняет их.
- Контекст переменных сохраняется в словаре self.context.
- При выполнении арифметических операций вычисляются значения с учетом текущего контекста.

- Пример выполнения:
- x = 10 — переменной x присваивается значение 10.
- y = 20 — переменной y присваивается значение 20.
- result = x * y + (x - y) / 2 — вычисляется выражение и результат сохраняется в result.
- print result — выводится значение переменной result.

Особенности ООП в коде:

- Инкапсуляция:
- Классы узлов AST скрывают детали реализации каждой операции.
- Контекст переменных хранится внутри интерпретатора.

- Наследование и полиморфизм:
- Все узлы AST наследуют от абстрактного класса ASTNode.
- Метод evaluate переопределяется в каждом узле для специфического поведения.

- Абстрактные классы и методы:
- Использование ABC и @abstractmethod для определения интерфейса узлов.

Как запустить код:

1. Убедитесь, что установлен Python 3.x.

2. Сохраните код в файле с расширением `.py`, например, `mini_interpreter.py`.

3. Запустите скрипт:


python mini_interpreter.py


Ожидаемый результат:

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

Возможные улучшения:
- Добавить поддержку дополнительных операторов и функций:
- Операторы сравнения (`==`, !=, >, `<`).
- Логические операторы (`and`, or, `not`).
- Встроенные функции (например, sqrt, sin, `cos`).

- Реализовать управление потоком исполнения:
- Условные операторы (`if`, `else`).
- Циклы (`while`, `for`).

- Добавить обработку ошибок и исключений:
- Улучшить сообщения об ошибках синтаксиса и выполнения.

- Оптимизировать парсер:
- Использовать более продвинутые техники парсинга (например, грамматики и генераторы парсеров).

Вывод:

Этот пример демонстрирует, как с помощью ООП можно реализовать интерпретатор простого языка программирования. Такой подход позволяет легко расширять функциональность языка, добавляя новые конструкции и возможности. Это отличное упражнение для понимания принципов парсинга, интерпретации и выполнения кода, а также для практики в использовании ООП в Python.

Подпишись 👉🏻 @KodduuPython 🤖
Давайте создадим сложную программу на Python без использования ООП. Мы реализуем алгоритм Быстрого преобразования Фурье (FFT) и применим его для анализа звукового сигнала. Также визуализируем результаты с помощью matplotlib.


import numpy as np
import matplotlib.pyplot as plt
import wave
import sys

# Чтение звукового файла
def read_wave(filename):
with wave.open(filename, 'r') as wf:
n_frames = wf.getnframes()
frames = wf.readframes(n_frames)
signal = np.frombuffer(frames, dtype=np.int16)
framerate = wf.getframerate()
return signal, framerate

# Реализация алгоритма FFT
def fft(signal):
N = len(signal)
if N <= 1:
return signal
even = fft(signal[::2])
odd = fft(signal[1::2])
T = [np.exp(-2j * np.pi * k / N) * odd[k % (N//2)] for k in range(N//2)]
return [even[k] + T[k] for k in range(N//2)] + \
[even[k] - T[k] for k in range(N//2)]

# Пример использования
if __name__ == "__main__":
# Проверка аргументов командной строки
if len(sys.argv) < 2:
print("Использование: python fft.py <filename.wav>")
sys.exit(1)

filename = sys.argv[1]
signal, framerate = read_wave(filename)
N = 1024 # Размер выборки

# Обрезаем или дополняем сигнал до нужного размера
if len(signal) < N:
signal = np.pad(signal, (0, N - len(signal)), 'constant')
else:
signal = signal[:N]

# Выполняем FFT
spectrum = fft(signal)

# Получаем частоты
freqs = np.fft.fftfreq(N, d=1/framerate)

# Визуализация
plt.figure(figsize=(12, 6))

plt.subplot(121)
plt.plot(np.arange(N), signal)
plt.title('Временная область')
plt.xlabel('Образцы')
plt.ylabel('Амплитуда')

plt.subplot(122)
plt.plot(freqs[:N//2], np.abs(spectrum[:N//2]) * 2 / N)
plt.title('Частотная область')
plt.xlabel('Частота (Гц)')
plt.ylabel('Амплитуда')

plt.tight_layout()
plt.show()


Описание кода:

1. Импортируем необходимые модули:
- numpy для работы с массивами и математическими операциями.
- matplotlib.pyplot для визуализации.
- wave для работы с WAV-файлами.
- sys для доступа к аргументам командной строки.

2. Функция `read_wave`:
- Читает WAV-файл и возвращает сигнал и частоту дискретизации.

3. Функция `fft`:
- Рекурсивная реализация алгоритма Быстрого преобразования Фурье.
- Разделяет сигнал на четные и нечетные части и рекурсивно вычисляет их преобразования.
- Использует формулу "бабочки" для комбинирования результатов.

4. Основная часть программы:
- Проверяет наличие аргумента командной строки (имя файла).
- Читает звуковой сигнал из файла.
- Обрезает или дополняет сигнал до размера N (1024 образца).
- Выполняет FFT и получает спектр сигнала.
- Вычисляет частоты, соответствующие каждому значению спектра.
- Визуализирует исходный сигнал и его спектр.

5. Визуализация:
- Первая графика показывает сигнал во временной области.
- Вторая графика показывает амплитудный спектр сигнала в частотной области.

Как использовать:

1. Подготовьте WAV-файл:
- Убедитесь, что у вас есть звуковой файл в формате WAV для анализа.

2. Запустите скрипт:
- В командной строке выполните:

python fft.py your_audio_file.wav

- Замените your_audio_file.wav на имя вашего файла.

3. Просмотрите результаты:
- Появится окно с двумя графиками, отображающими сигнал и его спектр.

Примечания:

- Размер выборки `N`:
- Выбранное значение N = 1024 обеспечивает баланс между разрешением по времени и частоте.
- Можно изменить N в коде для анализа большего или меньшего фрагмента сигнала.

- Ограничения:
- Данная реализация FFT не оптимизирована и предназначена для образовательных целей.
- Для больших сигналов рекомендуется использовать numpy.fft.fft для производительности.

Подпишись 👉🏻 @KodduuPython 🤖
🎉1
Собрали программу для тех кому некогда, но нужно быстро пройти весь Python + DataScience 👈👈👈

Подпишись 👉🏻 @KodduuPython 🤖
4🔥1
Создадим интересную программу на Python, которая реализует генетический алгоритм для решения задачи оптимизации — нахождения минимума функции. Мы применим генетический алгоритм для минимизации функции Растригина, известной своей сложностью и множеством локальных минимумов.


import numpy as np
import matplotlib.pyplot as plt

# Определение функции Растригина
def rastrigin(x):
A = 10
return A * len(x) + sum([(xi**2 - A * np.cos(2 * np.pi * xi)) for xi in x])

# Параметры генетического алгоритма
POP_SIZE = 100 # Размер популяции
GENES = 2 # Количество генов (размерность задачи)
GENERATIONS = 50 # Количество поколений
MUTATION_RATE = 0.1 # Вероятность мутации

# Инициализация популяции
population = [np.random.uniform(-5.12, 5.12, GENES) for _ in range(POP_SIZE)]

# Эволюционный цикл
best_scores = []
for generation in range(GENERATIONS):
# Оценка приспособленности
fitness = [ -rastrigin(individual) for individual in population]

# Сохранение лучшего результата
best_scores.append(-min(fitness))

# Селекция
parents = [population[i] for i in np.argsort(fitness)[-POP_SIZE//2:]]

# Кроссовер
offspring = []
for _ in range(POP_SIZE//2):
parent1, parent2 = np.random.choice(parents, 2, replace=False)
crossover_point = np.random.randint(1, GENES)
child = np.concatenate([parent1[:crossover_point], parent2[crossover_point:]])
offspring.append(child)

# Мутация
for individual in offspring:
if np.random.rand() < MUTATION_RATE:
mutation_point = np.random.randint(GENES)
individual[mutation_point] += np.random.uniform(-1, 1)
individual[mutation_point] = np.clip(individual[mutation_point], -5.12, 5.12)

# Обновление популяции
population = parents + offspring

# Вывод результатов
best_individual = population[np.argmax(fitness)]
print("Лучшее решение:", best_individual)
print("Значение функции:", rastrigin(best_individual))

# Визуализация сходимости
plt.plot(best_scores)
plt.title('Сходимость генетического алгоритма')
plt.xlabel('Поколение')
plt.ylabel('Значение функции')
plt.show()


Описание кода:

1. Функция Растригина (`rastrigin`): сложная функция с множеством локальных минимумов, часто используемая для тестирования алгоритмов оптимизации.

2. Параметры алгоритма:
- POP_SIZE: количество особей в популяции.
- GENES: количество генов в хромосоме (размерность задачи).
- GENERATIONS: количество поколений для эволюции.
- MUTATION_RATE: вероятность мутации потомка.

3. Инициализация популяции: создаем начальную популяцию со случайными значениями в диапазоне [-5.12, 5.12].

4. Эволюционный цикл:
- Оценка приспособленности: вычисляем отрицательное значение функции Растригина для каждой особи (так как мы минимизируем функцию).
- Сохранение лучшего результата: отслеживаем наилучшее значение функции в каждом поколении.
- Селекция: выбираем лучших особей (с наилучшей приспособленностью) для создания потомства.
- Кроссовер: комбинируем гены пар родителей для создания новых особей.
- Мутация: вносим случайные изменения в гены потомков с определенной вероятностью.
- Обновление популяции: формируем новую популяцию из родителей и потомков.

5. Вывод результатов:
- Находим и выводим лучшую особь после всех поколений.
- Выводим соответствующее значение функции Растригина.
- Строим график сходимости значений функции по поколениям.

Как запустить код:

1. Установите необходимые библиотеки:


pip install numpy matplotlib


2. Сохраните код в файл, например `genetic_algorithm.py`.

3. Запустите скрипт:


python genetic_algorithm.py


Ожидаемый результат:

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

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

Заключение:

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

Подпишись 👉🏻 @KodduuPython 🤖
🔥2
Вот код, который создает интерактивную визуализацию, где точки движутся, образуя различные узоры. Этот код создает окно с анимацией точек, которые перемещаются по кругу и создают интересные узоры. К тому же, это легко настраивается для других геометрических форм.


import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

# Настройка параметров
num_points = 50
radius = 10

# Создаем исходные координаты точек на окружности
angles = np.linspace(0, 2 * np.pi, num_points, endpoint=False)
x_coords = radius * np.cos(angles)
y_coords = radius * np.sin(angles)

fig, ax = plt.subplots()
ax.set_aspect('equal', 'box')
scatter = ax.scatter(x_coords, y_coords, s=50, color='blue')
ax.set_xlim(-radius * 1.2, radius * 1.2)
ax.set_ylim(-radius * 1.2, radius * 1.2)

def update(frame):
# Обновление углов и координат для движения точек
new_angles = angles + 0.1 * frame
x_coords = radius * np.cos(new_angles + frame * 0.01)
y_coords = radius * np.sin(new_angles + frame * 0.01)
scatter.set_offsets(np.c_[x_coords, y_coords])
return scatter,

ani = animation.FuncAnimation(fig, update, frames=200, interval=50, blit=True)
plt.show()


Этот код:

- Создает 50 точек, которые расположены по кругу.
- С каждой итерацией они слегка смещаются, создавая эффект "волны" и "размытости".
- Получается гипнотическая анимация, которую можно менять, изменяя радиус, количество точек или скорость.

Подпишись 👉🏻 @KodduuPython 🤖
Попробуем создать более сложную анимацию, в которой точки будут двигаться по нелинейным траекториям, создавая узоры. Этот пример использует траектории, которые зависят от синусоидальных и косинусоидальных функций с переменным радиусом, чтобы добавить нелинейные колебания.


import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

# Настройка параметров
num_points = 50
base_radius = 10

# Создаем исходные углы для точек
angles = np.linspace(0, 2 * np.pi, num_points, endpoint=False)

fig, ax = plt.subplots()
ax.set_aspect('equal', 'box')
scatter = ax.scatter([], [], s=50, color='purple')
ax.set_xlim(-base_radius * 1.5, base_radius * 1.5)
ax.set_ylim(-base_radius * 1.5, base_radius * 1.5)

# Обновление координат для нелинейных траекторий
def update(frame):
# Нелинейный радиус с добавлением синусоидальной амплитуды
radius = base_radius + 3 * np.sin(frame * 0.05)
x_coords = radius * np.cos(angles + 0.05 * frame) + 2 * np.sin(angles * 3 + frame * 0.1)
y_coords = radius * np.sin(angles + 0.05 * frame) + 2 * np.cos(angles * 4 + frame * 0.1)

scatter.set_offsets(np.c_[x_coords, y_coords])
return scatter,

# Настройка анимации
ani = animation.FuncAnimation(fig, update, frames=400, interval=50, blit=True)
plt.show()


### Как это работает:
- Радиус каждой точки зависит от времени и изменяется по синусоиде, что делает движение точек более динамичным.
- Траектория движения каждой точки - это комбинация нескольких синусоидальных и косинусоидальных функций, что создает уникальные узоры на каждом кадре.
- Частотные множители для angles (например, 3 и 4 внутри x_coords и `y_coords`) добавляют колебания по окружности, что создает сложные, закрученные траектории.

Подпишись 👉🏻 @KodduuPython 🤖
This media is not supported in your browser
VIEW IN TELEGRAM
Код анимации выше 👆👆👆

Подпишись 👉🏻 @KodduuPython 🤖
👍3
Собрали программу Junior FullStack Developer and Data Scientist, включает 3 наших самых компактных курса, можно пройти за 2-3 викэнда. В программе вся база по Python + вся база по JavaScript + вся база по Data Science в Python 😎

Подпишись 👉🏻 @KodduuPython 🤖