Это девятая подборка советов про Python и программирование из моего авторского канала @pythonetc.
Предыдущие подборки.
Сравнение структур
Иногда при тестировании бывает нужно сравнить сложные структуры, игнорируя некоторые значения. Обычно это можно сделать, сравнивая конкретные значения из такой структуры:
>>> d = dict(a=1, b=2, c=3)
>>> assert d['a'] == 1
>>> assert d['c'] == 3
Однако можно создать особое значение, которое будет равно любому другому:
>>> assert d == dict(a=1, b=ANY, c=3)
Это легко делается с помощью магического метода
__eq__
:>>> class AnyClass:
... def __eq__(self, another):
... return True
...
>>> ANY = AnyClass()
stdout
sys.stdout — это обёртка, позволяющая писать строковые, а не байты. Эти строковые значения автоматически кодируются с помощью
sys.stdout.encoding
:>>> sys.stdout.write('Straße\n')
Straße
>>> sys.stdout.encoding
'UTF-8'
sys.stdout.encoding
доступно только для чтения и равно кодировке по умолчанию, которую можно настраивать с помощью переменной среды
PYTHONIOENCODING
:$ PYTHONIOENCODING=cp1251 python3
Python 3.6.6 (default, Aug 13 2018, 18:24:23)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-28)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.stdout.encoding
'cp1251'
Если вы хотите записать в
stdout
байты, то можете пропустить автоматическое кодирование, обратившись с помощью sys.stdout.buffer
к помещённому в обёртку буферу:>>> sys.stdout
<_io.TextIOWrapper name='<stdоut>' mode='w' encoding='cp1251'>
>>> sys.stdout.buffer
<_io.BufferedWriter name='<stdоut>'>
>>> sys.stdout.buffer.write(b'Stra\xc3\x9fe\n')
Straße
sys.stdout.buffer
тоже является обёрткой. Её можно обойти, обратившись с помощью
sys.stdout.buffer.raw
к дескриптору файла:>>> sys.stdout.buffer.raw.write(b'Stra\xc3\x9fe')
Straße
Константа Ellipsis
В Python очень мало встроенных констант. Одну из них,
Ellipsis
, можно также записать в виде ...
. Для интерпретатора эта константа не имеет какого-то конкретного значения, но зато она используется там, где уместен подобный синтаксис.numpy
поддерживает Ellipsis
в качестве аргумента __getitem__
, например, x[...]
возвращает все элементы x
.PEP 484 определяет для этой константы ещё одно значение:
Callable[..., type]
позволяет определять типы вызываемого без указания типов аргументов.Наконец, вы можете использовать
...
для обозначения того, что функция ещё не реализована. Это полностью корректный код на Python:def x():
...
Однако в Python 2
Ellipsis
нельзя записать в виде ...
. Единственным исключением является a[...]
, что интерпретируется как a[Ellipsis]
.Этот синтаксис корректен для Python 3, но для Python 2 корректна лишь первая строка:
a[...]
a[...:2:...]
[..., ...]
{...:...}
a = ...
... is ...
def a(x=...): ...
Повторный импорт модулей
Уже импортированные модули не будут загружаться снова. Команда
import foo
просто ничего не сделает. Однако она полезна для переимпортирования модулей при работе в интерактивной среде. В Python 3.4+ для этого нужно использовать importlib
:In [1]: import importlib
In [2]: with open('foo.py', 'w') as f:
...: f.write('a = 1')
...:
In [3]: import foo
In [4]: foo.a
Out[4]: 1
In [5]: with open('foo.py', 'w') as f:
...: f.write('a = 2')
...:
In [6]: foo.a
Out[6]: 1
In [7]: import foo
In [8]: foo.a
Out[8]: 1
In [9]: importlib.reload(foo)
Out[9]: <module 'foo' from '/home/v.pushtaev/foo.py'>
In [10]: foo.a
Out[10]: 2
Для
ipython
также есть расширение autoreload
, которое в случае надобности автоматически переимпортирует модули:In [1]: %load_ext autoreload
In [2]: %autoreload 2
In [3]: with open('foo.py', 'w') as f:
...: f.write('print("LOADED"); a=1')
...:
In [4]: import foo
LOADED
In [5]: foo.a
Out[5]: 1
In [6]: with open('foo.py', 'w') as f:
...: f.write('print("LOADED"); a=2')
...:
In [7]: import foo
LOADED
In [8]: foo.a
Out[8]: 2
In [9]: with open('foo.py', 'w') as f:
...: f.write('print("LOADED"); a=3')
...:
In [10]: foo.a
LOADED
Out[10]: 3
\G
В некоторых языках вы можете использовать выражение
\G
. Оно выполняет поиск соответствия с той позиции, на которой закончился предыдущий поиск. Это позволяет нам писать конечные автоматы, которые обрабатывают строковые значения слово за словом (при этом слово определяется регулярным выражением).В Python ничего подобного этому выражению нет, и реализовать похожую функциональность можно, вручную отслеживая позицию и передавая часть строки в функции регулярных выражений:
import re
import json
text = '<a><b>foo</b><c>bar</c></a><z>bar</z>'
regex = '^(?:<([a-z]+)>|</([a-z]+)>|([a-z]+))'
stack = []
tree = []
pos = 0
while len(text) > pos:
error = f'Error at {text[pos:]}'
found = re.search(regex, text[pos:])
assert found, error
pos += len(found[0])
start, stop, data = found.groups()
if start:
tree.append(dict(
tag=start,
children=[],
))
stack.append(tree)
tree = tree[-1]['children']
elif stop:
tree = stack.pop()
assert tree[-1]['tag'] == stop, error
if not tree[-1]['children']:
tree[-1].pop('children')
elif data:
stack[-1][-1]['data'] = data
print(json.dumps(tree, indent=4))
В приведённом примере можно сэкономить время на обработку, не разбивая строку раз за разом, а просить модуль
re
начинать искать с другой позиции.Для этого нужно внести в код кое-какие изменения. Во-первых,
re.search
не поддерживает определение позиции начала поиска, так что придётся компилировать регулярное выражение вручную. Во-вторых, ^
обозначает начало строкового значения, а не позицию начала поиска, поэтому нужно проверять вручную, что соответствие найдено в той же позиции.import re
import json
text = '<a><b>foo</b><c>bar</c></a><z>bar</z>' * 10
def print_tree(tree):
print(json.dumps(tree, indent=4))
def xml_to_tree_slow(text):
regex = '^(?:<([a-z]+)>|</([a-z]+)>|([a-z]+))'
stack = []
tree = []
pos = 0
while len(text) > pos:
error = f'Error at {text[pos:]}'
found = re.search(regex, text[pos:])
assert found, error
pos += len(found[0])
start, stop, data = found.groups()
if start:
tree.append(dict(
tag=start,
children=[],
))
stack.append(tree)
tree = tree[-1]['children']
elif stop:
tree = stack.pop()
assert tree[-1]['tag'] == stop, error
if not tree[-1]['children']:
tree[-1].pop('children')
elif data:
stack[-1][-1]['data'] = data
def xml_to_tree_slow(text):
regex = '^(?:<([a-z]+)>|</([a-z]+)>|([a-z]+))'
stack = []
tree = []
pos = 0
while len(text) > pos:
error = f'Error at {text[pos:]}'
found = re.search(regex, text[pos:])
assert found, error
pos += len(found[0])
start, stop, data = found.groups()
if start:
tree.append(dict(
tag=start,
children=[],
))
stack.append(tree)
tree = tree[-1]['children']
elif stop:
tree = stack.pop()
assert tree[-1]['tag'] == stop, error
if not tree[-1]['children']:
tree[-1].pop('children')
elif data:
stack[-1][-1]['data'] = data
return tree
_regex = re.compile('(?:<([a-z]+)>|</([a-z]+)>|([a-z]+))')
def _error_message(text, pos):
return text[pos:]
def xml_to_tree_fast(text):
stack = []
tree = []
pos = 0
while len(text) > pos:
error = f'Error at {text[pos:]}'
found = _regex.search(text, pos=pos)
begin, end = found.span(0)
assert begin == pos, _error_message(text, pos)
assert found, _error_message(text, pos)
pos += len(found[0])
start, stop, data = found.groups()
if start:
tree.append(dict(
tag=start,
children=[],
))
stack.append(tree)
tree = tree[-1]['children']
elif stop:
tree = stack.pop()
assert tree[-1]['tag'] == stop, _error_message(text, pos)
if not tree[-1]['children']:
tree[-1].pop('children')
elif data:
stack[-1][-1]['data'] = data
return tree
print_tree(xml_to_tree_fast(text))
Результаты:
In [1]: from example import *
In [2]: %timeit xml_to_tree_slow(text)
356 µs ± 16.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [3]: %timeit xml_to_tree_fast(text)
294 µs ± 6.15 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Округление чисел
Этот пункт написал orsinium, автор Telegram-канала @itgram_channel.
Функция
round
округляет число до заданного количества знаков после запятой.>>> round(1.2)
1
>>> round(1.8)
2
>>> round(1.228, 1)
1.2
Можно задать и отрицательную точность округления:
>>> round(413.77, -1)
410.0
>>> round(413.77, -2)
400.0
round
возвращает значение того же типа, что и входное число:
>>> type(round(2, 1))
<class 'int'>
>>> type(round(2.0, 1))
<class 'float'>
>>> type(round(Decimal(2), 1))
<class 'decimal.Decimal'>
>>> type(round(Fraction(2), 1))
<class 'fractions.Fraction'>
Для своих собственных классов вы можете определить обработку
round
с помощью метода __round__
:>>> class Number(int):
... def __round__(self, p=-1000):
... return p
...
>>> round(Number(2))
-1000
>>> round(Number(2), -2)
-2
Здесь значения округлены до ближайших чисел, кратных
10 ** (-precision)
. Например, с precision=1
значение будет округлено до числа, кратного 0,1: round(0.63, 1)
возвращает 0.6
. Если два кратных числа будут одинаково близки, то округление выполняется до чётного числа:>>> round(0.5)
0
>>> round(1.5)
2
Иногда округление числа с плавающей запятой может дать неожиданный результат:
>>> round(2.85, 1)
2.9
Дело в том, что большинство десятичных дробей нельзя точно выразить с помощью числа с плавающей запятой (https://docs.python.org/3.7/tutorial/floatingpoint.html):
>>> format(2.85, '.64f')
'2.8500000000000000888178419700125232338905334472656250000000000000'
Если хотите округлять половины вверх, то используйте
decimal.Decimal
:>>> from decimal import Decimal, ROUND_HALF_UP
>>> Decimal(1.5).quantize(0, ROUND_HALF_UP)
Decimal('2')
>>> Decimal(2.85).quantize(Decimal('1.0'), ROUND_HALF_UP)
Decimal('2.9')
>>> Decimal(2.84).quantize(Decimal('1.0'), ROUND_HALF_UP)
Decimal('2.8')