Написано очень много статей, посвящённых интересным возможностям Python. В них идёт речь о распаковке списков и кортежей в переменные, о частичном применении функций, о работе с итерируемыми объектами. Но в Python есть гораздо больше всего интересного. Автор статьи, перевод которой мы сегодня публикуем, говорит, что хочет рассказать о некоторых возможностях Python, которыми он пользуется. При этом описания этих возможностей, подобного тому, которое приведено здесь, ему пока не встречалось. Возможно, что и вы о них тоже ещё нигде не читали.
Очистка входных строковых данных
Задача очистки данных, вводимых пользователем, актуальна практически для любой программы. Часто такая обработка входных данных сводится к преобразованию символов в верхний или нижний регистр. Иногда данные можно очистить с помощью регулярного выражения. Но в случаях, когда задача усложняется, можно применить более удачный способ её решения. Например — такой:
user_input = "Thisnstring hastsome whitespaces...rn"
character_map = {
ord('n') : ' ',
ord('t') : ' ',
ord('r') : None
}
user_input.translate(character_map) # This string has some whitespaces... "
Здесь можно видеть, как пробельные символы "n"
и "t"
заменяются на обычные пробелы, и как символ "r"
удаляется из строки полностью. Это — простой пример, но мы можем его расширить, создавая большие таблицы переназначения символов с использованием пакета unicodedata
и его функции combining()
. Такой подход позволяет убирать из строк всё то, что там не нужно.
Получение срезов итераторов
Если вы попытаетесь получить срез (slice) итератора, то столкнётесь с ошибкой TypeError
, сообщающей о том, что на объект-генератор нельзя оформить подписку. Однако эта проблема поддаётся решению:
import itertools
s = itertools.islice(range(50), 10, 20) # <itertools.islice object at 0x7f70fab88138>
for val in s:
...
Воспользовавшись методом itertools.islice
можно создать объект islice
, который представляет собой итератор, выдающий нужные элементы. Однако здесь важно отметить то, что эта конструкция использует все элементы генератора вплоть до начала среза и все элементы в объекте islice
.
Пропуск начала итерируемого объекта
Иногда нужно работать с файлом, который, как заранее известно, начинается с некоторого числа ненужных строк — вроде строк с комментариями. Для того чтобы пропустить эти строки, можно, снова, прибегнуть к возможностям itertools
:
string_from_file = """
// Author: ...
// License: ...
//
// Date: ...
Actual content...
"""
import itertools
for line in itertools.dropwhile(lambda line: line.startswith("//"), string_from_file.split("n")):
print(line)
Этот код выдаёт лишь строки, находящиеся после блока комментариев, расположенного в начале файла. Такой подход может быть полезен тогда, когда нужно отбросить лишь элементы (в нашем случае — строки) в начале итерируемого объекта, но при этом точное их количество неизвестно.
Функции, поддерживающие только именованные аргументы (kwargs)
Для того чтобы при использовании некоей функции сделать так, чтобы ей можно было бы передавать только именованные аргументы, можно поступить следующим образом:
def test(*, a, b):
pass
test("value for a", "value for b") # TypeError: test() takes 0 positional arguments...
test(a="value", b="value 2") # А так - работает...
Это может быть полезно для того, чтобы улучшить понятность кода. Как видите, наша задача легко решается при помощи использования аргумента *
перед списком именованных аргументов. Здесь, что вполне очевидно, можно использовать и позиционные аргументы — в том случае, если поместить их до аргумента *
.
Создание объектов, поддерживающих выражение with
Все знают о том, как, например, открыть файл, или, возможно, как установить блокировку с использованием оператора with
. Но можно ли самостоятельно реализовать механизм управления блокировками? Да, это вполне реально. Протокол управления контекстом исполнения реализуется с использованием методов __enter__
и __exit__
:
class Connection:
def __init__(self):
...
def __enter__(self):
# Инициализируем соединение...
def __exit__(self, type, value, traceback):
# Закрываем соединение...
with Connection() as c:
# __enter__() executes
...
# conn.__exit__() executes
Это — наиболее распространённый способ реализации возможностей менеджера контекста в Python, но то же самое можно сделать и проще:
from contextlib import contextmanager
@contextmanager
def tag(name):
print(f"<{name}>")
yield
print(f"</{name}>")
with tag("h1"):
print("This is Title.")
Здесь протокол управления контекстом реализован с использованием декоратора contextmanager
. Первая часть функции tag
(до yield
) выполняется при входе в блок with
. Затем выполняется сам этот блок, а после этого выполняется оставшаяся часть функции tag
.
Экономия памяти с помощью __slots__
Если вы когда-нибудь писали программы, которые создают по-настоящему большие количества экземпляров некоего класса, то вы могли заметить, что таким программам неожиданно может понадобиться очень много памяти. Происходит это из-за того, что Python использует словари для представления атрибутов экземпляров классов. Это хорошо сказывается на производительности, но, с точки зрения потребления памяти, это неэффективно. Обычно, правда, проблем эта особенность не вызывает. Однако если вы столкнулись в подобной ситуации с нехваткой памяти — можете попробовать воспользоваться атрибутом __slots__
:
class Person:
__slots__ = ["first_name", "last_name", "phone"]
def __init__(self, first_name, last_name, phone):
self.first_name = first_name
self.last_name = last_name
self.phone = phone
Здесь, когда мы объявляем атрибут __slots__
, Python использует для хранения атрибутов не словарь, а маленький массив фиксированного размера. Это серьёзно сокращает объём памяти, необходимый для каждого из экземпляров класса. У применения атрибута __slots__
есть и некоторые недостатки. Так, пользуясь им, мы не можем объявлять новые атрибуты, мы ограничены только теми, которые имеются в __slots__
. Кроме того, классы c атрибутом __slots__
не могут использовать множественное наследование.
Ограничение использования процессора и памяти
Если вместо того, чтобы оптимизировать программу, или улучшить то, как она пользуется процессором, вам нужно просто задать жёсткое ограничение на доступные ей ресурсы, можно воспользоваться соответствующей библиотекой:
import signal
import resource
import os
# Для ограничения процессорного времени
def time_exceeded(signo, frame):
print("CPU exceeded...")
raise SystemExit(1)
def set_max_runtime(seconds):
# Устанавливаем обработчик signal и задаём лимит ресурса
soft, hard = resource.getrlimit(resource.RLIMIT_CPU)
resource.setrlimit(resource.RLIMIT_CPU, (seconds, hard))
signal.signal(signal.SIGXCPU, time_exceeded)
# Для ограничения использования памяти
def set_max_memory(size):
soft, hard = resource.getrlimit(resource.RLIMIT_AS)
resource.setrlimit(resource.RLIMIT_AS, (size, hard))
Тут показано ограничение процессорного времени и объёма памяти. Для того чтобы ограничить использование программой процессора, мы сначала получаем значения нежёсткого (soft) и жёсткого (hard) лимитов для конкретного ресурса (RLIMIT_CPU
). Затем мы устанавливаем лимит, используя некое число секунд, задаваемое аргументом seconds
, и ранее полученное значение жёсткого лимита. После этого мы регистрируем обработчик signal
, который, при превышении выделенного программе процессорного времени, инициирует процедуру выхода. В случае с памятью, мы, опять же, получаем значения для нежёсткого и жёсткого лимитов, после чего устанавливаем ограничение с помощью метода setrlimit
, которому передаём размер ограничения (size
) и ранее полученное значение жёсткого лимита.
Управление тем, что может быть импортировано из модуля, а что — нет
В некоторых языках имеются предельно чёткие механизмы экспорта из модулей переменных, методов, интерфейсов. Например — в Golang экспортируются лишь сущности, имена которых начинаются с большой буквы. В Python же экспортируется всё. Но лишь до тех пор, пока не используется атрибут __all__
:
def foo():
pass
def bar():
pass
__all__ = ["bar"]
В вышеприведённом примере экспортирована будет лишь функция bar
. А если оставить атрибут __all__
пустым, то из модуля не будет экспортироваться вообще ничего. При попытке импорта чего-либо из такого модуля будет выдана ошибка AttributeError
.
Упрощение создания операторов сравнения
Существует немало операторов сравнения. Например — __lt__
, __le__
, __gt__
, __ge__
. Мало кому понравится перспектива их реализации для некоего класса. Можно ли как-то упростить эту скучную задачу? Да, можно — с помощь декоратора functools.total_ordering
:
from functools import total_ordering
@total_ordering
class Number:
def __init__(self, value):
self.value = value
def __lt__(self, other):
return self.value < other.value
def __eq__(self, other):
return self.value == other.value
print(Number(20) > Number(3))
print(Number(1) < Number(5))
print(Number(15) >= Number(15))
print(Number(10) <= Number(2))
Декоратор functools.total_ordering
используется здесь для упрощения процесса реализации упорядочения экземпляров класса. Для обеспечения его работы нужно лишь чтобы были объявлены операторы сравнения __lt__
и __eq__
. Это — тот минимум, который нужен декоратору для конструирования остальных операторов сравнения.
Итоги
Нельзя сказать, что всё то, о чём я тут рассказал, совершенно необходимо в повседневной работе каждого Python-программиста. Но некоторые из приведённых здесь методик, время от времени, могут оказываться очень кстати. Они, кроме того, способны упростить решение задач, для обычного решения которых может потребоваться много кода и большой объём однообразной работы. Кроме того, мне хотелось бы отметить то, что всё, о чём шла речь, является частью стандартной библиотеки Python. Мне, честно говоря, некоторые из этих возможностей кажутся чем-то довольно-таки неожиданным для стандартной библиотеки. Это наводит на мысль о том, что если некто собирается реализовать в Python что-то, кажущееся не вполне обычным, ему стоит сначала хорошо порыться в стандартной библиотеке. Если того, что нужно, там сразу найти не удаётся, то, возможно, стоит ещё раз, очень внимательно, там покопаться. Правда, если и тщательный поиск успехом не увенчался — то, скорее всего, того что нужно там действительно нет. А если это так — тогда стоит обратиться к библиотекам сторонних разработчиков. В них это точно можно будет найти.
Уважаемые читатели! Знаете ли вы о каких-нибудь стандартных возможностях Python, которые на первый взгляд могут показаться довольно-таки необычными для того, чтобы называться «стандартными»?
Автор: ru_vds