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

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



## Простые классы функций

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

.

Здесь *y* является функций времени *t* и кроме того, зависит от других параметров *v 0* и

*g*. Мы могли бы придумать какое-то новое обозначение, вроде

*y*(

*t;*v

0

*,*g

*), чтобы показать, что*t

*является независимой переменной, а*v

0

*и*g

*задаваемыми параметрами. При этом, строго говоря для Земли*g

*, гравитационная постоянная, неизменна, то есть правильнее было бы писать 'y*(

*t; v*). В общем случае, у нас может иметься функция, которая будет записываться

0*f*(

*x*;

*p*).

1, ..., 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; *v 0*=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*. После этой формулы мы выводим значение *v 0*:



```
'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}
```


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