Как я выбирал скриптовый язык и остановился на Python
8 декабря 2015
Задачи в программировании бывают разными. Например, часто бывает нужно написать простой скрипт на 10 строк кода. Долгое время я использовал для таких задач Perl. Но, как показывает опыт, многие команды находят Perl сложным в изучении и использовании. Поэтому возникает вопрос поиска более лучшего скриптового языка. После долгих раздумий я пришел к выводу, что такой язык — это, скорее всего, Python, и в этой заметке будет рассказано, почему.
Отмечу, что для скриптовых языков существует огромная ниша. Фактически, во всех задачах, где скорость выполнения программы не очень важна, имеет смысл использовать скриптовые языки, так как скорость разработки на этих языках существенно выше. Во-первых, так происходит по той причине, что скриптовые языки изначально не претендуют на высокую производительность кода, что позволяет делать их более высокоуровневыми и выразительными. Во-вторых, скриптовые языки позволяют программисту видеть результат мгновенно, без перекомпиляции проекта, перезапуска веб-сервера, и так далее. Такого рода не требовательных к скорости перемножения матричек задач намного больше, чем может показаться на первый взгляд.
Например:
- Написание небольших скриптов в стиле «пропарсил логи, вывел статистику» и подобного рода;
- Разработка тестов — интеграционных, системных, нагрузочных, …;
- Всем нужен простой консольный калькулятор, а у скриптовых языков обычно есть REPL. Иногда нужно не просто складывать и умножать, а, например, отсортировать 1000 целых чисел. Это все тоже относится к «калькулятору»;
- В веб-стартапах важно быстро слепить прототип, налить на него трафика и посмотреть, понравилась ли идея пользователям. Если взлетит, находим 10% кода, которые тормозят по правилу 10:90, и переписываем их на Си. Смотри историю развития ВКонтакте, Facebook и прочих;
- Не стоит также забывать, что обычных, не хайлоад проектов, в мире очень много — всяких там бложиков, небольших интернет-магазинов и форумов. Кроме того, даже в хайлоаде может быть какой-то генератор статики или веб-админка, которой одновременно никогда не пользуется больше десяти человек;
- Кстати, по поводу хайлоада. Если Reddit подходит Python, GitHub’у подходит Ruby, а Facebook’у подходит PHP, то и вашему хайлоуду наверняка подойдет Python. Дело в том, что в типичном хайлоаде почти все время обработки запроса тратится на посылку запросов к кэшам и базам данных, и не имеет большого значения, на Python вы пишите или на чистом Си. Если перед приложением поставить Nginx или lighttpd, то вы без проблем сможете держать 10 000+ одновременных соединений. Польза же от легковесных процессов, как в Go или Erlang, становится не очень большой. Например, потому что количество потоков, реально работающих параллельно, ограничивается числом соединений, которое может держать РСУБД;
- Во многих приложениях (игровых движках, отладчиках, текстовых редакторах, …) часто нужен какой-то встраиваемых язык, чтобы пользователь мог описать некую кастомную логику;
- То, что исходники скриптов всегда видны, может быть важно с точки зрения безопасности. Хорошо знать, что устанавливаемое приложение не делает чего-нибудь лишнего. Ведь совсем не факт, что всякий бинарный пакет собран из тех же исходников, что лежат на GitHub;
Почему же на мой взгляд для таких задач идеально подходит Python и в меньше степени подходит Perl или, скажем, Ruby:
- Простой, единообразный, строгий синтаксис. Код либо отформатирован правильно, либо не запускается. При работе в коллективе из нескольких человек это действительно здорово. В отношении Perl и Ruby это не так;
- Предельно низкий порог вхождения, вполне себе боевой код можно начать писать за считанные часы;
- Огромное сообщество пользователей, а следовательно куча книг, куча ответов на StackOverflow, и так далее;
- Большая коллекция готовых библиотек практически на все случаи жизни. Многие библиотеки, такие, как Matplotlib, NumPy, SciPy, SymPy, Pandas, NLTK, а также TensorFlow, Keras и PyTorch не имеют достойных аналогов в других языках, как скриптовых, так и компилируемых;
- Строгая динамическая типизация. В Perl она нестрогая, то есть числа можно складывать со строками, что по моему опыту сильно усложняет поддержку кода;
- Сборка мусора не на подсчете ссылок, но и с полноценным GC для циклических ссылок. В Perl есть только подсчет ссылок;
- Все не примитивные типы передаются по ссылке, как в Java, что делает код намного более читаемым. В Perl нужно отличать данные от ссылок на данные;
- Есть возможность JIT-компиляции — PyPy, разрабатываемый в Dropbox’е Pyston, плюс Jython для компиляции под JVM. Аналогов для Perl мне не известно. Для Ruby JIT вроде есть, но мне неизвестно, в каком состоянии находятся эти проекты;
- Для Python существует мощная и бесплатная IDE — PyCharm. Кроме того, есть замечательный плагин к Sublime Text — Anaconda. Для Perl, насколько я знаю, полноценной IDE до сих пор не существует. Для Ruby есть RubyMine, но она распространяется только за деньги;
- В отличие от Perl из коробки есть поддержка работы с большими числами, с комплексными числами, есть нормальный ООП, нормальная многопоточность (с точностью до GIL в CPython; кстати, по большому счету наличие или отсутствие GIL определяет лишь то, какие приложения на языке будут работать быстрее, многопроцессные или многопоточные), нормальные исключения, нормальный boolean, контейнер set, не говоря уже о нормальной передаче аргументов функциям, REPL, генераторах (которые с ключевым словом yield), кортежах, генераторах списков, и пусть простом, но все-таки pattern matching’е;
- Есть type hints, которые сильно упрощают чтение кода. Плюс есть реализация gradual typing в лице MyPy;
- Есть много вакансий. Притом, есть основания полагать, что в соответствующих проектах не слишком сильный легаси, что не справедливо в отношении Perl. Плюс есть некое разнообразие в решаемых задачах. Ruby же практически всегда означает Ruby on Rails;
- Куча саксесс сторис (Google, Yahoo, Яндекс, Mail.ru, Dropbox, Bitbucket, Disqus, …) и известных проектов (Graphite, Mercurial, SCons, Meson, Ansible, Trac, Mailman, MoinMoin, Pelican, …). Кроме того, Python используется в Sublime Text, GNU Radio, Radare2, Hopper, bcc/eBPF, KiCad, Sigrok, FreeCAD и ряде других проектов в качестве встраиваемого языка;
Резюмируя вышесказанное — по сравнению с Perl язык Python по очень многим параметрам лучше просто в техническом плане. Также с уверенностью можно сказать, что Ruby не лучше Python. При этом то, что для Python есть бесплатная IDE от JetBrains, а также что ни в каких Яндексах с Mail.ru нет открытых вакансий для программистов на Ruby, сыграли решающую роль. К тому же, за всю жизнь я не написал ни одной строчки на Ruby, а Python как-никак когда-то приходилось немного трогать. Варианты же с JavaScript, PHP, Lua, TCL и прочими я не особо рассматривал. Под Linux из коробки нет соответствующих интерпретаторов, а root на машине не всегда имеется. И вообще, в мире *nix систем на этих языках не принято писать скрипты. Нельзя полностью исключать культурный фактор!
Идея с написанием скриптов на Scala была отвергнута по теме же соображением, плюс скрипты на Scala имеют такой недостаток, что они секунд 5 только запускаются, и лишь потом начинают делать что-то действительно полезное.
В заключение хотелось бы поделиться некоторыми поделками, которые я написал в процессе изучения (скорее даже вспоминания) Python.
Генератор шоунотов (см аналоги на Go, Rust и Kotlin):
import re
import sys
import urllib3
import threading
import queue
if len(sys.argv) < 2:
print("Usage: " + sys.argv[0] + " input.txt [workersnum=8]")
sys.exit(1)
fname = sys.argv[1]
http = urllib3.PoolManager()
taskq = queue.Queue()
resultq = queue.Queue()
taskdonelock = threading.Lock()
tasksdone = 0
taskstotal = 0
workersnum = 8
if len(sys.argv) >= 3:
workersnum = int(sys.argv[2])
def worker():
while True:
task = taskq.get()
if task is None:
break
(tasknum, line) = task
result = (tasknum, process_line(line))
resultq.put(result)
def process_line(line):
global tasksdone
m = re.search("""(?i)(https?\S+)""", line)
url = m.group(1)
title = "[NO TITLE]"
try:
res = http.request('GET', url)
body = res.data.decode('utf-8')
m = re.search("(?is)<title>(.*?)</title>", body)
title = m.group(1).strip()
title = re.sub(r'\s+', ' ', title)
except KeyboardInterrupt:
raise
except Exception as e:
title = "[FAILED: " + str(e) + "]"
tasknum = -1
with taskdonelock:
tasksdone += 1
tasknum = tasksdone
print("Done [" + str(tasknum) + "/" + str(taskstotal) + "] " + url)
return '<li><a href="' + url + '">' + title + '</a></li>'
with open(fname) as f:
for line in f:
task = (taskstotal, line)
taskq.put(task)
taskstotal += 1
for _ in range(0, workersnum):
taskq.put(None)
thread = threading.Thread(target=worker)
thread.start()
results = list(range(0, taskstotal))
for _ in range(0, taskstotal):
(i, res) = resultq.get()
results[i] = res
print("\n\n<ul>")
for res in results:
print(res)
print("</ul>")
Заливка картинок на ImageShack (аналог на Perl):
import re
import sys
import urllib3
import random
import os.path
if len(sys.argv) < 2:
print("Usage: " + sys.argv[0] + " <image>")
sys.exit(1)
fname = sys.argv[1]
url = 'http://imageshack.us/upload_api.php'
http = urllib3.PoolManager()
data = None
with open(fname, "rb") as f:
data = f.read()
basename = os.path.basename(fname)
fields = {
"key": "015EFMNVfe7f6f7e93cb4a7b0a41e19956ce59f8",
"Filedata": (basename, data)
}
res = http.request('POST', url, fields)
body = res.data.decode('utf-8')
m = re.search("(?is)<image_link>(.*?)</image_link>", body)
url = m.group(1).strip()
print(url)
Заливалка текстовых файлов на PasteBin (тут более подробно про requests):
import re
import sys
import requests
if len(sys.argv) < 2:
print("Usage: " + sys.argv[0] + " <file>")
sys.exit(1)
fname = sys.argv[1]
base_url = 'http://pastebin.com'
post_url = base_url + '/post.php'
headers = {}
headers['user-agent'] = u'Mozilla/5.0 (compatible; MSIE 9.0; ' + \
u'Windows NT 6.0; Trident/5.0; Trident/5.0)'
res = requests.get(base_url, headers = headers)
body = res.text
m = re.search("""(?is)name="csrf_token_post" value="(.*?)">""", body)
token = m.group(1).strip()
cookies = {
"__cfduid": res.cookies.get('__cfduid'),
"PHPSESSID": res.cookies.get('PHPSESSID')
}
paste_code = None
with open(fname, "rb") as f:
paste_code = f.read()
data = {
"csrf_token_post": token,
"paste_code": paste_code,
"submit_hidden": "submit_hidden",
"paste_format": "1",
"paste_expire_date": "N",
"paste_private": "0",
"paste_name": ""
}
res = requests.post(post_url, data = data, headers = headers,
cookies = cookies)
print(base_url + "/raw/" + res.url.split("/")[-1])
Посылка SMS через Infobip (см также посылку SMS на Scala через Plivo):
import sys
import json
import urllib3
# change this!
source = 'source'
login = 'login'
password = 'Pa$$w0rd'
if len(sys.argv) < 3:
print("Usage: " + sys.argv[0] + " <phone> <text>")
sys.exit(1)
phone = sys.argv[1]
text = sys.argv[2]
http = urllib3.PoolManager()
body = json.dumps({'from': source, 'to': phone, 'text': text})
headers = urllib3.make_headers(basic_auth = login + ':' + password)
headers['content-type'] = u'application/json'
headers['accept'] = u'application/json'
# see http://dev.infobip.com/docs/send-single-sms
res = http.urlopen('POST', 'https://api.infobip.com/sms/1/text/single',
headers = headers, body = body)
if res.status != 200:
print("ERROR: HTTP status = " + str(res.status))
sys.exit(1)
doc = json.loads(res.data.decode('utf-8'))
status = doc['messages'][0]['status']
if status['groupId'] != 1:
print("ERROR: " + status['description'])
sys.exit(1)
print("OK")
Скрипт mostviewed.py (аналог вот этой программы на Scala):
import sys
import urllib3
import re
http = urllib3.PoolManager()
headers = urllib3.make_headers()
headers['user-agent'] = u'Mozilla/5.0 (compatible; MSIE 9.0; ' + \
u'Windows NT 6.0; Trident/5.0; Trident/5.0)'
def get_content(url):
res = http.urlopen("GET", url, headers = headers)
if res.status != 200:
raise Exception("res.status == " + str(res.status) +
" for url " + url)
return res.data.decode("utf-8")
def get_stat(domain, date):
stat_url = "http://www.liveinternet.ru/stat/" + domain + \
"/pages.html?date=" + date + \
"&period=month&total=yes&per_page=100"
data = get_content(stat_url)
http_home = "http://" + domain + "/"
https_home = "http://" + domain + "/"
re_str = """(?s)for="id_\d+"><a href="([^"]+)"[^>]*>.*?""" + \
"""<td>([\d,]+)</td>"""
for m in re.finditer(re_str, data):
[url, count] = [ m.group(x) for x in [1,2] ]
if url == http_home or url == https_home:
continue
yield ( url, int(count.replace(",", "")) )
def get_title(url):
data = get_content(url)
m = re.search("<h2>(.*?)</h2>", data)
return m.group(1)
def get_ending(count):
rem = count % 100
if 5 <= rem and rem <= 20:
return "ов"
rem = count % 10
if rem == 1:
return ""
if 2 <= rem and rem <= 4:
return "а"
return "ов"
if len(sys.argv) < 4:
print("Usage: " + sys.argv[0] + " <domain> <date> <number>")
sys.exit(1)
[domain, date, number] = sys.argv[1:4]
number = int(number)
all_stat = list(get_stat(domain, date))
print("<ul>")
for (url, count) in all_stat[:number]:
title = get_title(url)
print('<li><a href="' + url+ '">' + title + '</a>, ' +
str(count) + ' просмотр' + get_ending(count) +
' за месяц</li>')
print("</ul>")
Генерация уникальных текстов (аналоги на Perl, Erlang и Haskell):
import re
import random
def repl(m):
lst = m.group(1).split("|")
return lst[int(random.random() * len(lst))]
text = "{Hi|{Hello|Good day}}, {world|people}!"
saved = ""
while(saved != text):
saved = text
text = re.sub("""\{([^\{\}]+)\}""", repl, text)
print(text)
Ротация логов:
import os
import re
import time
import calendar
import argparse
def dprint(str):
if not debug_print_enabled:
return
print("DEBUG: " + str)
parser = argparse.ArgumentParser(description='Rotate logs')
parser.add_argument(
'-d', '--debug', action="store_true",
help='Enable debug output')
parser.add_argument(
'-n', '--ndays', metavar='N', type=int, default=30,
help='Keep logs for N days')
parser.add_argument(
'dir', metavar='DIR', type=str,
help='Path to directory with log files')
args = parser.parse_args()
dir = args.dir
max_ndays = args.ndays
debug_print_enabled = args.debug
dprint("dir = " + dir + ", max_ndays = " + str(max_ndays))
now = int(time.time())
os.chdir(dir)
for fname in os.listdir("."):
m = re.search("(\d{4}-\d{2}-\d{2})", fname)
ftime = calendar.timegm(time.strptime(m.group(1), "%Y-%m-%d"))
ndays = int((now - ftime) / (60 * 60 * 24))
dprint("fname = " + fname + ", ndays = " + str(ndays))
if ndays < 1:
dprint(" Skipping")
elif ndays > max_ndays:
dprint(" Deleting")
os.unlink(fname)
elif fname.endswith(".gz"):
dprint(" Already compressed")
else:
dprint(" Compressing")
os.system("gzip " + fname)
Посылка e-mail (аналог на Perl, аналог на Scala):
from smtplib import SMTP
# from smtplib import SMTP_SSL as SMTP
from email.mime.text import MIMEText
import getpass
server = 'mail.example.ru'
port = 25 # 587
login = "afiskon@example.ru"
password = getpass.getpass("SMTP Password: ")
subj = """Test message"""
from_addr = 'Aleksander Alekseev <afiskon@example.ru>'
addr_list = """
user1@example.ru
user2@example.ru
""".split("\n")
body = """\
Message body here
"""
for receiver in addr_list:
receiver = receiver.strip()
if receiver == "":
continue
print("Sending to: '" + receiver + "'")
msg = MIMEText(body, 'plain')
msg['Subject'] = subj
msg['From'] = from_addr
msg['To'] = receiver
with SMTP(server, port) as conn:
conn.starttls()
conn.login(login, password)
conn.sendmail(from_addr, [receiver], msg.as_string())
Резервное копирование GitHub-репозиториев:
import subprocess
import getpass
import sys
def eprint(msg):
print(msg, file=sys.stderr)
sys.exit(1)
if len(sys.argv) < 2:
eprint("Usage: {} <username>".format(sys.argv[0]))
username = sys.argv[1]
password = getpass.getpass("{}'s password: ".format(username))
page = 0
while True:
page += 1
cmd = ("curl -s -u '{}:{}' https://api.github.com/user/repos" +
"?page={} | jq -r '.[].git_url' 2>/dev/null"
).format(username, password, page)
replist = subprocess.check_output(cmd, shell=True)
replist = replist.decode('utf-8').strip()
if not replist:
if page == 1:
eprint("Something is wrong. Auth failed?")
break
for repo in replist.split("\n"):
print("Cloning {}...".format(repo))
cmd = "git clone {} 2>/dev/null".format(repo)
code = subprocess.call(cmd, shell=True)
if code != 0:
eprint("Command '{}' failed - code {}".format(cmd, code))
print("All done!")
В продолжение темы о Python см заметки про Virtualenv, Flask, Jinja, psycopg2, PyTest, Matplotlib и далее по ссылкам.
Метки: Python.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.