НОЧУ ДПО "Национальный открытый университет "ИНТУИТ"
Опубликован: 24.01.2021 | Доступ: свободный | Студентов: 2489 / 106 | Длительность: 03:57:00
Лекция 32:

Заключение

< Лекция 1 || Лекция 32

Смотреть на youtube

Проект для лекции Lecture11.rar.

Это заключительная лекция нашего курса "Основы программирования на языке Python". В предыдущих лекциях мы показали, что Python является мощным языком программирования. Он обладает такими встроенными структурами данных как списки, кортежи, множества, словари, файлы. В большинстве языков программирования такие структуры определены на уровне дополнительных библиотек, не являясь частью языка программирования. Управляющие структуры языка Python - структуры выбора, организации циклов, определение процедур и функций, рекурсия - не уступают возможностям других языков программирования. Владея этими средствами, можно создавать проекты на первом уровне программирования - на уровне программирования в процедурах и функциях.

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

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

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

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

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

Если мы создаем приложение на чистом Python, то трудно рассчитывать, что оно будет столь же эффективным по времени выполнения как приложение, написанное на компилируемых языках, например, C++, C# или Java. Приложение Python выполняется в интерпретируемом режиме. Интерпретатор имеет свои достоинства. Можно выполнять приложение в пошаговом режиме, следить за ходом вычислений. Платой за это является время. Конечно, мы привыкли, что обычно при создании проекта на других языках отладка приложения ведется в режиме интерпретатора, а отлаженное приложение компилируется и выполняется как исполняемый (exe) файл. У Python компилятора нет. Здесь эффективность выполнения достигается другими средствами.

Давайте посмотрим, насколько Python проигрывает по времени выполнения на классических задачах.

Для проведения исследований я создал новый проект и добавил в него класс Linear, в который поместил несколько классических методов. Начну с двух методов, позволяющих создать вектор и матрицу, заполненные целыми случайными числами из заданного диапазона. Для получения случайных чисел импортирую пакет random. Роль вектора играет список, содержащий элементы вектора, а роль прямоугольной матрицы - список списков, элементы внешнего списка являются списками, задающими строки матрицы.

Вот начальный код этого класса:

class Linear(object):
     """ 
    Методы создания random матриц
    Методы линейной алгебры    
    Методы сортировки массивов
    Все методы декорированы декоратором timer
    """

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

def timer(repeat = 1):
        """ Обертка декоратора timer """
        def decor_timer(func):
            """
            измеряет время работы функции
            при вызове функции repeat раз
            """
            from time import time
            def wrapper(*args, **kwargs):
                start = time()
                for i in range(repeat):
                    res = func(*args, **kwargs)
                fin = time()
                L = []
                L.append(fin - start)
                L.append(res)
                return L
            return wrapper
        return decor_timer

Построим теперь два метода, генерирующие создание вектора и матрицы заданных размеров, заполненных случайными числами:

   @timer(repeat = 10)
    def CreateVector(self, n:int, min:int, max:int)->list:
        """
        Создание random вектора (списка) размера n
        Элементы целые числа из диапазона [min, max]
        """
        from random import randint
        return [randint(min, max) for i in range(n)] 

    @timer(repeat = 10)    
    def CreateMatrix(self, n:int, m:int, min:int, max:int)->list:
        """
        Создание random матрицы (списка) размера n * m
        Элементы целые числа из диапазона [min, max]
        """
        from random import randint
 return [[randint(min, max) for j in range(m)] for i in range(n)]

Методы декорированы декоратором timer. Реализация методов использует инструмент генератор, что повышает эффективность методов.

Наряду с классом Linear я построил тестирующий модуль, в котором создаю тесты, позволяющие выполнять методы класса Linear. Вот первый тест, позволяющий измерить время создания вектора:

def test1():
    lin = Linear()
    L = lin.CreateVector(10000, -100, 100)
    print('Время создания вектора размера 10000: ', L[0])
    L = lin.CreateVector(100000, -100, 100)
    print('Время создания вектора размера 100000: ', L[0])
    L = lin.CreateVector(1000000, -100, 100)
    print('Время создания вектора размера 1000000: ', L[0]) 

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

Таблица 1. Время создания0 вектора (Python, C#)
n 10000 100000 1000000
vector Python 0.094 0.872 8.838
vector C 0,010 0,020 0,201

Тест, позволяющий вычислить время создания матрицы, не привожу, поскольку он является понятной вариацией теста 1.

В таблице 2 показано время десятикратного создания квадратной матрицы в Python и в C#.

Таблица 2. Время создания матрицы (Python, C#)
n 100 500 1000
matrix Python 0,093 2,161 8,700
matrix C# 0,003 0,056 0,226

Анализ результатов производительности показывает, что Python проигрывает C# примерно два порядка и это замедление становится ощутимым при создании векторов и матриц большого размера.

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

@timer(repeat = 1)    
    def BubbleSort(self, ar):
        """ Алгоритм пузырьковой сортировки """
        n = len(ar)
        for i in range(n-1):
            for j in range(n-1, i, -1):
                if ar[j] < ar[j-1]:
                    ar[j], ar[j-1] = ar[j-1], ar[j]

Простой алгоритм квадратичной сложности. В таблице 3 приведены соответствующие оценки времени.

Таблица 3. Время пузырьковой сортировки массива (Python, C#)
2 100 500 1000 10000
BubbleSort Python 0.001 0.016 0.070 6.969
BubbleSort C# 0.0001 0.0002 0.002 0.288

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

def QuickSort(self, ar, start, finish):
        if start < finish:
            left, right, mid = start, finish, (start + finish)//2
            item = ar[mid]
            while left <= right:
                while ar[left] < item: left+=1
                while ar[right] > item: right -=1
                if left <= right:
                    ar[left], ar[right] = ar[right], ar[left]
                    left +=1; right -=1
            self.QuickSort(ar, start, right)
            self.QuickSort(ar, left, finish)
    
   
    def QSort(self, ar):
        from time import time
        start = time()
        self.QuickSort(ar, 0, len(ar) - 1)
        finish = time()
        print('QuickSort: n = ', len(ar), ' time = ', finish - start)
    

Заметьте, декорировать рекурсивную функцию не следует из-за многократного декорирования всех внутренних вызовов. Поэтому измерение времени встроено непосредственно в нерекурсивную обертку рекурсивного метода сортировки.

Коды тестов я уже не привожу, поскольку они строятся по одному образцу. В таблице 4 приведены соответствующие оценки времени.

Таблица 4. Время быстрой сортировки массива (Python, C#)
n 1000 10000 100000 1000000
QuickSort Python 0.001 0.017 0.214 2.59
QuickSort C# 0.00001 0.001 0.00903 0.098

Заметьте, что времена порядка 0,001 сравнимы с погрешностью измерения времени. Отрадно отметить, что интерпретатор Python хорошо справляется с рекурсией и проигрыш Python не возрос в сравнении с нерекурсивным алгоритмом.

В заключение сравнения относительных оценок производительности рассмотрим алгоритм умножения матриц, имеющий сложность O(n^3) и работающий с двумерным массивом (списком списков в нашей реализации).

Вот код соответствующего алгоритма:

@timer(repeat = 1) 
    def MultMatr(self, A, B):
        """
        Умножение матриц:  C = A * B
        """ 
        n = len(A)
        p = len(A[0]) 
        m = len(B[0])
        C = []
        for i in range(n):
            v = []
            for j in range(m):
                item = 0
                for k in range((p)):
                    item += A[i] [k] * B[k][j]
                v.append(item)
            C.append(v)
        return C

В таблице 5 приведены оценки времени умножения двух квадратных матриц размера n*n.

Таблица 5. Время умножения матриц (Python, C#)
n 100 200 500
Multmatr Python 0.124 0.946 16.211
Multmatr C# 0.006 0.046 0.814

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

Как эффективность выполнения достигается в Python

Что же предлагает Python, когда время выполнения критично. Одно из решений состоит в том, что для некоторых важных задач Python предлагает эффективно реализованные методы. Например, для сортировки структур данных Python предлагает функцию sorted, применимую ко всем итерируемым объектам, которая по эффективности сравнима с эффективностью быстрой сортировки массива, написанной на языке C#.

Главное достижение Python, позволяющее создавать эффективные приложения для решения сложных и важных задач - это работа с пакетами. Для Python написано большое число пакетов для решения задач в самых разных прикладных областях:

  • Извлечение смысла из текстов на естественных языках (задачи NLP - Natural Language Processing).
  • Нейронные сети и глубокое обучение.
  • Извлечение знаний из данных (методы Data Mining)
  • Многие другие прикладные задачи.

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

Многие пакеты встроены в стандартную библиотеку пакетов Python, другие легко добавляются в библиотеку сторонних модулей. Существующий и постоянно расширяющийся набор пакетов Python является гарантией долгой жизни этого языка.

На этой позитивной ноте я заканчиваю курс "Основы программирования на языке Python". Надеюсь, курс будет полезен для тех, кто создает проекты Python как на уровне программирования в процедурах и функциях, так и на уровне программирования в модулях и классах. Для перехода на следующий уровень требуется отдельный курс "Программирование в пакетах Python".

< Лекция 1 || Лекция 32
Алексей Авилов
Алексей Авилов

Неужели не нашлось русских специалистов, чтобы записать курс по пайтону ? Да, можно включить переводчик и слушать с переводом, но это что? Это кто-то считает хорошим и понятным курсом для начинающих? 

Елена Лаптева
Елена Лаптева

Думаю. что не смогу его закончить. Хотелось предупредить других - не тратьте зря время, ищите другой курс.