Привет. Я веду канал @pythonetc с советами про Python в частности и про программирование в целом. С этого месяца мы запускаем серию дайджестов с лучшими постами за месяц в переводе на русский.
Передача данных по цепочке вызовов
Когда вы хотите передать какую-то информацию по цепочке вызовов, то обычно используете самый простой способ: передаете данные в виде аргументов функций.
Но иногда очень неудобно изменять все функции в цепочке, только чтобы передать новую порцию данных. В таких случаях лучше создать своеобразный контекст, который будут использовать функции. Как это сделать?
Простейшее решение — глобальная переменная. В Python в качестве хранителей контекста можно использовать модули и классы, поскольку они, строго говоря, тоже глобальные переменные. Возможно, вы периодически так и делаете, скажем, когда создаете логгеры.
Если у вас многопоточное приложение, то обычные глобальные переменные не помогут, поскольку не потокобезопасны (thread-safe). Одновременно у вас могут исполняться несколько цепочек вызовов, и каждой понадобится свой контекст. Модуль threading
предоставляет потокобезопасный объект threading.local()
. Сохраняйте в нем любые данные, просто обращаясь к атрибутам: threading.local().symbol = '@'
.
Однако оба подхода не concurrency-safe, то есть они не будут работать в цепочках вызовов корутин, в которых корутины могут не вызывать другие корутины, а делать на них await. Если корутина в состоянии ожидания, event loop может запустить другую корутину из другой цепочки. Этот вариант не будет работать:
import asyncio
import sys
global_symbol = '.'
async def indication(timeout):
while True:
print(global_symbol, end='')
sys.stdout.flush()
await asyncio.sleep(timeout)
async def sleep(t, indication_t, symbol='.'):
loop = asyncio.get_event_loop()
global global_symbol
global_symbol = symbol
loop.create_task(indication(indication_t))
await asyncio.sleep(t)
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(
sleep(1, 0.1, '0'),
sleep(1, 0.1, 'a'),
sleep(1, 0.1, 'b'),
sleep(1, 0.1, 'c'),
))
Решить проблему можно, заставив event loop сохранять и восстанавливать контекст при каждом возвращении к корутине. Так поступает модуль aiotask_context
, который с помощью loop.set_task_factory
меняет способ создания объектов задач. Вот такой вариант сработает:
import asyncio
import sys
import aiotask_context as context
async def indication(timeout):
while True:
print(context.get('symbol'), end='')
sys.stdout.flush()
await asyncio.sleep(timeout)
async def sleep(t, indication_t, symbol='.'):
loop = asyncio.get_event_loop()
context.set(key='symbol', value=symbol)
loop.create_task(indication(indication_t))
await asyncio.sleep(t)
loop = asyncio.get_event_loop()
loop.set_task_factory(context.task_factory)
loop.run_until_complete(asyncio.gather(
sleep(1, 0.1, '0'),
sleep(1, 0.1, 'a'),
sleep(1, 0.1, 'b'),
sleep(1, 0.1, 'c'),
))
Создаем SVG
SVG — векторный графический формат, хранящий информацию об изображении в виде всех форм и чисел, необходимых для отрисовки в XML. К примеру, оранжевый круг можно представить так:
<svg xmlns="http://www.w3.org/2000/svg">
<circle cx="125" cy="125" r="75" fill="orange"/>
</svg>
Поскольку SVG является подмножеством XML, можно довольно легко создавать SVG-файлы на любом языке. В том числе и на Python, например, с помощью lxml. Но есть и модуль svgwrite, созданный как раз для создания SVG.
Вот пример, как можно отобразить последовательность Рекамана в виде диаграммы, которую вы видели в начале статьи.
Обращение к внешним областям видимости
Когда вы используете переменную в Python, он сначала ищет ее в текущей области видимости. Если не находит, то ищет уже в области на уровень выше. И так до тех пор, пока не дойдет глобального пространства имен.
x = 1
def scope():
x = 2
def inner_scope():
print(x) # prints 2
inner_scope()
scope()
Но присвоение переменной работает иначе. Новая переменная всегда создается в текущей области видимости, если только не указано global
или nonlocal
:
x = 1
def scope():
x = 2
def inner_scope():
x = 3
print(x) # prints 3
inner_scope()
print(x) # prints 2
scope()
print(x) # prints 1
global
позволяет использовать переменные глобального пространства имен, а в случае с nonlocal
Python ищет переменную в ближайшем объемлющем контексте. Сравните:
x = 1
def scope():
x = 2
def inner_scope():
global x
x = 3
print(x) # prints 3
inner_scope()
print(x) # prints 2
scope()
print(x) # prints 3
x = 1
def scope():
x = 2
def inner_scope():
nonlocal x
x = 3
print(x) # prints 3
inner_scope()
print(x) # prints 3
scope()
print(x) # prints 1
Выполнение скриптов
python
поддерживает несколько способов запуска скрипта. Обычная команда python foo.py
просто исполняет foo.py
.
Также можно использовать конструкцию python -m foo
. Если foo
не является пакетом, то система найдет foo.py
в sys.path
и выполнит. Если же является, Python выполнит foo/__init__.py
, а потом foo/__main__.py
. Обратите внимание, что переменная __name__
в ходе выполнения __init__.py
принимает значение foo
, а в ходе выполнения __main__.py
— __main__
.
Также можно использовать форму python dir/
или даже python dir.zip
. Тогда python
будет искать dir/__main__.py
, и если найдет — выполнит.
$ ls foo
__init__.py __main__.py
$ cat foo/__init__.py
print(__name__)
$ cat foo/__main__.py
print(__name__)
$ python -m foo
foo
__main__
$ python foo/
__main__
$ python foo/__init__.py
__main__
Количество секунд с начала эпохи
До появления Python 3.3 трудно было преобразовать объект datetime
в количество секунд с начала эпохи Unix.
Логичнее всего использовать метод strftime
, который умеет форматировать datetime
. Взяв %s
в качестве формата, можно получить timestamp.
naive_time = datetime(2018, 3, 31, 12, 0, 0)
utc_time = pytz.utc.localize(naive_time)
ny_time = utc_time.astimezone(
pytz.timezone('US/Eastern'))
ny_time is the exact the same moment as utc_time, but written as New Yorkers see it:
# utc_time
datetime.datetime(2018, 3, 31, 12, 0,
tzinfo=<UTC>)
# utc_time
datetime.datetime(2018, 3, 31, 8, 0,
tzinfo=<DstTzInfo 'US/Eastern' ...>)
Если моменты одинаковы, то и таймстемпы должны быть эквивалентны:
In : int(utc_time.strftime('%s')),
int(ny_time.strftime('%s'))
Out: (1522486800, 1522468800)
Эээ, что? Они совершенно разные. На самом деле, использовать strftime
для решения этой задачи нельзя. В Python strftime
вообще не поддерживает %s
в качестве аргумента, а работает это лишь потому, что внутри вызывается функция strftime()
платформы С-библиотеки. Но, как видите, часовой пояс объекта datetime
полностью проигнорирован.
Правильный результат можно получить с помощью простого вычитания:
In : epoch_start = pytz.utc.localize(
datetime(1970, 1, 1))
In : (utc_time - epoch_start).total_seconds()
Out: 1522497600.0
In : (utc_time - epoch_start).total_seconds()
Out: 1522497600.0
А если вы используете Python 3.3+, то можете решить проблему с помощью timestamp
класса datetime
: utc_time.timestamp()
.
Автор: pushtaev