
Это девятая подборка советов про 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')
