PyBuka

Время проектов! Немного увлекаюсь барабанами, в частности дарбукой (или думбеком). Написал небольшой проектик на Python для проигрывания ритмов дарбуки. Он преобразует общепринятую текстовую запись в зацикленный звук с заданным ритмом.

Для работы нужен pygame (pip install pygame). Запустить плеер можно из терминала (первый аргумент – ритм, второй – число ударов в минуту):

python pybuka.py "D-T---T-D---T-tkD-T---T-D--kS---" 160

D – низкий глубокий удар

T – звонкий громкий удар об обод

t или k – звонкие, но тише, чем T

S – слэп (удар плашмя по центру)

Дефис – пауза.

Особенность воспроизведения звука в pygame: для каждого типа удара о барабан создается отдельный канал channel = mixer.Channel(ch_id), чтобы рядом стоящие по времени ноты не мешали друг другу.

Пример записи звука прилагается.

Ссылка на исходник проекта на GitHub.  

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

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈

Перенос строк кода Python

Подписывайтесь на мой канал в Телеграм @pyway , чтобы быть в курсе о новых статьях!

PEP-8 не рекомендует писать строки кода длиннее, чем 79 символов. С этим можно не согласиться, однако, встречаются строки, которые не влезают даже на наши широкоформатные мониторы.

👨‍🎓 Старайтесь не делать очень длинные строки, разбивая сложные условия или формулы на отдельные части, вынося их в переменные или функции с осмысленными названиями.

Если есть острая необходимость иметь длинное выражение, тогда приходится переносить код на следующие строки. Можно делать двумя способами: скобками и слэшем. 

Если, перед выражением открыта скобка (круглая, квадратная или фигурная в зависимости от контекста), но она не закрыта в этой строке, то Python будет сканировать последующие строки, пока не найдет соответствующую закрывающую скобку (англ. implicit line joining). Примеры:

# вычисления
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

if (student_loan_interest > ira_deduction
        and qualified_dividends == 0):
    ...

# словари
d = {
    "hello": 10,
    "world": 20,
    "abc": "foo"
}

# аргументы функции
some_func(arg1,
    arg2,
    more_arg,
    so_on_and_on)

Обратите внимание, что в первом примере скобки очень важны. Без скобок код не скомпилируется из-за отступов, а если их убрать, то результат будет неверен: income станет gross_wages, а последующие строки не будут иметь эффекта!

# неправильно!
income = gross_wages
+ taxable_interest
+ (dividends - qualified_dividends)
- ira_deduction
- student_loan_interest

Метод переноса обратным слэшем. Ставим обратный слэш конце строки и сразу энтер (перенос строки): тогда следующая строка будет включена в текущую (англ. explicit line joining), не взирая на отступы, как будто бы они написаны в одну строку:

income = gross_wages \
         + taxable_interest \
         + (dividends - qualified_dividends) \
         - ira_deduction \
         - student_loan_interest

Еще примеры со слэшем:

if student_loan_interest > ira_deduction \
        and qualified_dividends == 0:
    ...

# допустимо, согласно PEP-8
with open('/path/to/some/file/you/want/to/read') as file_1, \
     open('/path/to/some/file/being/written', 'w') as file_2:
    file_2.write(file_1.read())

# пробелы в строку попадут, а энтер - нет!
str = "Фу\
      < вот эти пробелы тоже в строке"

Почему скобки лучше для переноса:

  • Лучше восприятие
  • Скобок две, а слэшей надо по одному на каждый перенос
  • Можно забыть слэш и сломать код
  • Можно поставить пробел после слэша и тоже сломать

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Отрезок времени в Python – timedelta

В модуле datatime содержатся классы для работы с датой и временем. В частности часто используются datetime для хранения даты и времени некоторого события и timedelta для хранения интервала времени между каким-то двумя событиями.

Удобно работать с datetime и timedelta путем математических операций. 

📎 Примеры. Добавить к дате один день, год или отнять 2:20 (функция str тут для человекочитаемого формата):

>>> str(datetime.now() + timedelta(days=1))
'2019-10-06 15:51:09.089691'
>>> str(datetime.now() + timedelta(days=365))
'2020-10-04 15:52:04.618896'
>>> str(datetime.now() - timedelta(hours=2, minutes=20))
'2019-10-05 13:41:27.617589'

Разница во времени между событиями:

>>> a = datetime.now()
>>> b = datetime.now() + timedelta(minutes=5)
>>> b - a
datetime.timedelta(0, 317, 99915)
>>> str(b - a)
'0:05:17.099915'

timedelta внутренне хранит только секунды от начала дня и сами дни, хотя в конструкторе принимает дни, секунды, микросекунды, миллисекунды, минуты, часы и недели (можно выбрать любые их сочетания для задания интервала): 

datetime.timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0)

>>> str(timedelta(days=1, hours=2, milliseconds=333))
'1 day, 2:00:00.333000'

Причем мы не обязаны нормализовывать аргументы: он сам поймет, что 200 минут – это 3 часа 20 минут:

>>> str(timedelta(minutes=200))
'3:20:00'

Достать часы и минуты (странно, что у объекта нет свойств hours и minutes):

def hours_minutes(td):
    return td.seconds // 3600, (td.seconds // 60) % 60

>>> hours_minutes(timedelta(0, 12345))
(3, 25)

Сколько всего секунд в интервале:

>>> timedelta(minutes=200, seconds=21, hours=25).total_seconds()
102021.0

Можно даже умножать timedelta на числа или поделить два timedelta или взять остаток. Допустим рабочая смена длится 7 часов 30 минут, сколько полных смен в 3-х сутках?

>>> a = timedelta(days=3)
>>> b = timedelta(hours=7, minutes=30)
>>> a // b
9
>>> str(a % b)
'4:30:00'

Ответ 9 полных смен и еще останется 4 часа 30 минут лишних.

Бонус. Формат даты по-нашенскому (ДД.ММ.ГГГГ):

>>> datetime.strftime(datetime.now(), '%d.%m.%Y')
'05.10.2019'

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈

Деление с остатком преподнесло сюрприз

Деление с остатком – часто используемая операция в программировании. Начиная от классических заданий для начинающих на вычисление минут и секунд:

total_seconds = 119
seconds = total_seconds % 60
minutes = total_seconds // 60
print(f'{minutes}:{seconds}')  # 1:59

Заканчивая тем, что на остатках построена львиная доля криптографии. Нахождения остатка часто называют modulo (или коротко mod). 

При делении a на b неполное частное q и остаток r связаны формулой:

a = b · q + r, где b ≠ 0

В Python 3 частное и остаток вычисляются операторами:

q = a // b
r = a % b

Именно двойной слэш, одинарный слэш – деление без остатка (до конца). Иногда двойной слэш называют целочисленным делением, что не очень справедливо, потому что мы можем без проблем делить числа с запятой. Если оба числа целые (int), то частное будет тоже целым числом (int), иначе float. Посмотрите примеры:

10 / 3 == 3.3333333333333335
10 // 3 == 3
10.0 / 3.0 == 3.3333333333333335
10.0 // 3.0 == 3.0 
10.0 % 3.0 == 1.0
10 % 3 == 1

2.4 // 0.4 == 5.0
2.4 / 0.4 == 5.999999999999999
2.4 % 0.4 == 0.3999999999999998

Последние три примера немного обескураживают из-за особенностей вычислений с плавающей точкой на компьютере, но формула a = b · q + r всегда остается справедлива.

Поговорим об отрицательных числах. Математически остаток не должен быть меньше нуля и больше или равен модулю делителя b: 0 ≤ r < |b|. Однако, Intel в своих процессорах случайно либо намеренно ввела отрицательные остатки в реализации ассемблерных команд деления. Компиляторы языков C и С++, являясь платформо-зависимыми, обычно полагаются на процессорное поведение. Пример на С++. И вообще посмотрите на эту огромную таблицу, каждый язык программирования пляшет, как хочет. Не будем спорить, кто из них прав. Просто узнаем, как у нас в Python:

a, b = [10, -10], [3, -3]
for x in a:
  for y in b:
    print(f'{x} // {y} = {x // y}')
    print(f'{x} % {y} = {x % y}')
    print()

10 // 3 = 3
10 % 3 = 1

10 // -3 = -4
10 % -3 = -2

-10 // 3 = -4
-10 % 3 = 2

-10 // -3 = 3
-10 % -3 = -1

Формула выполняется всегда, но результаты отличаются для С++ и Python, где при делении на положительное число – остаток всегда положителен, а на отрицательное число – отрицателен. Если бы мы сами реализовали взятие остатка, то получилось бы так:

def mod_python(a, b):
  return int(a - math.floor(a / b) * b)

# на С++ работает так:
def mod_cpp(a, b):
  return int(a - math.trunc(a / b) * b)

Где floor – ближайшее целое число не превышающее аргумент: floor(-3.3) = -4, а trunc – функция отбрасывания целой части: trunc(-3.3) = -3. Разница проявляется между ними только для отрицательных чисел. Отсюда и разные остатки и частные – все зависит от того, с какой стороны числовой оси мы приближаемся к частному.

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

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈

Умножение списка на число

Студент Макс узнал, что в Python умножать можно не только числа, но и другие объекты, например, строку на число:

>>> "Max" * 3
'MaxMaxMax'

«Вау!» — подумал Макс — «А что если умножить список на число?»:

>>> [42, 26] * 3
[42, 26, 42, 26, 42, 26]

Значит можно создать двумерный массив очень кратко и элегантно?

>>> [[]] * 3
[[], [], []]

Заполнить его:

arr = [[]] * 3
arr[0].append(10)
arr[1].append(20)
arr[2].append(30)

Макс ожидал получить:

[[10], [20], [30]]

А вышло:

[[10, 20, 30], [10, 20, 30], [10, 20, 30]]

😯 Как же так?! Дело в том, что умножение списка на число не копирует сам объект, а лишь ссылку на него. Все три элемента arr ссылаются на один и тот же список. Легко проверить, сравнив адреса объектов:

>>> arr[0] is arr[1]
True
>>> id(arr[0]), id(arr[1])
(4400840776, 4400840776)
Диаграмма: все элементы arr указывают на один и тот же список.

Аналогично в случае классов:

class Dummy: ...
arr = [Dummy()] * 2
arr[0].x = 10
arr[1].x = 20
print(arr[0].x, arr[0] is arr[1])  # 20 True

А вот с числами, строками и кортежами умножение списка будет работать как ожидал Макс, потому что это неизменяемые типы. Вот такая тонкость, которую нужно знать. Максу следовало бы написать так:

arr = [[] for _ in range(3)]  
arr[0].append(10)
arr[1].append(20)
arr[2].append(30)
>>> arr
[[10], [20], [30]]

Менее кратко, но зато работает без сюрпризов: каждую итерацию создается новый пустой список.

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈