20 малоизвестных фич и особенностей Python
Небольшая подборка полезных фич и особенностей 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 выглядит так:
- Домашний каталог программы, который может отличаться от текущего рабочего каталога
- Адреса из переменной окружения PYTHONPATH.
- Каталоги стандартной Python библиотеки, которые устанавливаются автоматически.
- Директории, перечисленные в *.pth файлах.
- Каталог 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
.