Программирование и научные вычисления на языке 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
a1s  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()

Круг[править]

Для задания и быстрого анализа геометрических фигур, например, круга, также отлично подходят классы. Круг на плоскости задается координатами центра (x0, 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


Как мы знаем производная от x3 это 3x2 и ответ с определенной точностью представления (ошибка в 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__()