Генераторы

Создание итератора в Python требует большой работы. Класс (в ООП) должен быть построен с применением методов __iter__() и __next__(), внутренние состояния должны быть сохранены и обновлены, а ошибка StopIteration должна выбрасываться, когда нет возвращаемого значения.

Итерирование в Python

Чтобы избежать этого длительного процесса, генераторы Python могут автоматически создавать итераторы, выполняя все процессы, описанные выше. Генератор — это функция, возвращающая объект (итератор), который можно итерировать (покрывая одно значение за раз).

Функция-генератор определяется как функция, но использует ключевое слово yield вместо обычного return. Если тело функции содержит yield, это автоматически означает, что функция является генератором.

def simple_generator():
     yield 1
     yield 2
     yield 3

for value in simple_generator():
     print(value)

Первые четыре строчки определяют генератор, который будет итеративно возвращать 1, 2 и 3. Последние две строчки демонстрируют итерацию, выводя значения:

1
2
3

Объект-генератор всё ещё требует функцию для схемы объекта, но он должен быть задан равным переменной, в данном случае x.

x = simple_generator()
print(next(x))
print(next(x))
print(next(x))

Например, следующий код создаёт генератор последовательности Фибоначчи, где каждый член является суммой двух предыдущих (0, 1, 1, 2, 3, 5, 8, 13, …).

def fibonacci(limit):
     a, b = 0, 1
     
     while a < limit:
          yield a
          a, b = b, a+b

Числа a и b, используемые для отслеживания последовательности, инициализируются равными 0 и 1 соответственно. Пока a находится ниже предела  —  числа, указанного в качестве параметра функции  — программа возвращает значение a. Затем a и b обновляются одновременно, когда a задано равным b, а b равен самому себе плюс a. Это смещает последовательность на единицу вправо, сохраняя при этом соответствующую информацию (два предыдущих числа) для генерации следующего значения последовательности.

Логика генератора последовательности Фибоначчи

Генератор затем можно итерировать, в данном случае с помощью цикла for:

for i in fibonacci(8):
     print(i)

Итерация будет повторяться до числа 8:

0
1
1
2
3
5

Объектно-ориентированное программирование

Объектно-ориентированное программирование — это особенность Python, которая обеспечивает чистое и организованное хранение методов и переменных. ООП в Python состоит из объектов class, содержащих информацию об этом объекте.

Скажем, мы хотим создать виртуальную собаку на Python. Персональные атрибуты класса хранятся в функции __init__, в которую должен быть включён параметр self, как и другие атрибуты объекта, которые должны быть определены при создании, такие как species и age.

class dog:
     def __init__(self, species, age):
          self.species = species
          self.age = age

Переменные и функции объекта могут вызываться внутри объекта с помощью . с предыдущим элементом, ссылающимся на объект, и элементом после точки, ссылающимся на вызываемый объект. self.species = species задаёт внутреннюю переменную к тому, чем является входной параметр species.

Функции также можно создавать в классе:

class dog:
     def __init__(self, species, age):
          self.species = species
          self.age = age

     def bark(self, call='Woof!'):
          print(call)

     def reveal_information(self):
          print('I am a', self.species)
          print('I am', self.age, 'years old')

Две внутренние функции bark и reveal_information являются методами, выполняемыми и прикрепляемыми классом. Затем мы можем задать переменную pepper классу dpg. Мы должны указать параметры инициализации species и age.

pepper = dog(species='German Shepard', age=3)

Затем можем вызвать атрибуты pepper:

pepper.reveal_information()

Вот вывод:

I am a German Shepard
I am 3 years old
Визуализация объектно-ориентированного класса

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

Замыкания

Замыкания помогают избежать использования глобальных переменных и предоставляют форму скрытия данных, обеспечивая объектно-ориентированное решение проблемы. Когда в классе реализовано мало методов, замыкания могут обеспечить альтернативное и более элегантное решение. Когда количество атрибутов и методов растёт, класс становится более подходящим решением. Следующие критерии демонстрируют замыкания в Python, когда вложенная функция ссылается на значение в своей объемлющей области:

  • существует вложенная функция (функция внутри функции);
  • вложенная функция ссылается на значение, объявленное в замыкающей функции;
  • замыкающая функция возвращает вложенную функцию.
Диаграмма замыкания

Продемонстрируем замыкание на примере функции make_multiplier, которая принимает параметр n и возвращает функцию с этим параметром.

def make_multiplier(n):
    def multiplier(x):
        return x * n
    return multiplier

Создаём функцию, умножающую что-то на 3:

times3 = make_multiplier(3)

Поскольку make_multiplier возвращает функцию, times3 является функцией…

print(times3(9))

возвращающей…

27

потому что 3, умноженное на 9, даёт 27. Замыкание, вероятно, лучший способ выполнить эту задачу в Python.

Встроенное перечисление

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

counter = 0
for item in a_list:
     do_something_with(item)
     do_something_else_with(counter)
     counter += 1

Однако функция Python enumerate() автоматически отслеживает счётчик при каждой итерации, возвращая кортеж, который можно распаковать:

for index, item in enumerate(a_list):
     do_something_with(item)
     do_something_with(index)

Следующий код…

for index, item in enumerate(['a','b','c']):
     print(index,item)

выведет:

0 a
1 b
2 c

Декораторы

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

Скажем, у нас есть функция ordinary(), единственная цель которой — печатать строку “I am an ordinary function.”.

def ordinary():
    print("I am an ordinary function.")

Скажем, мы хотим добавить ещё одно сообщение “I was decorated!”. Мы можем создать функцию decorate(), которая принимает функцию, печатает дополнительное сообщение, затем вызывает исходную функцию, чей объект становится параметром функции decorate(). Процесс добавления строки и вызова оригинальной функции можно сохранить во внутренней функции, чей объект возвращается с помощью decorate().

def decorate(function):
    def inner_function():
        print("I was decorated!")
        function()
    return inner_function

Чтобы декорировать оригинальную функцию ordinary(), вызовем функцию decorate() в исходной функции. Переменная, в которую сохраняются выходные данные decorate(), decorated, является функцией (inner_function в функции decorate()).

decorated = decorate(ordinary)
decorated()

Вызов decorated() возвращает:

I was decorated!
I am an ordinary function.

Декораторы используют символ @ для автоматического декорирования функции.

@decorate
def ordinary():
    print("I am an ordinary function.")

Использование символа @ перед определением функции автоматически декорирует функцию. Она вычисляется так же, как ранее описанный метод декорирования функции.

Несколько декораторов можно связать друг с другом, добавив перед функцией @decorate несколько строк.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Andre Ye: Essential Python Concepts & Structures Any Serious Programmer Needs to Know, Explained