Pull to refresh
143.25
Skillfactory
Учим работать в IT на курсах и в магистратурах

Как писать питонический код: три рекомендации и три книги

Reading time15 min
Views38K
Original author: David Amos

Новички в Python часто спрашивают, как писать питонический код. Проблема — расплывчатое определение слова "питонический". Подробным материалом, в котором вы найдёте ответы на вопрос выше и три полезные книги, делимся к старту курса по Fullstack-разработке на Python.


Что значит «питонический»?

Python более 30 лет. За это время накоплен огромный опыт его применения в самых разных задачах. Этот опыт обобщался, и возникали лучшие практики, которые обычно называют «питоническим» кодом.

Философия Python раскрывается в The Zen of Python Тима Питерса, доступной в любом Python по команде import this в REPL:

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Начинающих в Python больше всего раздражает красота Zen of Python. В Zen передаётся дух того, что значит «питонический» — и без явных советов. Вот первый принцип дзена Python: «Красивое лучше, чем уродливое». Согласен на 100%! Но как сделать красивым мой некрасивый код? Что это вообще такое — «красивый код»?

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

Каким же будет определение слова «питонический»? Лучшее найденное мной определение взято из ответа на вопрос «Что означает «питонический» В этом ответе питонический код описывается так:

Код, где правилен не только синтаксис, но соблюдаются соглашения сообщества Python, а язык используется так, как он должен использоваться.

Из этого делаем два ключевых вывода:

  1. Слово «питонический» связано скорее со стилем, чем с синтаксисом. Хотя идиомы Python часто имеют последствия за рамками чисто стилистического выбора, в том числе повышение производительности кода.

  2. То, что считается «питоническим», определяется сообществом Python.

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

1. Подружитесь с PEP8

PEP8 — это официальное руководство по стилю кода Python. PEP расшифровывается как Python Enhancement Proposal («Предложение по улучшению Python»). Это документы, предлагающие новые особенности языка. Они образуют официальную документацию особенности языка, принятие или отклонение которой обсуждается в сообществе Python. Следование PEP8 не сделает код абсолютно «питоническим», но способствует узнаваемости кода для многих Python-разработчиков.

В PEP8 решаются вопросы, связанные с символами пробелов. Например, использование четырёх пробелов для отступа вместо символа табуляции или максимальной длиной строки: согласно PEP8, это 79 символов, хотя данная рекомендация, вероятно, самая игнорируемая.

Первое, что стоит усвоить из PEP8 новичкам, — это рекомендации и соглашения по именованию. Например, следует писать имена функций и переменных в нижнем регистре и с подчёркиваниями между словами lowercase_with_underscores:

# Correct
seconds_per_hour = 3600

# Incorrect
secondsperhour = 3600
secondsPerHour = 3600

Имена классов следует писать с прописными первыми буквами слов и без пробелов, вот так: CapitalizedWords:

# Correct
class SomeThing:
    pass

# Incorrect
class something:
    pass

class some_thing:
    pass

Константы записывайте в верхнем регистре и с подчёркиваниями между словами: UPPER_CASE_WITH_UNDERSCORES:

# Correct
PLANCK_CONSTANT = 6.62607015e-34

# Incorrect
planck_constant = 6.6260715e-34
planckConstant = 6.6260715e-34

В PEP8 изложены рекомендации по пробелам: как использовать их с операторами, аргументами и именами параметров функций и для разбиения длинных строк. Хотя эти рекомендации можно освоить, годами практикуясь в чтении и написании совместимого с PEP8 кода, многое всё равно пришлось бы запоминать.

Запоминать все соглашения PEP8 не нужно: найти и устранить проблемы PEP8 в коде могут помочь такие инструменты, как flake8. Установите flake8 с помощью pip:

# Linux/macOS
$ python3 -m pip install flake8

# Windows
$ python -m pip install flake8

flake8 можно использовать как приложение командной строки для просмотра файла Python на предмет нарушений стиля. Допустим, есть файл myscript.py с таким кодом:

def add( x, y ):
    return x+y

num1=1
num2=2
print( add(num1,num2) )

При запуске на этом коде flake8 сообщает, как и где именно нарушается стиль:

$ flake8 myscript.py
myscript.py:1:9: E201 whitespace after '('
myscript.py:1:11: E231 missing whitespace after ','
myscript.py:1:13: E202 whitespace before ')'
myscript.py:4:1: E305 expected 2 blank lines after class or function definition, found 1
myscript.py:4:5: E225 missing whitespace around operator
myscript.py:5:5: E225 missing whitespace around operator
myscript.py:6:7: E201 whitespace after '('
myscript.py:6:16: E231 missing whitespace after ','
myscript.py:6:22: E202 whitespace before ')'

В каждой выводимой строке flake8 сообщается, в каком файле и в какой строке проблема, в каком столбце строки начинается ошибка, номер ошибки и её описание. Используйте эти обозначения, flake8 можно настроить на игнорирование конкретных ошибок:

Как читать вывод flake8
Как читать вывод flake8

Для проверки качества кода с помощью flake8 вы даже можете настроить редакторы, например VS Code. Пока вы пишете код, он постоянно проверяется на нарушения PEP8. Когда обнаруживается проблема, во flake8 под частью кода с ошибкой появляется красная волнистая линия, найденные ошибки можно увидеть во вкладке встроенного терминала Problems:

Проверки flake8 в Visual Studio Code
Проверки flake8 в Visual Studio Code

flake8 — отличный инструмент для поиска связанных с нарушением PEP8 ошибок, но исправлять их придётся вручную. А значит, будет много работы. К счастью, весь процесс автоматизируемый. Автоматически форматировать код согласно PEP можно с помощью инструмента под названием Black

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

Установите black c помощью pip:

# Linux/macOS
$ python3 -m pip install black

# Windows
$ python -m pip install black

После установки командой black --check можно посмотреть, будут ли black изменять файл:

$ black --check myscript.py
would reformat myscript.py

Oh no! 💥 💔 💥
1 file would be reformatted.

Чтобы увидеть разницу после изменений, используйте флаг --diff:

$ black --diff myscript.py
--- myscript.py	2022-03-15 21:27:20.674809 +0000
+++ myscript.py	2022-03-15 21:28:27.357107 +0000
@@ -1,6 +1,7 @@
-def add( x, y ):
-    return x+y
+def add(x, y):
+    return x + y

-num1=1
-num2=2
-print( add(num1,num2) )
+
+num1 = 1
+num2 = 2
+print(add(num1, num2))
would reformat myscript.py

All done! ✨ 🍰 ✨
1 file would be reformatted.

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

$ black myscript.py
reformatted myscript.py

All done! ✨ 🍰 ✨
1 file reformatted.

# Show the formatted file
$ cat myscript.py
def add(x, y):
    return x + y

num1 = 1
num2 = 2
print(add(num1, num2))

Чтобы проверить совместимость с PEP8, снова запустите flake8 и посмотрите на вывод:

# No output from flake8 so everything is good!
$ flake8 myscript.py

При работе с black следует иметь в виду, что максимальная длина строки по умолчанию в нём — это 88 символов. Это противоречит рекомендации PEP8 о 79 символах, поэтому при использовании black в отчёте flake8 вы увидете ошибки о длине строки. 

Многие разработчики Python используют 88 знаков вместо 79, а некоторые — строки ещё длиннее. Можно настроить black на 79 символов, или flake8 — на строки большей длины.

Важно помнить, что PEP8 — это лишь набор рекомендаций, хотя многие программисты на Python относятся к ним серьёзно. PEP8 не применяется в обязательном порядке. Если в нём есть что-то, с чем вы категорически не согласны, вы вправе это игнорировать! Если же вы хотите строго придерживаться PEP8, инструменты типа flake8 и black сильно облегчат вам жизнь.

2. Избегайте циклов в стиле C

В таких языках, как C или C++, отслеживание индексной переменной при переборе массива — обычное дело. Поэтому программисты, которые перешли на Python из C или C++, при выводе элементов списка нередко пишут:

>>> names = ["JL", "Raffi", "Agnes", "Rios", "Elnor"]

>>> # Using a `while` loop
>>> i = 0
>>> while i < len(names):
...     print(names[i])
...     i += 1
JL
Raffi
Agnes
Rios
Elnor

>>> # Using a `for` loop
>>> for i in range(len(names)):
...     print(names[i])
JL
Raffi
Agnes
Rios
Elnor

Вместо итерации можно перебрать все элементы списка сразу:

>>> for name in names:
...     print(name)
JL
Raffi
Agnes
Rios
Elnor

Этим вторая рекомендация не ограничивается: она намного глубже простого перебора элементов списка. Такие идиомы Python, как списковые включения, встроенные функции (min(), max() и sum()) и методы объектов, может помочь вывести ваш код на новый уровень.

Отдавайте предпочтение списковым включениям, а не простым циклам for

Обработка элементов массива и сохранение результатов в новом — типичная задача программирования. Допустим, нужно преобразовать список чисел в список их квадратов. Избегая циклов в стиле C, можно написать:

>>> nums = [1, 2, 3, 4, 5]

>>> squares = []
>>> for num in nums:
...     squares.append(num ** 2)
...
>>> squares
[1, 4, 9, 16, 25]

Но более питонически применить списковое включение:

>>> squares = [num ** 2 for num in nums]  # <-- List comprehension
>>> squares
[1, 4, 9, 16, 25]

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

Вот так я обычно пишу списковые включения:

  1. Начинаю с создания литерала пустого списка:
    [].

  2. Первым в списковое включение помещаю то, что обычно идёт в метод .append()при создании списка с помощью цикла for:
    [num ** 2].

  3. И, наконец, помещаю в конец списка заголовок цикла for:
    [num ** 2 for num in nums].

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

Используйте встроенные функции, такие как min(), max() и sum()

Ещё одна типичная задача программирования — это поиск минимального или максимального значения в массиве чисел. Найти наименьшее число в списке можно с помощью for:

>>> nums = [10, 21, 7, -2, -5, 13]

>>> min_value = nums[0]
>>> for num in nums[1:]:
...     if num < min_value:
...         min_value = num
...
>>> min_value
-5

Но более «питонически» применять встроенную функцию min():

>>> min(nums)
-5

То же касается нахождения наибольшего значения в списке: вместо цикла применяется встроенная функция max():

>>> max(nums)
21

Чтобы найти сумму чисел списка, написать цикл for можно, но более питонически воспользоваться sum():

>>> # Not Pythonic: Use a `for` loop
>>> sum_of_nums = 0

>>> for num in nums:
...     sum_of_nums += num
...
>>> sum_of_nums
44

>>> # Pythonic: Use `sum()`
>>> sum(nums)
44

Также sum() полезна при подсчёте количества элементов списка, для которых выполняется некое условие. Например, вот цикл for для подсчёта числа начинающихся с буквы A строк списка:

>>> capitals = ["Atlanta", "Houston", "Denver", "Augusta"]

>>> count_a_capitals = 0
>>> for capital in capitals:
...     if capital.startswith("A"):
...         count_a_capitals += 1
...
>>> count_a_capitals
2

Функция sum() со списковым включением сокращает цикл for до одной строки:

>>> sum([capital.startswith("A") for capital in capitals])
2

Красота! Но ещё более питонической эту строку сделает замена спискового включения на выражение-генератор. Убираем скобки списка:

>>> sum(capital.startswith("A") for capital in capitals)
2

Как именно работает код? И списковое включение, и выражение-генератор возвращают итерируемый объект со значением True, если строка в списке capitals начинается с буквы A, и False — если это не так:

>>> [capital.startswith("A") for capital in capitals]
[True, False, False, True]

В Python True и False — это завуалированные целые числа. True равно 1, а False — 0:

>>> isinstance(True, int)
True

>>> True == 1
True

>>> isinstance(False, int)
True

>>> False == 0
True

Когда в sum() передаётся списковое включение или выражение-генератор, значения True и False считаются 1 и 0 соответственно. Всего два значения True и два False, поэтому сумма равна 2.

Использование sum() для подсчёта числа удовлетворяющих какому-то условию элементов списка подчёркивает важность понятия «питонический». Я нахожу такое применение sum() очень питонически. Ведь с sum() используется несколько особенностей этого языка и создаётся, на мой взгляд, лаконичный и удобный для восприятия код. Но, возможно, не каждый разработчик на Python со мной согласится.

Можно было бы возразить, что в этом примере нарушается один из принципов Zen of Python: «Явное лучше неявного». Ведь не очевидно, что True и False — целые числа и что sum() вообще должна работать со списком значений True и False. Чтобы освоить это применение sum(), нужно глубоко понимать встроенные типы Python.

Узнать больше о True и False как целых числах, а также о других неожиданных фактах о числах в Python можно из статьи 3 Things You Might Not Know About Numbers in Python («3 факта о числах в Python, которых вы могли не знать»).

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

3. Используйте правильную структуру данных

Большая роль при написании чистого, питонического кода для конкретной задачи отводится выбору подходящей структуры данных. Python называют языком «с батарейками в комплекте». Некоторые батарейки из комплекта Python — это эффективные, готовые к применению структуры данных.

Используйте словари для быстрого поиска

Вот CSV-файл clients.csv с данными по клиентам:

first_name,last_name,email,phone
Manuel,Wilson,mwilson@example.net,757-942-0588
Stephanie,Gonzales,sellis@example.com,385-474-4769
Cory,Ali,coryali17@example.net,810-361-3885
Adam,Soto,adams23@example.com,724-603-5463

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

Используя объект DictReader из модуля csv, можно прочитать каждую строку файла как словарь:

>>> import csv

>>> with open("clients.csv", "r") as csvfile:
...     clients = list(csv.DictReader(csvfile))
...
>>> clients
[{'first_name': 'Manuel', 'last_name': 'Wilson', 'email': 'mwilson@example.net', 'phone': '757-942-0588'},
{'first_name': 'Stephanie', 'last_name': 'Gonzales', 'email': 'sellis@example.com', 'phone': '385-474-4769'},
{'first_name': 'Cory', 'last_name': 'Ali', 'email': 'coryali17@example.net', 'phone': '810-361-3885'},
{'first_name': 'Adam', 'last_name': 'Soto', 'email': 'adams23@example.com', 'phone': '724-603-5463'}]

clients — это список словарей. Поэтому, чтобы найти клиента по адресу почты, например sellis@example.com, нужно перебрать список и сравнить почту каждого клиента с целевой почтой, пока не будет найден нужный клиент:

>>> target = "sellis@example.com"
>>> phone = None

>>> for client in clients:
...     if client["email"] == target:
...         phone = client["phone"]
...         break
...
>>> print(phone)
385-474-4769

Но есть проблема: перебор списка клиентов неэффективен. Если в файле много клиентов, на поиск клиента с совпадающим адресом почты у программы может уйти много времени. А сколько теряется времени, если такие проверки проводятся часто!

Более питонически сопоставить клиентов с их почтами, а не хранить клиентов в списке. Для этого отлично подойдёт словарное включение:

>>> with open("clients.csv", "r") as csvfile:
...     # Use a `dict` comprehension instead of a `list`
...     clients = {row["email"]: row["phone"] for row in csv.DictReader(csvfile)}
... 
>>> clients
{'mwilson@example.net': '757-942-0588', 'sellis@example.com': '385-474-4769',
'coryali17@example.net': '810-361-3885', 'adams23@example.com': '724-603-5463'}

Словарные включения очень похожи на списковые включения:

  1. Я начинаю с создания пустого словаря:
    {}.

  2. Затем помещаю туда разделённую двоеточием пару «ключ — значение»:
    {row["email"]: row["phone"]}.

  3. И пишу выражение с for, которое перебирает все строки в CSV:
    {row["email"]: row["phone"] for row in csv.DictReader(csvfile)}.

Вот это словарное включение, преобразованное в цикл for:

>>> clients = {}
>>> with open("clients.csv", "r") as csvfile:
...     for row in csv.DictReader(csvfile):
...         clients[row["email"]] = row["phone"]

С этим словарём clients вы можете найти телефон клиента по его почте без циклов:

>>> target = "sellis@example.com"
>>> clients[target]
385-474-4769

Этот код не только короче, но и намного эффективнее перебора списка циклом. Но есть проблема: если в clients нет клиента с искомой почтой, поднимается ошибка KeyError:

>>> clients["tsanchez@example.com"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'tsanchez@example.com'

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

>>> target = "tsanchez@example.com"
>>> try:
...     phone = clients[target]
... except KeyError:
...     phone = None
...
>>> print(phone)
None

Но более питонически применять метод словаря .get(). Если пара с ключом существует, этот метод возвращает значение пары, иначе возвращается None:

>>> clients.get("sellis@example.com")
'385-474-4769'

Сравним решения выше:

import csv

target = "sellis@example.com"
phone = None

# Un-Pythonic: loop over a list
with open("clients.csv", "r") as csvfile:
    clients = list(csv.DictReader(csvfile))

for client in clients:
    if client["email"] == target:
        phone = client["phone"]
        break

print(phone)

# Pythonic: lookup in a dictionary
with open("clients.csv", "r") as csvfile:
    clients = {row["email"]: row["phone"] for row in csv.DictReader(csvfile)}

phone = clients.get(target)
print(phone)

Питонический код короче, эффективнее и не менее удобен для восприятия.

Используйте операции над множествами

Множества — это настолько недооценённая структура данных Python, что даже разработчики среднего уровня склонны их игнорировать, упуская возможности. Пожалуй, самое известное применение множеств в Python — это удаление повторяющихся в списке значений:

>>> nums = [1, 3, 2, 3, 1, 2, 3, 1, 2]
>>> unique_nums = list(set(nums))
>>> unique_nums
[1, 2, 3]

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

Вот придуманный, но реалистичный пример. У владельца магазина есть CSV-файл клиентов с адресами их почты. Снова возьмём файл clients.csv. Есть также CSV-файл заказов за последний месяц orders.csv, тоже с адресами почты:

date,email,items_ordered
2022/03/01,adams23@example.net,2
2022/03/04,sellis@example.com,3
2022/03/07,adams23@example.net,1

Владельцу магазина нужно отправить купон на скидку каждому клиенту, который в прошлом месяце ничего не заказывал. Для этого он может считать адреса почты из файлов clients.csv и orders.csv и отфильтровать их списковым включением:

>>> import csv

>>> # Create a list of all client emails
>>> with open("clients.csv", "r") as clients_csv:
...     client_emails = [row["email"] for row in csv.DictReader(clients_csv)]
...

>>> # Create a list of emails from orders
>>> with open("orders.csv") as orders_csv:
...     order_emails = [row["email"] for row in csv.DictReader(orders_csv)]
...

>>> # Use a list comprehension to filter the clients emails
>>> coupon_emails = [email for email in clients_emails if email not in order_emails]
>>> coupon_emails
["mwilson@example.net", "coryali17@example.net"]

Код нормальный и выглядит вполне питонически. Но что, если каждый месяц клиентов и заказов будут миллионы? Тогда при фильтрации почты и определении, каким клиентам отправлять купоны, потребуется перебор всего списка client_emails. А если в файлах client.csv и orders.csv есть повторяющиеся строки? Бывает и такое.

Более питонически считать адреса почты клиентов и заказов в множествах и отфильтровать множества почтовых адресов клиентов оператором разности множеств:

>>> import csv

>>> # Create a set of all client emails using a set comprehension
>>> with open("clients.csv", "r") as clients_csv:
...     client_emails = {row["email"] for row in csv.DictReader(clients_csv)}
...

>>> # Create a set of emails frp, orders using a set comprehension
>>> with open("orders.csv", "r") as orders_csv:
...     order_emails = {row["email"] for row in csv.DictReader(orders_csv)}
...

>>> # Filter the client emails using set difference
>>> coupon_emails = client_emails - order_emails
>>> coupon_emails
{"mwilson@example.net", "coryali17@example.net"}

Этот подход намного эффективнее предыдущего: адреса клиентов перебираются только один раз, а не два. Вот ещё одно преимущество: все повторы почтовых адресов из обоих CSV-файлов удаляются естественным образом.

Три книги, чтобы писать более питонический код

За один день писать чистый питонический код не научиться. Нужно изучить много примеров кода, пробовать писать собственный код и консультироваться с другими разработчиками Python. Чтобы облегчить вам задачу, я составил список из трёх книг, очень полезных для понимания питонического кода. Все они написаны для программистов уровня выше среднего или среднего.

Если вы новичок в Python (и тем более в программировании в целом), загляните в мою книгу Python Basics: A Practical Introduction to Python 3 («Основы Python: Практическое введение в Python 3»).

Python Tricks Дэна Бейдера

Короткая и приятная книга Дэна Бейдера Python Tricks: A Buffet of Awesome Python Features («Приёмы Python: набор потрясающих функций Python») — отличная отправная точка для начинающих и программистов, желающих больше узнать о том, как писать питонический код.

С Python Tricks вы изучите шаблоны написания чистого идиоматичного кода Python, лучшие практики для написания функций, эффективное применение функционала объектно-ориентированного программирования Python и многое другое.

Effective Python Бретта Слаткина

Effective Python («Эффективный Python») Бретта Слаткина — это первая книга, которую я прочитал после изучения синтаксиса Python. Она открыла мне глаза на возможности питонического кода.

В Effective Python содержится 90 способов улучшения кода Python. Одна только первая глава Python Thinking («Мыслить на Python») — это кладезь хитростей и приёмов, которые будут полезными даже для новичков, хотя остальная часть книги может быть для них трудной.

Fluent Python Лучано Рамальо

Если бы у меня была только одна книга о Python, это была бы книга Лучано Рамальо Fluent Python («Python. К вершинам мастерства»). Рамальо недавно обновил свою книгу до современного Python. Сейчас можно оформить предзаказ. Настоятельно рекомендую сделать это: первое издание устарело.

Полная практических примеров, чётко изложенная книга Fluent Python — отличное руководство для всех, кто хочет научиться писать питонический код. Но имейте в виду, что Fluent Python не предназначена для новичков. В предисловии к книге написано:

«Если вы только изучаете Python, эта книга будет трудной для вас». 

У вас может сложиться впечатление, что в каждом скрипте на Python должны использоваться специальные методы и приёмы метапрограммирования. Преждевременная абстракция так же плоха, как и преждевременная оптимизация.

Опытные программисты на Python извлекут из этой книги большую пользу.

А мы поможем вам прокачать скиллы или с самого начала освоить профессию в IT, актуальную в любое время:

Выбрать другую востребованную профессию.

Краткий каталог курсов и профессий

Tags:
Hubs:
Total votes 14: ↑13 and ↓1+13
Comments7

Articles

Information

Website
www.skillfactory.ru
Registered
Founded
Employees
501–1,000 employees
Location
Россия
Representative
Skillfactory School