20 малоизвестных фич и особенностей Python - Библиотека программиста

20 малоизвестных фич и особенностей Python - Библиотека программиста

proglib.io

Небольшая подборка полезных фич и особенностей 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.func_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

Фигурные скобки

Python использует для форматирования кода не скобки, как C-подобные языки (Java, C#, PHP), а табуляцию. Если такой синтаксис вам непривычен, используйте пакет braces из модуля __future__.

from __future__ import braces

Эта фича очень спорная. Настоящие питонисты возмущены самим наличием подобной 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

Интересуетесь программированием на Python?

Подпишитесь на нашу рассылку, чтобы получать больше интересных материалов:

И не беспокойтесь, мы тоже не любим спам. Отписаться можно в любое время.

Report Page