Как я выбирал скриптовый язык и остановился на 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):

#!/usr/bin/env python3

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):

#!/usr/bin/env python3

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):

#!/usr/bin/env python3

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):

#!/usr/bin/env python3

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):

#!/usr/bin/env python3

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):

#!/usr/bin/env python3

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)

Ротация логов:

#!/usr/bin/env python3

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):

#!/usr/bin/env python3

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-репозиториев:

#!/usr/bin/env python3

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 и далее по ссылкам.

Метки: .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.