Программирование и научные вычисления на языке 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}
Итак, в этом уроке мы рассмотрели классы с технической точки зрения. Следующий урок скорее посвящен классам как пути моделирования в терминах данных и операциях над данными.