20 малоизвестных фич и особенностей Python

Небольшая подборка полезных фич и особенностей Python, о которых вы, возможно, никогда не слышали.

20 малоизвестных фич и особенностей Python

У нас есть списки наиболее значимых проектов на Python. Но это достаточно глобальные обзоры, верно? Что ж, теперь посмотрите на фишки, которые значительно упростят вашу жизнь.

Отладка регулярных выражений

Регулярные выражения Python – мощный и полезный инструмент, но отлаживать их – то еще удовольствие. Оказывается, любую регулярку можно визуализировать в виде дерева синтаксического анализа. Эта возможность языка пока экспериментальная, за нее отвечает флаг re.DEBUG в методе re.compile.

Посмотрим на регулярное выражение для поиска тегов font. С ним что-то не так.

re.compile("^\[font(?:=(?P<size>[-+][0-9]{1,2}))?\](.*?)[/font]",
    re.DEBUG)
at at_beginning
literal 91
literal 102
literal 111
literal 110
literal 116
max_repeat 0 1
  subpattern None
    literal 61
    subpattern 1
      in
        literal 45
        literal 43
      max_repeat 1 2
        in
          range (48, 57)
literal 93
subpattern 2
  min_repeat 0 65535
    any None
in
  literal 47
  literal 102
  literal 111
  literal 110
  literal 116

Теперь ясно, что именно. В закрывающем дескрипторе [/font] не экранированы квадратные скобки, поэтому он воспринимается не как тег, а как группа символов.

re.compile("""
 ^              # начало строки
 \[font         # тег font
 (?:=(?P<size>  # опционально [font=+size]
 [-+][0-9]{1,2} # определение размера
 ))?
 \]             # конец тега
 (.*?)          # содержимое тега
 \[/font\]      # закрывающий тег
 """, re.DEBUG|re.VERBOSE|re.DOTALL)

Выражения-генераторы

В Python есть очень удобные генераторы коллекций (списков, множеств, словарей), которые позволяют легко и быстро создавать отфильтрованные коллекции значений. Например, вот так можно создавать Python списки:

numbers = range(10)
x = [n for n in numbers if n % 2 == 0]

print(x) # 0 2 4 6 8

А еще есть выражения-генераторы, которые не загружают коллекцию в память целиком, а выдают лишь один элемент по требованию. В некоторых случаях это позволяет существенно сэкономить расходы памяти. Единственное отличие в синтаксисе – это круглые скобки:

y = (n for n in numbers if n % 2 == 0)

print(y) # <generator object>

Ряд особенностей Python генераторов:

  • невозможно получить их длину;
  • нельзя сделать срез элементов, перемотать или получить случайный элемент по его индексу;
  • функция print выводит объект генератора, а не список элементов.

Зато их удобно использовать в различных конструкциях, где требуется итерируемый объект. В выражения-генераторы можно включать множественные условия отбора значений и сочетать несколько циклов:

n = ((a,b) for a in range(0,2) for b in range(4,6))

for i in n:
    print(i)

# (0, 4)
# (0, 5)
# (1, 4)
# (1, 5)

Подводные камни дефолтных аргументов

Устанавливая значение аргументов функции по умолчанию, будьте очень осторожны:

def foo(x = []):
    x.append(1)
    print(x)

foo() # [1]
foo() # [1, 1]
foo() # [1, 1, 1]

Вряд ли вы хотели, чтобы список x изменялся при каждом вызове функции. Так происходит из-за того, что дефолтные параметры хранятся в неизменном кортеже в атрибуте foo.__defaults__, который создается в момент определения функции.

Вместо мутабельных списков лучше использовать значение None, а список присваивать в x уже внутри функции:

def foo2(x=None):
    if x is None:
        x=[]
    x.append(1)
    print(x)

foo2() # [1]
foo2() # [1]
foo2() # [1]

Передача значений в генератор

Язык программирования Python поддерживает генераторы – функции со множественными точками входа. В генератор можно передать значение на каждом шаге работы, что очень удобно, если приходится работать с динамическими данными:

def my_generator():
    a = 5 # значение, которое вернется при первом вызове
    while True:
        f = (yield a) # вернуть a и получить новое значение f
        if f is not None:
            a = f # сохранить новое значение

Это бессмысленная в целом функция, которая просто сохраняет полученное значение и возвращает его при следующем вызове.

В старом стандарте языка был метод my_generator.next(value), который сразу возвращал текущее значение генератора и принимал новое. В Python 3 необходимо использовать два метода: next(my_generator) и my_generator.send(value).

g = my_generator() # создать новый генератор
print(next(g)) # первый возврат по умолчанию (5)
g.send(100) # передача нового значения в f
print(next(g)) # получение переданного ранее значения (100)
g.send(42)
print(next(g)) # 42

Встроенная функция breakpoint()

Хотя мы и хотим писать идеальный код, правда в том, что это невозможно. Но такие фичи, как новая встроенная функция breakpoint(), пришедшая с Python 3.7, существенно облегчают жизнь питонистов. Нет, это не добавляет принципиально новой функциональности в язык программирования Python, зато делает дебагинг более гибким и понятным.

Шаг среза

Третий аргумент slice-оператора в Python определяет шаг среза. По умолчанию он равен единице, поэтому в итоговый срез попадают все элементы диапазона подряд.

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(a[2:8:]) # [3, 4, 5, 6, 7, 8]

А можно взять, например, каждый второй элемент:

print(a[2:8:2]) # [3, 5, 7]

Если передать третьим параметром -1, счет пойдет в обратном порядке. Так можно легко развернуть список или строку.

print(a[::-1]) # [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

Декораторы

Декоратор – это обертка для функции, позволяющая изменить некоторым образом ее поведение. Например, просто распечатать аргументы перед вызовом:

def print_args(function):
    def wrapper(*args, **kwargs):
        print('args:', args)
        print('kwargs:', kwargs)
        return function(*args, **kwargs)
    return wrapper

Теперь необходимо передать функции print_args другую функцию, аргументы которой необходимо распечатать:

def write(a, b):
    print(a, b)

write_with_print = print_args(write)
write_with_print('foo', 'bar')

# args: ('foo', 'bar')
# kwargs: {}
# foo bar

Все работает правильно, но приходится создавать новую функцию и вызывать именно ее.

В Python работа с декораторами устроена гораздо удобнее. Вы можете сохранить исходное имя функции и ее подпись при интроспеции:

@print_args
def write(a, b):
    print(a, b)

write('foo', 'bar')

# args: ('foo', 'bar')
# kwargs: {}
# foo bar

Отсутствующие элементы словарей

В Python 2.5 у словарей появился специальный метод __missing__. Он вызывается при обращении к отсутствующим элементам:

class MyDict(dict):
    def __missing__(self, key):
        self[key] = rv = []
        return rv

m = MyDict()
m["foo"].append(1)
m["foo"].append(2)
dict(m)

# {'foo': [1, 2]}

Примерно то же самое делает подкласс defaultdict: он вызывает для несуществующих элементов функцию без аргументов.

from collections import defaultdict
m = defaultdict(list)
m["foo"].append(1)
m["foo"].append(2)
print(m)

# {'foo': [1, 2]}

Многострочные регулярные выражения

Одна из самых приятных особенностей Python – возможность разбить длинные регулярки на несколько строк, добавить комментарии и сделать их более читаемыми.

pattern = """
^                   # beginning of string
M{0,4}              # thousands - 0 to 4 M's
(CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),
                    #            or 500-800 (D, followed by 0 to 3 C's)
(XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
                    #        or 50-80 (L, followed by 0 to 3 X's)
(IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
                    #        or 5-8 (V, followed by 0 to 3 I's)
$                   # end of string
"""
re.search(pattern, 'M', re.VERBOSE)

Многострочные регулярные выражения Python можно создавать и без re.VERBOSE, используя обычную конкатенацию строчных литералов:

pattern = (
    "^"                 # beginning of string
    "M{0,4}"            # thousands - 0 to 4 M's
    "(CM|CD|D?C{0,3})"  # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),
                        #            or 500-800 (D, followed by 0 to 3 C's)
    "(XC|XL|L?X{0,3})"  # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
                        #        or 50-80 (L, followed by 0 to 3 X's)
    "(IX|IV|V?I{0,3})"  # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
                        #        or 5-8 (V, followed by 0 to 3 I's)
    "$"                 # end of string
)
print pattern
# ^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$

Кроме того, совпадения можно именовать:

p = re.compile(r'(?P<word>\b\w+\b)')
m = p.search( '(((( Lots of punctuation )))' )
m.group('word')
# 'Lots'

Распаковка аргументов

Параметры можно передать в функцию в виде списка или словаря и распаковать их автоматически, используя синтаксис * и **.

def draw_point(x, y):
    # do some magic

point_foo = (3, 4)
point_bar = {'y': 3, 'x': 2}

draw_point(*point_foo)
draw_point(**point_bar)

Эта фича языка очень полезна, так как в Python списки, кортежи и словари широко используются в качестве контейнеров.

Динамическое создание типов

Программирование на Python допускает создание новых типов прямо во время выполнения программы.

NewType = type("NewType", (object,), {"x": "hello"})
n = NewType()
print(n.x) # "hello"

Это то же самое, что и:

class NewType(object):
    x = "hello"

n = NewType()
print(n.x) # "hello"

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

Метод словарей get

Если вы обратитесь к несуществующему ключу словаря dict[key], то получите исключение. Эту проблему можно решить с помощью метода dict.get(key), который вернет None для несуществующих ключей. Вторым параметром ему можно передать значение по умолчанию:

dict = { "a": 1, "b": 2 }

print(dict.get("c")) # None
print(dict.get("c", 0)) # 0

Это удобно, например, при арифметических операциях.

Дескрипторы

Атрибуты можно превратить в дескрипторы, изменив их стандартное поведение с помощью методов __get__, __set__ или __delete__. Таким образом можно, например, запретить перезапись или удаление свойства.

Создадим такой дескриптор, используя для удобства декоратор:

class MyDescriptor(object):
    def __init__(self, fget):
        self.fget = fget

    def __get__(self, obj, type):
        print("__get__({}, {})".format(obj, type))
        return self.fget(obj)

class MyClass(object):
    @MyDescriptor
    def foo(self):
        print("Foo!")

obj = MyClass()
obj.foo

# __get__(<__main__.MyClass object ...>, <class '__main__.MyClass'>)
# Foo!

Теперь при обращении через точку к дескриптору foo, управление передается его методу __get__, который сначала печатает строчку с данными дескриптора, а затем вызывает его "родной" геттер (выполняется код функции foo).

Дескрипторы – довольно сложная фича, но вам стоит разобраться в ней, чтобы глубже понимать как работает язык программирования Python.

Doctest: документация + юнит-тестирование

Модуль doctest находит в коде фрагменты, похожие на интерактивные сессии, и выполняет их, чтобы проверить заявленный результат. Фактически, с его помощью можно создать "исполняемую документацию".

Вот официальный пример работы doctest:

"""
Это модуль-пример.

Этот модуль предоставляет одну функцию - factorial().  Например,

>>> factorial(5)
120
"""

def factorial(n):
    """Возвращает факториал числа n, которое является числом >= 0.

    Если результат умещается в int, возвращается int.
    Иначе возвращается long.

    >>> [factorial(n) for n in range(6)]
    [1, 1, 2, 6, 24, 120]
    >>> [factorial(long(n)) for n in range(6)]
    [1, 1, 2, 6, 24, 120]
    >>> factorial(30)
    265252859812191058636308480000000L
    >>> factorial(30L)
    265252859812191058636308480000000L
    >>> factorial(-1)
    Traceback (most recent call last):
        ...
    ValueError: n must be >= 0

    Можно вычислять факториал числа с десятичной частью, если она
    равна 0:
    >>> factorial(30.1)
    Traceback (most recent call last):
        ...
    ValueError: n must be exact integer
    >>> factorial(30.0)
    265252859812191058636308480000000L

    Кроме того, число не должно быть слишком большим:
    >>> factorial(1e100)
    Traceback (most recent call last):
        ...
    OverflowError: n too large
    """

    import math
    if not n >= 0:
        raise ValueError("n must be >= 0")
    if math.floor(n) != n:
        raise ValueError("n must be exact integer")
    if n+1 == n:  # перехватываем значения типа 1e300
        raise OverflowError("n too large")
    result = 1
    factor = 2
    while factor <= n:
        result *= factor
        factor += 1
    return result


if __name__ == "__main__":
    import doctest
    doctest.testmod()

Чтобы увидеть результат, запустите этот модуль прямо из командной строки с флагом -v. Вы получите нечто вроде:

$ python example.py -v
Trying:
factorial(5)
Expecting:
120
ok
Trying:
[factorial(n) for n in range(6)]
Expecting:
[1, 1, 2, 6, 24, 120]
ok

Именованное форматирование строк

В Python 3 для форматирования строк используется метод format:

print("The {} is {}".format('answer', 42))

Передаваемые в строку параметры можно именовать для удобства:

print("The {foo} is {bar}".format(bar=42, foo='answer'))

Поиск модулей

Путь поиска импортируемых модулей в Python выглядит так:

  1. Домашний каталог программы, который может отличаться от текущего рабочего каталога
  2. Адреса из переменной окружения PYTHONPATH.
  3. Каталоги стандартной Python библиотеки, которые устанавливаются автоматически.
  4. Директории, перечисленные в *.pth файлах.
  5. Каталог site-packages, в котором автоматически размещаются все сторонние расширения.

try-except-else

В конструкцию try-except можно добавить также блок else. Он отработает только в случае выполнения кода без ошибок:

try:
    a = float(input("Введите число: "))
    print(100 / a)
except ValueError:
    print ("Это не число!")
except ZeroDivisionError:
    print ("На ноль делить нельзя!")
except:
    print ("Неожиданная ошибка.")
else:
    print ("Код выполнился без ошибок")

Использовать блок else предпочтительнее, чем добавлять дополнительный код в блок try. Это позволяет избежать случайного перехвата исключений, которые не были вызваны кодом, защищенным конструкцией try-except.

Ререйз исключений

Применив оператор raise без параметров внутри обработчика ошибок, вы можете повторно "поднять" пойманное исключение с сохранением его оригинальной трассировки стека. Это полезно, если пойманное исключение должно обрабатываться на верхних уровнях программы:

try:
    some_operation()
except SomeError as e:
    if is_fatal(e):
        raise
    handle_nonfatal(e)

Оригинальную трассировку можно получить с помощью sys.exc_info().

Автодополнение для интерактивного интерпретатора

Одна из немногочисленных неприятных особенностей Python консоли: отсутствие встроенного автодополнения вводимых команд. Эту проблему решает модуль rlcompleter:

try:
    import readline
except ImportError:
    print "Unable to load readline module."
else:
    import rlcompleter
    readline.parse_and_bind("tab: complete")

Теперь с помощью клавиши TAB вы можете быстро подобрать нужные атрибуты:

>>> class myclass:
...    def function(self):
...       print "my function"
... 
>>> class_instance = myclass()
>>> class_instance.<TAB>
class_instance.__class__   class_instance.__module__
class_instance.__doc__     class_instance.function
>>> class_instance.f<TAB>unction()

import this

Главный священный текст любого питониста всегда даст ценный совет, подкинет полезную идею и подбодрит уставшего разработчика. Просто выполните команду import this.

Надеемся, вы узнали что-то новое о возможностях и особенностях Python. Своими открытиями делитесь в комментариях.

Лучшие материалы и книги по Python

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ