Это девятая подборка советов про 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')
Комментарии (7)
worldmind
19.03.2019 15:05-1Стоит явно оговоривать, что по умолчанию питон и многие другие языки не делают округления которому учили в худшем в мире советском образовании — 0.5 -> 1, ибо такое округление приводит к искажению при анализе данных — смещению в большую сторону.
Поэтому по умолчанию половина округляется до бижайшего чётного т.е. то вниз, то вверх.
Впрочем, как я понял все виды округления создают какое-то искажение, поэтому идеального алгоритма округления нет (иначе он был бы везде по умолчанию).
Косяк питона в том, что вообще есть функция с названием round и без всяких без параметров (тут пхп окахзался молодцом), ведь в математике такой операции нет и непонятно что ожидать от такой функции, хотя они и следует рекомендациям стандарта на плавающие числа.FRiMN
20.03.2019 11:47Зависит от версии
Python 2.7.12 (default, Nov 12 2018, 14:36:49) >>> round(0.5) 1.0 >>> round(1.5) 2.0 Python 3.5.2 (default, Nov 12 2018, 13:43:14) >>> round(0.5) 0 >>> round(1.5) 2
For the built-in types supporting round(), values are rounded to the closest multiple of 10 to the power minus ndigits; if two multiples are equally close, rounding is done toward the even choice (so, for example, both round(0.5) and round(-0.5) are 0, and round(1.5) is 2).
— https://docs.python.org/3.5/library/functions.html#round
BubaVV
19.03.2019 19:42С ANY явно должен существовать более простой способ, без явного инстанцирования. С stdout можно встрять при использовании rpdb.
homecreate
21.03.2019 13:27Для ANY можно использовать библиотеку mock — https://docs.python.org/3/library/unittest.mock.html#any.
Ghostik2005
21.03.2019 01:16sys.stdout — это обёртка, позволяющая писать строковые, а не байты. Эти строковые значения автоматически кодируются с помощью sys.stdout.encoding:
доступно только для чтения и равно кодировке по умолчанию, которую можно настраивать с помощью переменной среды PYTHONIOENCODING
замечательно меняется на лету
sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='CP1251', buffering=1)
Уже импортированные модули не будут загружаться снова. Команда import foo просто ничего не сделает. Однако она полезна для переимпортирования модулей при работе в интерактивной среде. В Python 3.4+ для этого нужно использовать importlib
Не работает если скрипт и модули находятся в zip архиве.
akura13
5?