Это девятая подборка советов про 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ßen')
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'Straxc3x9fen')
Straße
sys.stdout.buffer
тоже является обёрткой. Её можно обойти, обратившись с помощью sys.stdout.buffer.raw
к дескриптору файла:
>>> sys.stdout.buffer.raw.write(b'Straxc3x9fe')
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')
Автор: Пуштаев Вадим