Хабрахабр

Подборка @pythonetc, февраль 2019

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

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

В 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. Оно выполняет поиск соответствия с той позиции, на которой закончился предыдущий поиск. Это позволяет нам писать конечные автоматы, которые обрабатывают строковые значения слово за словом (при этом слово определяется регулярным выражением).

В 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')

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть