Part 11

Еще о генераторах

Списки, пожалуй, самая распространенная цель для генераторов, но генераторы работают с любой последовательностью элементов, включая строки. Как и в примерах со списками из предыдущего раздела, если генератор списка выполняется для строки, элементы (то есть символы) строки берутся по одному, обрабатываются согласно заданному выражению и сохраняются в списке.

name = "Peter Python"

uppercased = [character.upper() for character in name]
print(uppercased)
Пример вывода

['P', 'E', 'T', 'E', 'R', ' ', 'P', 'Y', 'T', 'H', 'O', 'N']

Результат действительно является списком, как и задается квадратными скобками вокруг выражения генератора. Если бы нам нужна была строка, можно было бы использовать строковый метод join, чтобы преобразовать список в строку. Помните, метод вызывается у строки, которую мы хотим использовать как "клей" между символами. Рассмотрим несколько примеров:

name = "Peter"
char_list = list(name)
print(char_list)

print("".join(char_list))
print(" ".join(char_list))
print(",".join(char_list))
print(" and ".join(char_list))
Пример вывода

['P', 'e', 't', 'e', 'r'] Peter P e t e r P,e,t,e,r P and e and t and e and r

Генераторы списков и метод join упрощают создание новых строк на основе других строк. Например, мы могли бы создать строку, содержащую только гласные из другой строки:

test_string = "Hello there, this is a test!"

vowels = [character for character in test_string if character in "aeiou"]
new_string = "".join(vowels)

print(new_string)
Пример вывода

eoeeiiae

В примере выше генератор списка и метод join находятся на отдельных строках, но их можно объединить в одно выражение:

test_string = "Hello there, this is a test!"

vowel_string = "".join([character for character in test_string if character in "aeiou"])

print(vowel_string)

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

sentence = "Sheila keeps on selling seashells on the seashore"

sentence_no_initials = " ".join([word[1:] for word in sentence.split()])
print(sentence_no_initials)
Пример вывода

heila eeps n elling eashells n he eashore

Разберем это пошагово:

  • word[1:] извлекает подстроку со второго символа (с индексом 1) и дальше
  • sentence.split() разделяет предложение на части по заданному символу. В данном случае аргумент методу не передан, поэтому по умолчанию предложение делится по пробельным символам
  • " ".join() объединяет элементы списка в новую строку, используя пробел между элементами

Более традиционный итеративный подход мог бы выглядеть так:

sentence = "Sheila keeps on selling seashells on the seashore"

word_list = []
words = sentence.split()
for word in words:
    word_no_initials = word[1:]
    word_list.append(word_no_initials)

sentence_no_initials = " ".join(word_list)

print(sentence_no_initials)
Loading

Собственные классы и генераторы

Генераторы могут быть полезным инструментом для обработки или создания экземпляров собственных классов, как мы увидим в следующих примерах.

Сначала рассмотрим класс Country, который является простой моделью одной страны с атрибутами имени и населения. В главной функции ниже мы сначала создаем несколько объектов Country, а затем используем генератор списка, чтобы выбрать только те, население которых больше пяти миллионов.

class Country:
    """ This class models a single country with population """
    def __init__(self, name: str, population: int):
        self.name = name
        self.population = population

if __name__ == "__main__":
    finland = Country("Finland", 6000000)
    malta = Country("Malta", 500000)
    sweden = Country("Sweden", 10000000)
    iceland = Country("Iceland", 350000)

    countries = [finland, malta, sweden, iceland]

    bigger_countries = [country.name for country in countries if country.population > 5000000]
    for country in bigger_countries:
        print(country)
Пример вывода

Finland Sweden

В генераторе списка выше мы выбрали из объектов Country только атрибут name, поэтому содержимое списка можно было вывести напрямую. Можно было бы также создать новый список самих стран и обращаться к атрибуту name в цикле for. Это было бы полезно, если тот же список стран использовался бы позже в программе или если бы в цикле for нам также был нужен атрибут population:

if __name__ == "__main__":
    finland = Country("Finland", 6000000)
    malta = Country("Malta", 500000)
    sweden = Country("Sweden", 10000000)
    iceland = Country("Iceland", 350000)

    countries = [finland, malta, sweden, iceland]

    bigger_countries = [country for country in countries if country.population > 5000000]
    for country in bigger_countries:
        print(country.name, country.population)

В следующем примере у нас есть класс RunningEvent, который моделирует отдельное соревнование по бегу с атрибутами длины и названия забега. Мы будем использовать генераторы списков для создания объектов RunningEvent на основе списка дистанций.

Параметр name имеет значение по умолчанию в конструкторе класса RunningEvent, поэтому нам не нужно передавать имя в качестве аргумента.

class RunningEvent:
    """ The class models a foot race event of a length of n metres  """
    def __init__(self, length: int, name: str = "no name"):
        self.length = length
        self.name = name

    def __repr__(self):
        return f"{self.length} m. ({self.name})"

if __name__ == "__main__":
    lengths = [100, 200, 1500, 3000, 42195]
    events = [RunningEvent(length) for length in lengths]

    # Print out all events
    print(events)

    # Pick one from the list and give it a name
    marathon = events[-1] # the last item in the list
    marathon.name = "Marathon"

    # Print out everything again, including the new name
    print(events)
Пример вывода

[100 m. (no name), 200 m. (no name), 1500 m. (no name), 3000 m. (no name), 42195 m. (no name)] [100 m. (no name), 200 m. (no name), 1500 m. (no name), 3000 m. (no name), 42195 m. (Marathon)]

Теперь выясним, что делает последовательность элементов пригодной для генератора. В предыдущей части мы узнали, как сделать собственные классы итерируемыми. Именно эта же возможность позволяет использовать и генераторы списков. Если ваш собственный класс итерируемый, его можно использовать как основу для выражения генератора списка. Следующие определения классов скопированы напрямую из части 10:

class Book:
    def __init__(self, name: str, author: str, page_count: int):
        self.name = name
        self.author = author
        self.page_count = page_count

class Bookshelf:
    def __init__(self):
        self._books = []

    def add_book(self, book: Book):
        self._books.append(book)

    # This is the iterator initialization method
    # The iteration variable(s) should be initialized here
    def __iter__(self):
        self.n = 0
        # the method returns a reference to the object itself as 
        # the iterator is implemented within the same class definition
        return self

    # This method returns the next item within the object
    # If all items have been traversed, the StopIteration event is raised
    def __next__(self):
        if self.n < len(self._books):
            # Select the current item from the list within the object
            book = self._books[self.n]
            # increase the counter (i.e. iteration variable) by one
            self.n += 1
            # return the current item
            return book
        else:
            # All books have been traversed
            raise StopIteration

# Test your classes
if __name__ == "__main__":
    b1 = Book("The Life of Python", "Montague Python", 123)
    b2 = Book("The Old Man and the C", "Ernest Hemingjavay", 204)
    b3 = Book("A Good Cup of Java", "Caffee Coder", 997)

    shelf = Bookshelf()
    shelf.add_book(b1)
    shelf.add_book(b2)
    shelf.add_book(b3)

    # Create a list containing the names of all books
    book_names = [book.name for book in shelf]
    print(book_names)
Loading
Loading

Генераторы и словари

В генераторах нет ничего внутренне "списочного". Результат является списком потому, что выражение генератора заключено в квадратные скобки, которые обозначают список Python. Генераторы так же хорошо работают со словарями Python, если использовать фигурные скобки. Помните, однако, что словарям нужны пары ключ-значение. При создании словаря, в том числе с генераторами, нужно указать и ключ, и значение.

Основой генератора может быть любая итерируемая последовательность: список, строка, кортеж, словарь, любой ваш собственный итерируемый класс и так далее.

В следующем примере мы используем строку как основу словаря. Словарь содержит все уникальные символы строки вместе с количеством их появлений:

sentence = "hello there"

char_counts = {character : sentence.count(character) for character in sentence}
print(char_counts)
Пример вывода

{'h': 2, 'e': 3, 'l': 2, 'o': 1, ' ': 1, 't': 1, 'r': 1}

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

{<key expression> : <value expression> for <item> in <series>}

В завершение этого раздела снова посмотрим на факториалы. На этот раз мы сохраняем результаты в словаре. Само число является ключом, а значение - результат факториала из нашей функции:

def factorial(n: int):
    """ The function calculates the factorial n! for integers above zero """
    k = 1
    while n >= 2:
        k *= n
        n -= 1
    return k

if __name__ == "__main__":
    numbers = [-2, 3, 2, 1, 4, -10, 5, 1, 6]
    factorials = {number : factorial(number) for number in numbers if number > 0}
    print(factorials)
Пример вывода

{3: 6, 2: 2, 1: 1, 4: 24, 5: 120, 6: 720}

Loading
Loading
Вы дошли до конца этого раздела! Перейти к следующему разделу:

Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.

В этой части:
  1. 1. Генераторы списков

  2. 2. Еще о генераторах

  3. 3. Рекурсия

  4. 4. Еще примеры рекурсии