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

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



## Банковские счета

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



```
class Account:
def __init__(self, name, account_number, initial_amount):
self.name = name
self.no = account_number
self.balance = initial_amount
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
self.balance -= amount
def dump(self):
s = '%s, %s, balance: %s' % \
(self.name, self.no, self.balance)
print s
```


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



```
>>> from classes import Account
>>> a1 = Account('John Olsson', '19371554951', 20000)
>>> a2 = Account('Liz Olsson', '19371564761', 20000)
>>> a1.deposit(1000)
>>> a1.withdraw(4000)
>>> a2.withdraw(10500)
>>> a1.withdraw(3500)
>>> print "a1's balance:", a1.balance
a1’s balance: 13500
>>> a1.dump()
John Olsson, 19371554951, balance: 13500
>>> a2.dump()
Liz Olsson, 19371564761, balance: 9500
```


«Заказчик» класса естественно не хочет чтобы пользователи могли управлять атрибутами экземпляров, то есть изменять имя, номер или баланс. Идея заключается в том, чтобы пользователи могли вызывать только конструктор и методы `deposit, withdraw` и `dump`, проверять (при желании) атрибут `balnce`, но не изменять его. Для таких атрибутов, которые нельзя трогать и методов, что нельзя вызывать, перед названием ставится символ нижнего подчеркивания. Такие имена можно назвать *охраняемыми* — они как бы принадлежат программе, а не пользователям. Тогда наш «защищенный» (protected) класс `AccountP` таков:



```
class AccountP:
def __init__(self, name, account_number, initial_amount):
self._name = name
self._no = account_number
self._balance = initial_amount
def deposit(self, amount):
self._balance += amount
def withdraw(self, amount):
self._balance -= amount
def get_balance(self):
return self._balance
def dump(self):
s = '%s, %s, balance: %s' % \
(self._name, self._no, self._balance)
print s
```



## Телефонная книга

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



```
class Person:
def __init__(self, name,
mobile_phone=None, office_phone=None,
private_phone=None, email=None):
self.name = name
self.mobile = mobile_phone
self.office = office_phone
self.private = private_phone
self.email = email
def add_mobile_phone(self, number):
self.mobile = number
def add_office_phone(self, number):
self.office = number
def add_private_phone(self, number):
self.private = number
def add_email(self, address):
self.email = address
```


Объекты `None` позволяет нам добавлять любое количество желаемых сведений кроме имени или не добавлять их вовсе. Пример использования класса:



```
>>> p1 = Person('Hans Hanson',
... office_phone='767828283', email='h@hanshanson.com')
>>> p2 = Person('Ole Olsen', office_phone='767828292')
>>> p2.add_email('olsen@somemail.net')
>>> phone_book = [p1, p2]
```


Было бы также неплохо добавить в класс специальный метод для вывода всех данных о человеке:



```
def dump(self):
s = self.name + '\n'
if self.mobile is not None:
s += 'mobile phone: %s\n' % self.mobile
if self.office is not None:
s += 'office phone: %s\n' % self.office
if self.private is not None:
s += 'private phone: %s\n' % self.private
if self.email is not None:
s += 'email address: %s\n' % self.email
return s
```


С таким методом несложно распечатать и всю телефонную книгу:



```
>>> for person in phone_book:
... person.dump()
...
Hans Hanson
office phone: 767828283
email address: h@hanshanson.com
Ole Olsen
office phone: 767828292
email address: olsen@somemail.net
```


Телефонная книга может быть представлена списком экземпляров класса `Person`, как показано в примере выше. Однако, если мы хотим быстро по имени просмотреть телефонные номера или e-mail, более удобным оказывается хранение телефонной книги в виде словаря, где имя служит ключом:



```
>>> phone_book = {'Hanson': p1, 'Olsen': p2}
>>> for person in sorted(phone_book): # алфавитный порядок
... phone_book[person].dump()
```



## Круг

Для задания и быстрого анализа геометрических фигур, например, круга, также отлично подходят классы. Круг на плоскости задается координатами центра (*x 0, y0*) и радиусом

*R*. Эти три числа мы возьмем как атрибуты класса. В качестве методов будем рассчитывать площадь (

`area`) круга и длину окружности (

`circumference`):



```
class Circle:
def __init__(self, x0, y0, R):
self.x0, self.y0, self.R = x0, y0, R
def area(self):
return pi*self.R**2
def circumference(self):
return 2*pi*self.R
```



```
>>> c = Circle(2, -1, 5)
>>> print 'A circle with radius %g at (%g, %g) has area %g' % \
... (c.R, c.x0, c.y0, c.area())
A circle with radius 5 at (2, -1) has area 78.5398
```


Естественно, идеи класса `Circle` могут быть распространены и на любые другие геометрические объекты.



## Специальные методы

Методы класса могут иметь имена начинающиеся и заканчивающиеся двойным подчеркиванием. Такой синтаксис показывает что эти методы являются *специальными*. Например, метод конструктора `__init__`. Этот метод автоматически вызывается при создании экземпляра. Другие специальные методы позволяют производить над экземплярами арифметические операции, сравнивать их, вызывать экземпляры как функции и так далее.



### __call__

Вычисление значения математической функции, показанное на предыдущем уроке с помощью класса `Y` и его экземпляров `y` происходит с помощью вызова `y.value(t)`. Если бы писали `y(t)`, то это выглядело бы как вызов обычной функции. И в действительности такой простой синтаксис возможен с помощью метода `__call__`. То есть вызов `y(t)` (в случае если этот метод определен) равносилен `y.__call__(t)`. И этот специальный метод добавить очень просто:



```
class Y:
...
def __call__(self, t):
return self.v0*t - 0.5*self.g*t**2
```


Ранее использующийся метод `value` теперь оказывается лишним. И это хорошая и удобная привычка включать метод `__call__` в классы, предоставляющие математические функции.



#### Пример: Производная

Имея реализацию математической функции *f(x)* в виде функции Python, мы хотим получить объект, который принимает эту функцию и находит производную *f'(x)*. Например, если этот объект имеет тип `Derivative` (производная), мы сможем написать что-то вроде:



```
>>> def f(x):
return x**3
...
>>> dfdx = Derivative(f)
>>> x = 2
>>> dfdx(x)
12.000000992884452
```


Как мы знаем производная от *x*3 это 3*x*2 и ответ с определенной точностью представления (ошибка в 7 знаке после запятой) и равен 12.

Maple, Mathematica и другие математические пакеты используют для целей дифференцирования и интегрирования символьные вычисления. Также имеется библиотека Python — SumPy, бесплатная и легко используемая для многих видов функций *f(x)*. Однако для целого ряда задач, например, таких как случайные числа, использование символьных вычислений представляется проблемой и требуется численный расчет.

Для нашей задачи численного дифференцирования мы будем использовать не самую точную, но зато самую простую формулу. Конечно, вы сами можете использовать более точные методы, что захотите. Идея сейчас в том, чтобы создать класс, с помощью которого мы будем дифференцировать функцию `f` с заданным шагом `h`. Чем меньше `h`, тем точнее и дольше вычисления. Эти переменные мы устанавливаем в конструкторе. Оператор `__call__` рассчитывает производную в точке `x` по известной формуле:



```
class Derivative:
def __init__(self, f, h=1E-9):
self.f = f
self.h = float(h)
def __call__(self, x):
f, h = self.f, self.h
return (f(x+h) - f(x))/h
```



```
>>> from math import sin, cos, pi
>>> df = Derivative(sin)
>>> x = pi
>>> df(x)
-1.000000082740371
>>> cos(x) # точно
-1.0
>>> def g(t):
... return t**3
...
>>> dg = Derivative(g)
>>> t = 1
>>> dg(t) # если точно, то должно быть 3
3.000000248221113
```



### __str__

Другой специальный метод это `__str__`. Он вызывается, когда экземпляр класса должен быть представлен в виде строки: `print a`, где `a` — экземпляр. Если Python для `a` находит специальный метод `__str__`, то он печатает его в соответствии с методом. Аналогично добавлению в класс `Y` метода `__call__`, мы можем усовершенствовать его и с помощью метода `__str__`



```
class Y:
...
def __str__(self):
return 'v0*t - 0.5*self.g*t**2; v0=%g' % self.v0
```


Таким образом метод `__str__` выгодно замещает наш старый метод `formula` точно так же как `__call__` заменил `value`. Таким образом, опытные программисты на Python могли решить нашу задачу из предыдущего урока с использованием только специальных методов, оказывающихся и более естественными и удобными:



```
class Y:
def __init__(self, v0):
self.v0 = v0
self.g = 9.81
def __call__(self, t):
return self.v0*t - 0.5*self.g*t**2
def __str__(self):
return 'v0*t - 0.5*g*t**2; v0=%g' % self.v0
```


И тут же видим более ясное применение в действии:



```
>>> y = Y(1.5)
>>> y(0.2)
0.1038
>>> print y
v0*t - 0.5*g*t**2; v0=1.5
```



### __add__ и другие

Пусть у нас есть два экземпляра класса `C`: `a` и `b`. И мы хотели бы задать определенный смысл выражению `a + b`, чтобы Python встречая знак сложения для экземпляров этого класса производил определенную операцию. Для этого существует метод `__add__`:



```
class C:
...
__add__(self, other):
...
```


Метод `__add__` складывает экземпляры `self` и `other` и возвращает результат как экземпляр. То есть когда Python встречает для наших экземпляров выражение `a + b` он вызывает метод `a.__add__(b)`.

Кроме операции сложения возможно еще множество подобных операций и других специальных методов:



a + b | a.__add__(b) |

a - b | a.__sub__(b) |

a*b | a.__mul__(b) |

a/b | a.__div__(b) |

a**b | a.__pow__(b) |

len(a) | a.__len__() |

abs(a) | a.__abs__() |

a == b | a.__eq__(b) |

a > b | a.__gt__(b) |

a >= b | a.__ge__(b) |

a < b | a.__lt__(b) |

a <= b | a.__le__(b) |

a != b | a.__ne__(b) |

-a | a.__neg__() |