Программирование и научные вычисления на языке Python/§15

Материал из Викиверситета

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


Простые классы функций[править]

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

.

Здесь y является функций времени t и кроме того, зависит от других параметров v0 и g. Мы могли бы придумать какое-то новое обозначение, вроде y(t; v0, g), чтобы показать, что t является независимой переменной, а v0 и g задаваемыми параметрами. При этом, строго говоря для Земли g, гравитационная постоянная, неизменна, то есть правильнее было бы писать 'y(t; v0). В общем случае, у нас может иметься функция, которая будет записываться f(x; p1, ..., pn).

Как нам лучше интерпретировать такие математические функции в виде программных? Первое очевидное решение получать и переменные, и изменяемые параметры как аргументы обычной функции:


def y(t, v0):
    g = 9.81
    return  v0*t - 0.5*g*t**2


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

и записывается в виде кода


def diff(f, x, h=1E-10):
    return (f(x+h) - f(x))/h


И наша ясная функция diff легко работает с функциями, принимающими один аргумент:


def h(t):
    return t**4 + 4*t

dh = diff(h, 0.1)

from math import sin, pi
x = 2*pi
dsin = diff(sin, x, h=1E-9)


Но, к несчастью, diff не будет работать с нашей функцией y(t, v0). Вызов diff(y, t) приведет к ошибке в функции diff, поскольку дифференцируемая функция должна принимать лишь один агумент, а принимает два.

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

Плохое решение: Глобальные переменные[править]

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


def y(t):
    g = 9.81
    return  v0*t - 0.5*g*t**2


Но поскольку v0 не определено, то вызов функции требует того, чтобы переменная была заранее определена и тогда мы уже можем определить значение для производной:


v0 = 3
dy = diff(y, 1)


Но использование глобальных переменных в этом случае это плохой стиль программирования. Почему это плохо, можно проиллюстрировать на примере когда нам нужно использовать разные версии одной функции. Например, мы бросаем мячик вверх со скоростями 1 и 5 м/с. Каждый раз, когда мы вызываем y, нам понадобиться задавать перед ним новое значение v0:


v0 = 1; r1 = y(t)
v0 = 5; r2 = y(t)


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

Итак, есть ли лекарство от нашей болезни? Ответ: да, и рецепт его расписан ниже.

Представление функции в виде класса[править]

Класс заключает в себе набор переменных (данных) и набор функций, связанных в единое целое. Переменные видны изнутри класса всем его функциям. То есть они «глобальные» для функций своего класса. Класс похож на модуль, но находящийся в тексте самой программы. Но при этом по технике его использования он существенно отличается. Например, вы можете создать множество копий одного класса, в то время как модуль выступает в единственном числе. Когда мы получше познакомимся с классами, вы и сами увидите схожие моменты и отличия. А сейчас продолжим рассмотрение нашего примера.

Обращаясь к нашей функции y(t; v0) мы можем сказать, что переменные v0 и g определяют данные, а t служит аргументом некоторой функции Python value(t).

Программист, практикующий классы, соберет данные v0 и g и функцию value(t) вместе в один класс. К тому же класс обычно содержит и другую функцию называемую конструктором (constructor) для инициализации данных. Конструктор всегда носит имя __init__. Каждый класс имеет имя, которое традиционно начинают с большой буквы, поэтому для нашего класса мы выберем имя Y, соотнося его таким образом с y для математической функции.

Реализация[править]

Законченный код для нашего класса Y выглядит следующим образом:


class Y:
    def __init__(self, v0):
        self.v0 = v0
        self.g = 9.81

    def value(self, t):
        return self.v0*t - 0.5*self.g*t**2


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


Использование[править]

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

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


y = Y(3)


Казалось бы, мы вызвали класс Y как будто это обычная функция. Однако, Y(3) автоматически представляется Python как вызов конструктора __init__ в классе Y. Аргументы при вызове, здесь это только число 3, всегда принимаются как аргументы функции-конструктора __init__ следующие после всегда стоящего на первом месте аргумента self.

Имея на руках экземпляр y, мы можем узнать значение y(t=0.1; v0=3) с помощью инструкции


v = y.value(0.1)


Теперь, поскольку происходит вызов value, аргумент self оказывается в стороне. Чтобы обратиться к функциям или переменным класса, нужно указывать префикс этой функции или имени переменной. Например, так мы можем вывести значение v0 экземпляра y:


print y.v0


В этом случае на выходе мы увидим число 3.

Кроме термина «экземпляр» для объектов, рожденных классом, говорят о функциях класса как методах и переменных класса как атрибутах. С этого момента мы будем пользоваться такой терминологией. В нашем простом классе Y имеются два метода: __init__ и value и два атрибута: v0 и g. Имена методов и атрибутов могут свободно меняться точно так же как имена обычных функций и переменных. Однако, конструктор обязательно должен называться __init__.


Переменная self[править]

Внутри конструктора __init__ аргумент self это переменная, содержащая создаваемый экземпляр. Когда мы пишем


self.v0 = v0
self.g = 9.81


мы определяем два новых атрибута в этом экземпляре. Записывая y = Y(3) мы не только передаем число, но и имя экземпляра, то есть этот вызов можно представить как


Y.__init__(y, 3)


Когда мы пишем в теле конструктора self.v0 = v0, мы в действительности инициализируем y.v0. Когда же пишем

value = y.value(0.1)

Python переводит это как вызов

value = y.value(y, 0.1)


Выражение внутри метода value

self.v0*t - 0.5*self.g*t**2

ввиду того, что self это y имеет смысл тот же, что

y.v0*t - 0.5*y.g*t**2


Правила касательно self следующие:

  • Любой метод класса содержит self в качестве первого аргумента.
  • self представляет в своем лице (произвольный) экземпляр класса.
  • Другой метод или атрибут класса используют self в виде self.name, где name имя этого атрибута или метода.
  • self в качестве аргумента пропускается при вызове методов класса

Расширение класса[править]

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


'v0*t - 0.5*g*t**2; v0=%g' % self.v0


где self это экземпляр класса Y. Вызов formula не требует никаких аргументов:


print y.formula()


Однако, из правил о self мы помним, что хотя метод formula при вызове и не требует никаких аргументов, но при определении мы должны передать ему аргумент self:


def formula(self):
    'v0*t - 0.5*g*t**2; v0=%g' % self.v0


Теперь наш класс целиком выглядит так:


class Y:
    """The  vertical  motion  of  a  ball."""

    def __init__(self, v0):
        self.v0 = v0
        self.g = 9.81

    def value(self, t):
        return self.v0*t - 0.5*self.g*t**2

    def formula(self):
        'v0*t - 0.5*g*t**2; v0=%g' % self.v0


И пример того как может использоваться:


y = Y(5)
t = 0.2
v = y.value(t)
print 'y(t=%g; v0=%g) = %g' % (t, y.v0, v)
print y.formula()


Результат:


y(t=0.2; v0=5) = 0.8038
v0*t - 0.5*g*t**2; v0=5

Методы как обычные функции[править]

Использование класса позволяет создать несколько функций y с разными значениями v0:


y1 = Y(1)
y2 = Y(1.5)
y3 = Y(-3)


При этом мы можем использовать y1.value, y2.value и y3.value как обычные функции от t, а значит и применять все то же, что имеется их для любых других функций одной переменной. Например, наше объяснение введения классов мы начали с примера взятия производной в точке:


dy1dt = diff(y1.value, 0.1)
dy2dt = diff(y2.value, 0.1)
dy3dt = diff(y3.value, 0.2)


Строки документации[править]

Классы, как и функции, могут быть описаны простым человеческим языком сразу в следующей строке после заголовка с помощью doc strings — строк документации. Вводятся они абсолютно таким же образом, с помощью тройки двойных кавычек с каждой стороны:


class Y:
    """The vertical motion of a ball."""

    def __init__(self, v0):
        ...


В случае объемного конечного продукта обычно пишут более исчерпывающее объяснение о том как этот класс может быть использован, какие методы и атрибуты включает, примеры использования класса:


class Y:
    """Mathematical function for the vertical motion of a ball.

    Methods:
        constructor(v0): set initial velocity v0.
        value(t): compute the height as function of t.
        formula(): print out the formula for the height.

    Attributes:
        v0: the initial velocity of the ball (time 0).
        g: acceleration of gravity (fixed).

    Usage:
    >>> y  =  Y(3)
    >>> position1 = y.value(0.1)
    >>> position2 = y.value(0.3)
    >>> print  y.formula()
    v0*t - 0.5*g*t**2; v0=3
    """


Альтернативная реализация классов функций[править]

Чтобы далее продолжить знакомство с программированием с участием классов, теперь мы реализуем класс Y другим образом.

Это хорошая привычка всегда в классе иметь конструктор и инициализировать в нем атрибуты класса. Но это не обязательное требование. Давайте выбросим конструктор и представим v0 как аргумент метода value. Если пользователь при вызове не задает v0, то мы используем значение из более ранних вызовов, находящееся в атрибуте self.v0. О том, задал ли пользователь v0 или нет, мы узнаем, задав в определении v0 значение по умолчанию None, а дальше проверяя его с помощью if. Наша альтернативная реализация представлена теперь классом Y2:


class Y2:
    def value(self, t, v0=None):
        if v0 is not None:
            self.v0 = v0
        g = 9.81
        return self.v0*t - 0.5*g*t**2


В этот раз у класса только один метод и один атрибут, поскольку мы обошлись без конструктора, а g сделали локальной переменной метода value.

Но если здесь нет конструктора, то как же создается экземпляр? На самом деле Python создает пустой конструктор. Это позволяет нам написать как и раньше:


y = Y2()


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


print y.v0


мы получим ошибку:


AttributeError: Y2  instance has no attribute 'v0'


Но при вызове


v = y.value(0.1, 5)


мы создаем атрибут self.v0 в методе value. Теперь


print y.v0


дает 5. Это значение v0 используется пока новый вызов не изменит его.

Возникающее исключение AttributeError следовало бы учесть в теле класса (а еще точнее методе value) с помощью блока try-except:


class Y2:
    def value(self, t, v0=None):
        if v0 is not None:
            self.v0 = v0
        g = 9.81
        try:
            value = self.v0*t - 0.5*g*t**2
        except AttributeError:
            msg = 'You cannot call value(t)  without first '
                  'calling value(t, v0) to set v0'
            raise TypeError(msg)
        return value


Конечно, класс Y это лучшая реализация, чем Y2, поскольку имеет более простую форму. Как уже отмечалось, использование конструктора это хорошая привычка программирования, конструктор осуществляет удобную связь между «внешним миром» и классом. Цель нашего класса Y2 только в том чтобы показать что Python обладает большой гибкостью к определению атрибутов и что вообще нет специальных требований что именно класс должен содержать.


Классы без классов[править]

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

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

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


Y.value(y, t)
Y.formula(y)


Класс представляется как пространство имен, то есть все его функции должны иметь префикс Y. Два разных класса, скажем С1 и С2 могут иметь функции с одним и тем же именем, например value, но при этом поскольку они относятся к разным классам, их имена становятся различны: С1.value и С2.value. Модули также представляют собой пространства имен для своих функций и переменных (math.sin, cmath.sin, numpy.sin)

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


y.value(t)
y.formula()


Мы можем легко реализовать концепцию класса и без него самого. Как мы уже выяснили, все что нам нужно это словарь и обычные функции. Наш класс Y может быть реализован и так:


def value(self, t):
    return self['v0']*t - 0.5*self['g']*t**2

def formula(self):
    print 'v0*t - 0.5*g*t**2; v0=%g' % self['v0']


Представим эти две функции расположены в модуле Y:


import Y
y = {'v0': 4, 'g': 9.81}  # создаем "экземпляр"
y1 = Y.value(y, t)


Теперь у нас нет вообще никакого конструктора, поскольку нет и класса. Инициализация происходит при создании словаря y, но мы можем включить инициализацию и в модуль Y:


def init(v0):
    return {'v0': v0,  'g': 9.81}


Использование такого модуля-класса теперь выглядит более похожим на обычное:


import Y
y = Y.init(4)
y1 = Y.value(y, t)


И такая реализация вполне возможна и существует. На самом деле любой класс в Python имеет словарь-атрибут __dict__, который хранит все имеющиеся в экземпляре переменные:


>>> y = Y(1.2)
>>> print y.__dict__
{'v0': 1.2, 'g': 9.8100000000000005}


Итак, в этом уроке мы рассмотрели классы с технической точки зрения. Следующий урок скорее посвящен классам как пути моделирования в терминах данных и операциях над данными.

Ссылки[править]