Программирование на Python

Модульное программирование: атрибуты и методы

Разбить на страницы
Показывать лекцию целиком

Подводя некоторые итоги, можно отметить, что модуль является контейнером, который может содержать:

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

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

    А может ли метод передать информацию глобальным переменным модуля? Могут ли методы модуля обмениваться информацией, используя глобальные переменные. Ответ - да, могут, но есть одна проблема. Пусть X - имя глобальной переменной, введенное в модуле, Y - локальная переменная, определенная в теле метода. Тогда в теле метода присваивание Y = X допустимо, и локальная переменная Y станет ссылкой на объект, связанный с глобальной переменной. Присваивание X = Y также допустимо, но по правилам Python в теле метода появится локальная переменная с именем X и она станет ссылкой на объект, связанный с локальной переменной Y. На атрибут модуля X присваивание в теле метода никак не повлияет. Так что напрямую изменять атрибуты в теле метода прямым присваиванием нового значения невозможно. Тем не менее есть два способа справиться с поставленной задачей. Первый из них хорошо знаком и подробно обсуждался, когда мы рассматривали процедуру сортировки списка. Напомню: "не продается вдохновенье, но можно рукопись продать". Нельзя изменить связь между именем и объектом, но можно изменить сам объект. Нельзя атрибуту в теле метода присвоить новое значение, но можно изменить объект, связанный с атрибутом. Рассмотрим пример:

    #Пример метода reverse, получающего информацию от атрибута Arr и изменяющего его состояние  
    Arr = [5, 7, 3, 15, 8]
    def reverse():
        n = len(Arr)
        i = 0; j = n - 1
        while(i < j):
            Arr[i], Arr[j] = Arr[j], Arr[i]
            i = i + 1; j = j - 1
    
    def test3():
        print ('Arr before reverse:', Arr )
        reverse()
        print ('Arr after reverse:', Arr )
    
    test3()
    

    Как видите, метод reverse не имеет параметров, получая информацию от атрибута Arr. Но метод не только получает информацию от атрибута, - он изменяет состояние объекта, связанного с атрибутом.

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

    global <кортеж имен глобальных переменных модуля>
    

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

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

     import random
    def MinMax(a):
        """
        Процедура MinMax  вычисляет максимальный и минимальный элементы - 
        случайные границы интервала чисел
        и передает информацию через создаваемые глобальные переменные Min, Max
        """
        global Min, Max
        Min = random.randint(0, a)
        Max = random.randint(Min + 1, 2 * a)
    

    Вызов процедуры MinMax создаст на уровне модуля глобальные переменные, доступные в других функциях. Функция CreateNumber имеет параметр, задающий уровень игры, и использует глобальные переменные Min и Max при вычислении интервала для выбора случайного числа:

    
    def CreateNumber(level):
        """
        Функция возвращает случайное число
        в диапазоне, зависящем от глобальных переменных Min, Max 
        и уровня игры - level
        """
        if level == 1:
            return random.randint(Min, Max)
        if level == 2:
            return random.randint(Min * 20, Max * 20) 
        if level > 2:
            return random.randint(Min * 40, Max * 40)
    

    Построим соответствующий тест:

    def test4():
        print('уровень 1')
        MinMax(10)
        n = CreateNumber(1)
        print('число ', n, 'из интервала ', Min, ' - ', Max)   
        print('уровень 2')
        MinMax(10)
        n = CreateNumber(2)
        print('число ', n, 'из интервала ', Min * 20, ' - ', Max * 20)
        print('уровень 4')
        MinMax(10)
        n = CreateNumber(4)
        print('число ', n, 'из интервала ', Min * 40, ' - ', Max * 40)
    
    test4()
    

    Вот результаты работы:

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

    Многоуровневая вложенность

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

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

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

    nonlocal <кортеж имен охватывающего метода>
    

    Заметьте, имена в теле метода могут иметь три различных статуса: локальные, нелокальные и глобальные.

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

    Ранее мы рассматривали функции высших порядков, которые имеют функции в качестве аргументов. В данном примере я построю функцию f_cover(x), которая в качестве результата возвращает функцию. Естественно, определить возвращаемую функцию в теле функции f_cover(x). Так появляется трехуровневая структура. Вложенная функция f_inside(x) будет иметь параметр и использовать локальную переменную охватывающей функции как глобальную информацию. Более того, вложенная функция будет изменять эту глобальную переменную в качестве побочного эффекта, используя механизм nonlocal переменных. Приводимый код имеет небольшой размер, но стоит его изучить внимательно:

    def f_cover(x):
        """
        Вложенные функции. Nonlocal переменные
        """
        y = x + 7
        def f_inside(x):
            nonlocal y
            y = y + 10
            return x + y    
        return f_inside
    
    

    Давайте построим тест, в котором будет вызвана функция f_cover:

    
    def test5():
        f = f_cover(1)
        res = f(2)
        print("res = ", res)
        res = f(2)
        print("res = ", res)
        res = f(2)
        print("res = ", res)
    test5()
    

    Заметьте, имя функции f_inside - это локальное имя и непосредственно вызвать эту функцию по имени не представляется возможным, но имя f можно связать с функцией, возвращаемой в результате вызова f_cover (по сути с функцией f_inside) и функцию f можно вызывать многократно.

    Вот результат вычислений:

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

    Мы построили пример трехуровневой структуры с использованием nonlocal переменных. Построим теперь содержательный пример трехуровневой структуры, чаще встречающийся на практике. Я уже говорил, что одно из основных назначений модуля - предоставлять сервисы, многократно используемые другими модулями. Классическим примером является пакет math. Сервис реализуется как метод модуля. Если метод сложный, то применяется декомпозиция и в результате появляются более простые методы. Возникает дилемма, на каком уровне определять эти вспомогательные методы, - на уровне модуля или встраивать их в тело метода, для которого строится декомпозиция. Зачастую, второй способ предпочтительнее, поскольку на глобальном уровне видны только методы, представляющие сервис, а вспомогательные методы скрыты (инкапсулированы) и недоступны для вызова.

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

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

    def PolygonSquare(vertices: list)->float:
        """
        Площадь выпуклого многоугольника,
        заданного списком его вершин - vertices
        """
        import math
        def Side (A:tuple, B:tuple)->float:
        
            x1 = A[0]; y1 =A[1]
            x2 = B[0]; y2 =B[1]
            s = math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
            return s
    
        def Geron(s1:float, s2: float, s3:float)-> float:
            p = (s1 + s2 + s3) / 2;
            return math.sqrt(p * (p - s1) * (p - s2) * (p - s3))
        
        def TriangleSquare()->float:
            sideAB = Side(A, B)
            sideAC = Side(A, C)
            sideBC = Side(B, C)
            s = Geron(sideAB, sideBC, sideAC)
            return s
    
        sp = 0
        n = len(vertices)
        A = vertices[0]
        for i in range(n-2):
            B = vertices[i+1]
            C= vertices[i+2] 
            st = TriangleSquare()
            sp += st
        return sp
    

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

    Построим простой тест, вычисляющий площадь квадрата:

    def test6():
        vertices= [(0, 0),(0, 1), (1, 1), (1, 0)]
        square = PolygonSquare(vertices)
        print('площадь мноугольника = ', square)
    test6()
    

    Вот результаты:

    Страницы:

    Подводя некоторые итоги, можно отметить, что модуль является контейнером, который может содержать:

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

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

    А может ли метод передать информацию глобальным переменным модуля? Могут ли методы модуля обмениваться информацией, используя глобальные переменные. Ответ - да, могут, но есть одна проблема. Пусть X - имя глобальной переменной, введенное в модуле, Y - локальная переменная, определенная в теле метода. Тогда в теле метода присваивание Y = X допустимо, и локальная переменная Y станет ссылкой на объект, связанный с глобальной переменной. Присваивание X = Y также допустимо, но по правилам Python в теле метода появится локальная переменная с именем X и она станет ссылкой на объект, связанный с локальной переменной Y. На атрибут модуля X присваивание в теле метода никак не повлияет. Так что напрямую изменять атрибуты в теле метода прямым присваиванием нового значения невозможно. Тем не менее есть два способа справиться с поставленной задачей. Первый из них хорошо знаком и подробно обсуждался, когда мы рассматривали процедуру сортировки списка. Напомню: "не продается вдохновенье, но можно рукопись продать". Нельзя изменить связь между именем и объектом, но можно изменить сам объект. Нельзя атрибуту в теле метода присвоить новое значение, но можно изменить объект, связанный с атрибутом. Рассмотрим пример:

    #Пример метода reverse, получающего информацию от атрибута Arr и изменяющего его состояние  
    Arr = [5, 7, 3, 15, 8]
    def reverse():
        n = len(Arr)
        i = 0; j = n - 1
        while(i < j):
            Arr[i], Arr[j] = Arr[j], Arr[i]
            i = i + 1; j = j - 1
    
    def test3():
        print ('Arr before reverse:', Arr )
        reverse()
        print ('Arr after reverse:', Arr )
    
    test3()
    

    Как видите, метод reverse не имеет параметров, получая информацию от атрибута Arr. Но метод не только получает информацию от атрибута, - он изменяет состояние объекта, связанного с атрибутом.

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

    global <кортеж имен глобальных переменных модуля>
    

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

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

     import random
    def MinMax(a):
        """
        Процедура MinMax  вычисляет максимальный и минимальный элементы - 
        случайные границы интервала чисел
        и передает информацию через создаваемые глобальные переменные Min, Max
        """
        global Min, Max
        Min = random.randint(0, a)
        Max = random.randint(Min + 1, 2 * a)
    

    Вызов процедуры MinMax создаст на уровне модуля глобальные переменные, доступные в других функциях. Функция CreateNumber имеет параметр, задающий уровень игры, и использует глобальные переменные Min и Max при вычислении интервала для выбора случайного числа:

    
    def CreateNumber(level):
        """
        Функция возвращает случайное число
        в диапазоне, зависящем от глобальных переменных Min, Max 
        и уровня игры - level
        """
        if level == 1:
            return random.randint(Min, Max)
        if level == 2:
            return random.randint(Min * 20, Max * 20) 
        if level > 2:
            return random.randint(Min * 40, Max * 40)
    

    Построим соответствующий тест:

    def test4():
        print('уровень 1')
        MinMax(10)
        n = CreateNumber(1)
        print('число ', n, 'из интервала ', Min, ' - ', Max)   
        print('уровень 2')
        MinMax(10)
        n = CreateNumber(2)
        print('число ', n, 'из интервала ', Min * 20, ' - ', Max * 20)
        print('уровень 4')
        MinMax(10)
        n = CreateNumber(4)
        print('число ', n, 'из интервала ', Min * 40, ' - ', Max * 40)
    
    test4()
    

    Вот результаты работы:

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

    Многоуровневая вложенность

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

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

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

    nonlocal <кортеж имен охватывающего метода>
    

    Заметьте, имена в теле метода могут иметь три различных статуса: локальные, нелокальные и глобальные.

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

    Ранее мы рассматривали функции высших порядков, которые имеют функции в качестве аргументов. В данном примере я построю функцию f_cover(x), которая в качестве результата возвращает функцию. Естественно, определить возвращаемую функцию в теле функции f_cover(x). Так появляется трехуровневая структура. Вложенная функция f_inside(x) будет иметь параметр и использовать локальную переменную охватывающей функции как глобальную информацию. Более того, вложенная функция будет изменять эту глобальную переменную в качестве побочного эффекта, используя механизм nonlocal переменных. Приводимый код имеет небольшой размер, но стоит его изучить внимательно:

    def f_cover(x):
        """
        Вложенные функции. Nonlocal переменные
        """
        y = x + 7
        def f_inside(x):
            nonlocal y
            y = y + 10
            return x + y    
        return f_inside
    
    

    Давайте построим тест, в котором будет вызвана функция f_cover:

    
    def test5():
        f = f_cover(1)
        res = f(2)
        print("res = ", res)
        res = f(2)
        print("res = ", res)
        res = f(2)
        print("res = ", res)
    test5()
    

    Заметьте, имя функции f_inside - это локальное имя и непосредственно вызвать эту функцию по имени не представляется возможным, но имя f можно связать с функцией, возвращаемой в результате вызова f_cover (по сути с функцией f_inside) и функцию f можно вызывать многократно.

    Вот результат вычислений:

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

    Мы построили пример трехуровневой структуры с использованием nonlocal переменных. Построим теперь содержательный пример трехуровневой структуры, чаще встречающийся на практике. Я уже говорил, что одно из основных назначений модуля - предоставлять сервисы, многократно используемые другими модулями. Классическим примером является пакет math. Сервис реализуется как метод модуля. Если метод сложный, то применяется декомпозиция и в результате появляются более простые методы. Возникает дилемма, на каком уровне определять эти вспомогательные методы, - на уровне модуля или встраивать их в тело метода, для которого строится декомпозиция. Зачастую, второй способ предпочтительнее, поскольку на глобальном уровне видны только методы, представляющие сервис, а вспомогательные методы скрыты (инкапсулированы) и недоступны для вызова.

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

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

    def PolygonSquare(vertices: list)->float:
        """
        Площадь выпуклого многоугольника,
        заданного списком его вершин - vertices
        """
        import math
        def Side (A:tuple, B:tuple)->float:
        
            x1 = A[0]; y1 =A[1]
            x2 = B[0]; y2 =B[1]
            s = math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
            return s
    
        def Geron(s1:float, s2: float, s3:float)-> float:
            p = (s1 + s2 + s3) / 2;
            return math.sqrt(p * (p - s1) * (p - s2) * (p - s3))
        
        def TriangleSquare()->float:
            sideAB = Side(A, B)
            sideAC = Side(A, C)
            sideBC = Side(B, C)
            s = Geron(sideAB, sideBC, sideAC)
            return s
    
        sp = 0
        n = len(vertices)
        A = vertices[0]
        for i in range(n-2):
            B = vertices[i+1]
            C= vertices[i+2] 
            st = TriangleSquare()
            sp += st
        return sp
    

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

    Построим простой тест, вычисляющий площадь квадрата:

    def test6():
        vertices= [(0, 0),(0, 1), (1, 1), (1, 0)]
        square = PolygonSquare(vertices)
        print('площадь мноугольника = ', square)
    test6()
    

    Вот результаты:

    Вернуться к учебному плану