company_banner

Полезные советы по Python, которых вы ещё не встречали

Original author: Martin Heinz
  • Translation
Написано очень много статей, посвящённых интересным возможностям Python. В них идёт речь о распаковке списков и кортежей в переменные, о частичном применении функций, о работе с итерируемыми объектами. Но в Python есть гораздо больше всего интересного. Автор статьи, перевод которой мы сегодня публикуем, говорит, что хочет рассказать о некоторых возможностях Python, которыми он пользуется. При этом описания этих возможностей, подобного тому, которое приведено здесь, ему пока не встречалось. Возможно, что и вы о них тоже ещё нигде не читали.



Очистка входных строковых данных


Задача очистки данных, вводимых пользователем, актуальна практически для любой программы. Часто такая обработка входных данных сводится к преобразованию символов в верхний или нижний регистр. Иногда данные можно очистить с помощью регулярного выражения. Но в случаях, когда задача усложняется, можно применить более удачный способ её решения. Например — такой:

user_input = "This\nstring has\tsome whitespaces...\r\n"

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, которые на первый взгляд могут показаться довольно-таки необычными для того, чтобы называться «стандартными»?


RUVDS.com
RUVDS – хостинг VDS/VPS серверов

Comments 7

    +4
    itertools.islice(range(50), 10, 20)

    Забавно, но это плохой пример. Во втором питоне результатом range(50) будет список, и срез его можно, очевидно получить сразу. А в Python3 (хотя не уверен во всех ли версиях) результат range(50) — это хоть и генератор, но особый, с поддержкой протокола срезов.
    В таком вот примере следовало бы вместо range(50) использовать (i**2 for i in range(50)). Квадрат для пущего подавления бессмысленности примера.

      +1
      Для обеспечения его работы нужно лишь чтобы были объявлены операторы сравнения lt и eq. Это — тот минимум, который нужен декоратору для конструирования остальных операторов сравнения.

      На самом деле достаточно, чтобы была объявлена любая пара методов, на основе которой можно восстановить остальные. К примеру это может быть __ne__ и __ge__.

        0
        Действительно, где я их еще мог раньше увидеть, cовершенно свежие и новые идеи
          0
          Если вы попытаетесь получить срез (slice) итератора, то столкнётесь с ошибкой TypeError, сообщающей о том, что на объект-генератор нельзя оформить подписку.

          Хочу оформить подписку на объект-генератор. Как я могу это сделать?

            +3
            character_map = {
             ord('\n') : ' ',
             ord('\t') : ' ',
             ord('\r') : None
            }
            Можно так записать:
            character_map = str.maketrans('\n\t', '  ', '\r')

              +1
              В вышеприведённом примере экспортирована будет лишь функция bar. А если оставить атрибут __all__ пустым, то из модуля не будет экспортироваться вообще ничего. При попытке импорта чего-либо из такого модуля будет выдана ошибка AttributeError.

              Разве? Насколько мне известно, по полному имени функцию (сабмодуль/класс/итд) можно импортировать вне зависимости от __all__, он влияет только на import * from bar. В доке я тоже не нашел ничего подобного.
                +1
                На сайте с официальной документацией по contextlib размещен пример создания менеджера контекста с помощью декоратора @contextmanager. Вы не обозначили возмонжости, которые он предоставляет.

                from contextlib import contextmanager
                
                @contextmanager
                def tag(some_val):
                  print(some_val)
                  try:
                    yield 2
                  except:
                    print("was exception!")
                  finally:
                    print("end")
                
                with tag("test") as val:
                  print(val)
                  a = 2 / 0
                


                Действительно, при входе в блок with выполняется код до оператора yield. Но вы не обозначили, что с помощью yield можно вернуть объект на который можно сослаться с помощью оператора as внутри блока with.
                Также, необходимо обернуть оператор yield в блок try/except/finally. finally является аналогом метода __exit__ и гарантирует нам, что при любом выходе из блока with выполнится завершающий код (например, закрытие db). Блок except необходим для обработки исключений возникших внутри блока with.

                Only users with full accounts can post comments. Log in, please.