company_banner

Подборка @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()

    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')
    • +37
    • 6,6k
    • 7
    Mail.ru Group
    1 016,66
    Строим Интернет
    Поделиться публикацией

    Похожие публикации

    Комментарии 7

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

          Зависит от версии


          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
            0
            Ну второй питон я из головы выбросил, пока нужды нет ретроградствовать.
          0
          С ANY явно должен существовать более простой способ, без явного инстанцирования. С stdout можно встрять при использовании rpdb.
          0
          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 архиве.

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

          Самое читаемое