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


  1. akura13
    19.03.2019 12:10
    -1

    5?


  1. worldmind
    19.03.2019 15:05
    -1

    Стоит явно оговоривать, что по умолчанию питон и многие другие языки не делают округления которому учили в худшем в мире советском образовании — 0.5 -> 1, ибо такое округление приводит к искажению при анализе данных — смещению в большую сторону.
    Поэтому по умолчанию половина округляется до бижайшего чётного т.е. то вниз, то вверх.
    Впрочем, как я понял все виды округления создают какое-то искажение, поэтому идеального алгоритма округления нет (иначе он был бы везде по умолчанию).
    Косяк питона в том, что вообще есть функция с названием round и без всяких без параметров (тут пхп окахзался молодцом), ведь в математике такой операции нет и непонятно что ожидать от такой функции, хотя они и следует рекомендациям стандарта на плавающие числа.


    1. 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


      1. worldmind
        20.03.2019 14:26

        Ну второй питон я из головы выбросил, пока нужды нет ретроградствовать.


  1. BubaVV
    19.03.2019 19:42

    С ANY явно должен существовать более простой способ, без явного инстанцирования. С stdout можно встрять при использовании rpdb.


    1. homecreate
      21.03.2019 13:27

      Для ANY можно использовать библиотеку mock — https://docs.python.org/3/library/unittest.mock.html#any.


  1. Ghostik2005
    21.03.2019 01:16

    sys.stdout — это обёртка, позволяющая писать строковые, а не байты. Эти строковые значения автоматически кодируются с помощью sys.stdout.encoding:

    доступно только для чтения и равно кодировке по умолчанию, которую можно настраивать с помощью переменной среды PYTHONIOENCODING

    замечательно меняется на лету
    sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='CP1251', buffering=1)

    Уже импортированные модули не будут загружаться снова. Команда import foo просто ничего не сделает. Однако она полезна для переимпортирования модулей при работе в интерактивной среде. В Python 3.4+ для этого нужно использовать importlib

    Не работает если скрипт и модули находятся в zip архиве.