Абстракция и декомпозиция
Абстракция в ООП позволяет составить из данных и алгоритмов обработки этих данных объекты, отвлекаясь от несущественных (на некотором уровне) с точки зрения составленной информационной модели деталей. Таким образом, программа подвергается декомпозиции на части "дозированной" сложности. Отдельный объект, даже вместе с совокупностью его связей с другими объектами, человеком воспринимается легче (именно так он привык оперировать в реальном мире), чем что-то неструктурированное и монотонное.
Перед тем как начать написание даже самой простенькой объектно-ориентированной программы, необходимо провести анализ предметной области, для того чтобы выявить в ней классы объектов.
При выделении объектов необходимо абстрагироваться (отвлечься) от большинства присущих им свойств и сконцентрироваться на свойствах, значимых для задачи..
Выделяемые объекты необязательно должны походить на физические объекты - ведь это абстракции, за которыми скрываются процессы, взаимодействия, отношения.
Удачная декомпозиция стоит многого. От нее зависят не только количественные характеристики кода (быстродействие, занимаемая память), но и трудоемкость дальнейшего развития и сопровождения. При отсутствии соответствующего опыта лучше не загадывать будущих путей развития программы, а делать ее как можно проще, под конкретную задачу.
Даже если просто перечислить все существительные, встретившиеся в описании задачи (явно или неявно), получится неплохой список кандидатов в классы.
При процедурном подходе тоже используется декомпозиция, но при объектно-ориентированном подходе производится декомпозиция не самого алгоритма на более мелкие части, а предметной области на классы объектов.
Ассоциация
Если в случае агрегации имеется довольно четкое отношение "ИМЕЕТ" (HAS-A) или "СОДЕРЖИТСЯ-В", которое даже отражено в синтаксисе Python:
lst = [1, 2, 3] if 1 in lst: ...
то в случае ассоциации ссылка на экземпляр другого класса используется без отношения включения одного в другой или принадлежности. О таком отношении между классами говорят как об отношении USE-A ("ИСПОЛЬЗУЕТ"). Это достаточно общее отношение зависимости между классами.
В языке Python границы между агрегацией и ассоциацией несколько размыты, так как объекты при агрегации обычно не хранятся в области памяти, выделенной под контейнер (хранятся только ссылки).
Объекты могут также ссылаться друг на друга. В этом случае возникают циклические ссылки, которые при неаккуратном использовании могут привести (в старых версиях Python) к утечкам памяти. В новых версиях Python для циклических ссылок работает сборщик мусора.
Разумеется, при "чистой" агрегации циклических ссылок не возникает.
Например, при представлении дерева ссылки могут идти от родителей к детям и обратно от каждого дочернего узла к родительскому.
Доступ к свойствам
В языке Python не считается зазорным получить доступ к некоторому атрибуту (не методу) напрямую, если, конечно, этот атрибут описан в документации как часть интерфейса класса. Такие атрибуты называются свойствами (properties). В других языках программирования принято для доступа к свойствам создавать специальные методы (вместо того чтобы напрямую обращаться к общедоступным членам-данным). В Python достаточно использовать ссылку на атрибут, если свойство ни на что в объекте не влияет (то есть другие объекты могут его произвольно менять). Если же свойство менее тривиально и требует каких-то действий в самом объекте, его можно описать как свойство (пример взят из документации к Python):
class C(object): def getx(self): return self.__x def setx(self, value): self.__x = value def delx(self): del self.__x x = property(getx, setx, delx, "I'm the 'x' property.")
Синтаксически доступ к свойству x будет обычной ссылкой на атрибут:
>>> c = C() >>> c.x = 1 >>> print c.x 1 >>> del c.x
А на самом деле будут вызываться соответствующие методы: setx(), getx(), delx().
Следует отметить, что в экземпляре класса в Python можно организовать доступ к любым (даже несуществующим) атрибутам, обрабатывая запрос на доступ к атрибуту группой специальных методов:
__getattr__(self, name) | Этот метод объекта вызывается в том случае, если атрибут не найден другим способом (его нет в данном экземпляре или в дереве классов). Здесь name - имя атрибута. Метод должен вычислить значение атрибута либо возбудить исключение AttributeError. Для получения полного контроля над атрибутами в "новых" классах (то есть потомках object) используйте метод __getattribute__(). |
__setattr__(self, name, value) | Этот метод вызывается при присваивании значения некоторому атрибуту. В отличие от __getattr__(), метод всегда вызывается, а не только тогда, когда атрибут может быть найден в экземпляре класса, поэтому нужно с осторожностью присваивать значения атрибутам внутри этого метода: это может вызвать рекурсию. Для присваивания значений атрибутов предпочтительнее присваивать словарю __dict__: self.__dict__[name] = value или (для "новых" классов) - обращение к __setattr__() базового класса: object.__setattr__(self, name, value). |
__delattr__(self, name) | Как можно догадаться из названия, этот метод служит для удаления атрибута. |
Следующий небольшой пример демонстрирует все перечисленные моменты. В этом примере из словаря создается объект, именами атрибутов которого будут ключи словаря, а значениями - значения из словаря по заданным ключам:
class AttDict(object): def __init__(self, dict=None): object.__setattr__(self, '_selfdict', dict or {})
def __getattr__(self, name): if self._selfdict.has_key(name): return self._selfdict[name] else: raise AttributeError
def __setattr__(self, name, value): if name[0] != '_': self._selfdict[name] = value else: raise AttributeError
def __delattr__(self, name): if name[0] != '_' and self._selfdict.has_key(name): del self._selfdict[name]
ad = AttDict({'a': 1, 'b': 10, 'c': '123'}) print ad.a, ad.b, ad.c ad.d = 512 print ad.d
Имитация типов
Для иллюстрации понятия полиморфизма можно построить собственный тип, похожий на встроенный тип "функция". Построить класс, объекты которого вызываются подобно методам или функциям, можно так:
class CountArgs(object): def __call__(self, *args, **kwargs): return len(args) + len(kwargs)
cc = CountArgs() print cc(1, 3, 4)
Как видно из этого примера, экземпляры класса CountArgs можно вызывать подобно функциям (в результате будет возвращено количество переданных параметров). При попытке вызова экземпляра на самом деле будет вызван метод __call__() со всеми аргументами.
Следующий пример показывает, что сравнением экземпляров класса тоже можно управлять:
class Point: def __init__(self, x, y): self.coord = (x, y) def __nonzero__(self): return self.coord[0] != 0 or self.coord[1] != 0 def __cmp__(self, p): return cmp(self.coord, p.coord)
for x in range(-3, 4): for y in range(-3, 4): if Point(x, y) < Point(y, x): print "*", elif Point(x, y): print ".", else: print "o", print
Программа выведет:
. * * * * * * . . * * * * * . . . * * * * . . . o * * * . . . . . * * . . . . . . * . . . . . . .
В данной программе класс Point (Точка) имеет метод __nonzero__(), который определяет истинностное значение объекта класса. Истину будут давать только точки, отличные от (0, 0). Другой метод - __cmp__() - вызывается при необходимости сравнить точку и другой объект (имеющий как и точка атрибут coord, который содержит кортеж как минимум из двух элементов). Нужно заметить, что вместо __cmp__ можно определить отдельные методы для операций сравнения: __lt__, __le__, __ne__, __eq__, __ge__, __gt__ (для <, <=, !=, ==, >=, > соответственно).
Достаточно легко имитировать и числовые типы. Класс, который пользуется удобством синтаксиса инфиксного +, можно определить так:
class Plussable: def __add__(self, x): ... def __radd__(self, x): ... def __iadd__(self, x): ...
Здесь метод __add__() вызывается, когда экземпляр класса Plussable стоит слева от сложения, __radd__() - если справа от сложения и метод слева от него не имеет метода __add__(). Метод __iadd__() нужен для реализации +=.
Инкапсуляция
Обычно считается, что без инкапсуляции невозможно представить себе ООП, что это ключевое понятие. История развития методологий программирования движима борьбой со сложностью разработки программного обеспечения. Сложность больших программных систем, в создании которых участвует сразу большое количество разработчиков, уменьшается, если на верхнем уровне не видно деталей реализации нижних уровней. Собственно, процедурный подход был первым шагом на этом пути. Под инкапсуляцией (incapsulation, что можно перевести по-разному, но на нужные ассоциации хорошо наводит слово "обволакивание") понимается сокрытие информации о внутреннем устройстве объекта, при котором работа с объектом может вестись только через его общедоступный (public) интерфейс. Таким образом, другие объекты не должны вмешиваться в "дела" объекта, кроме как используя вызовы методов.
В языке Python инкапсуляции не придается принципиального значения: ее соблюдение зависит от дисциплинированности программиста. В других языках программирования имеются определенные градации доступности методов объекта.
Итераторы - это объекты, которые предоставляют последовательный доступ к элементам контейнера (или генерируемым "на лету" объектам). Итератор позволяет перебирать элементы, абстрагируясь от реализации того контейнера, откуда он их берет (если этот контейнер вообще есть).
В следующем примере приведен итератор, выдающий значения из списка по принципу "считалочки" по N:
class Zahlreim: def __init__(self, lst, n): self.n = n self.lst = lst self.current = 0 def __iter__(self): return self def next(self): if self.lst: self.current = (self.current + self.n - 1) % len(self.lst) return self.lst.pop(self.current) else: raise StopIteration
print range(1, 11) for i in Zahlreim(range(1, 11), 5): print i,
Программа выдаст
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 5 10 6 2 9 8 1 4 7 3
В этой программе делегировано управление доступом к элементам списка (или любого другого контейнера, имеющего метод pop(n) для взятия и удаления n-го элемента) классу-итератору. Итератор должен иметь метод next() и возбуждать исключение StopIteration по завершении итераций. Кроме того, метод __iter__() должен выдавать итератор по экземпляру класса (в данном случае итератор - он сам (self)).
В настоящее время итераторы приобретают все большее значение, и о них много говорилось в лекции по функциональному программированию.
Контейнеры
Под контейнером обычно понимают объект, основным назначением которого является хранение и обеспечение доступа к другим объектам. Контейнеры реализуют отношение "HAS-A" ("ИМЕЕТ") между объектами. Встроенные типы, список и словарь -- яркие примеры контейнеров. Можно построить собственные типы контейнеров, которые будут иметь свою логику доступа к хранимым объектам. В контейнере хранятся не сами объекты, а ссылки на них.
Для практических нужд в Python обычно хватает встроенных контейнеров (словаря и списка), но если это необходимо, можно создать и другие. Ниже приведен класс Стек, реализованный на базе списка:
class Stack: def __init__(self): """Инициализация стека""" self._stack = [] def top(self): """Возвратить вершину стека (не снимая)""" return self._stack[-1] def pop(self): """Снять со стека элемент""" return self._stack.pop() def push(self, x): """Поместить элемент на стек""" self._stack.append(x) def __len__(self): """Количество элементов в стеке""" return len(self._stack) def __str__(self): """Представление в виде строки""" return " : ".join(["%s" % e for e in self._stack])
Использование:
>>> s = Stack() >>> s.push(1) >>> s.push(2) >>> s.push("abc") >>> print s.pop() abc >>> print len(s) 2 >>> print s 1 : 2
Таким образом, контейнеры позволяют управлять набором (любых) других объектов в соответствии со структурой их хранения, не вмешиваясь во внутренние дела объектов. Узнав интерфейс класса Stack, можно и не догадаться, что он реализован на основе списка, и каким именно образом он реализован с помощью него. Но для использования стека это не важно.
Примечание: В данном примере для краткости изложения не учтено, что в результате некоторых действий могут возбуждаться исключения. Например, при попытке снять элемент с вершины пустого стека. |
Критика ООП
Объектно-ориентированный подход сегодня считается "самым передовым". Однако не следует слепо верить в его всемогущество. Отдача (в виде скорости разработки) от объектного проектирования чувствуется только в больших проектах и в проектах, которые родственны объектному подходу: построение графического интерфейса, моделирование систем и т.п.
Также спорна большая гибкость объектных программ к изменениям. Она зависит от того, вносится ли новый метод (для серии объектов) или новый тип объекта. При процедурном подходе при появлении нового метода пишется отдельная процедура, в которой в каждой ветке алгоритма обрабатывается свой тип данных (то есть такое изменение требует редактирования одного места в коде). При ООП изменять придется каждый класс, внося в него новый метод (то есть изменения в нескольких местах). Зато ООП выигрывает при внесении нового типа данных: ведь изменения происходят только в одном месте, где описываются все методы для данного типа. При процедурном подходе приходится изменять несколько процедур. Сказанное иллюстрируется ниже. Пусть имеются классы A, B, C и методы a, b, c:
# ООП class A: def a(): ... def b(): ... def c(): ...
class B: def a(): ... def b(): ... def c(): ...
class C: def a(): ... def b(): ... def c(): ...
# процедурный подход
def a(x): if type(x) is A: ... if type(x) is B: ... if type(x) is C: ...
def b(x): if type(x) is A: ... if type(x) is B: ... if type(x) is C: ...
def c(x): if type(x) is A: ... if type(x) is B: ... if type(x) is C: ...
При внесении нового типа объекта изменения в ОО-программе затрагивают только один модуль, а в процедурной - все процедуры:
# ООП
class D: def a(): ... def b(): ... def c(): ...
# процедурный подход
def a(x): if type(x) is A: ... if type(x) is B: ... if type(x) is C: ... if type(x) is D: ...
def b(x): if type(x) is A: ... if type(x) is B: ... if type(x) is C: ... if type(x) is D: ...
def c(x): if type(x) is A: ... if type(x) is B: ... if type(x) is C: ... if type(x) is D: ...
И наоборот, теперь нужно добавить новый метод обработки. При процедурном подходе просто пишется новая процедура, а вот для объектного приходится изменять все классы:
# процедурный подход
def d(x): if type(x) is A: ... if type(x) is B: ... if type(x) is C: ...
# ООП
class A: def a(): ... def b(): ... def c(): ... def d(): ...
class B: def a(): ... def b(): ... def c(): ... def d(): ...
class C: def a(): ... def b(): ... def c(): ... def d(): ...
Язык программирования Python изначально был ориентирован на практические нужды. Приведенное выше выражается в стандартной библиотеке Python, то есть в том, что там применяются и функции (обычно сильно обобщенные на довольно широкий круг входных данных), и классы (когда операции достаточно специфичны). Обобщенная природа функций Python и полиморфизм, не завязанный целиком на наследовании - вот свойства языка Python, позволяющие иметь большую гибкость в комбинации процедурного и объектно-ориентированного подходов.
Метаклассы
Еще одним отношением между классами является отношение класс-метакласс. Метакласс можно считать "высшим пилотажем" объектно-ориентированного программирования, но, к счастью, в Python можно создавать собственные метаклассы.
В Python класс тоже является объектом, поэтому ничего не мешает написать класс, назначением которого будет создание других классов динамически, во время выполнения программы.
Пример, в котором класс порождается динамически в функции-фабрике классов:
def cls_factory_f(func): class X(object): pass setattr(X, func.__name__, func) return X
Использование будет выглядеть так:
def my_method(self): print "self:", self
My_Class = cls_factory_f(my_method) my_object = My_Class() my_object.my_method()
В этом примере функция cls_factory_f() возвращает класс с единственным методом, в качестве которого используется функция, переданная ей как аргумент. От этого класса можно получить экземпляры, а затем у экземпляров - вызвать метод my_method.
Теперь можно задаться целью построить класс, экземплярами которого будут классы. Такой класс, от которого порождаются классы, и называется метаклассом.
В Python имеется класс type, который на деле является метаклассом. Вот как с помощью его конструктора можно создать класс:
def my_method(self): print "self:", self
My_Class = type('My_Class', (object,), {'my_method': my_method})
В качестве первого параметра type передается имя класса, второй параметр - базовые классы для данного класса, третий - атрибуты.
В результате получится класс, эквивалентный следующему:
class My_Class(object): def my_method(self): print "self:", self
Но самое интересное начинается при попытке составить собственный метакласс. Проще всего наследовать метакласс от метакласса type (пример взят из статьи Дэвида Мертца):
>>> class My_Type(type): ... def __new__(cls, name, bases, dict): ... print "Выделение памяти под класс", name ... return type.__new__(cls, name, bases, dict) ... def __init__(cls, name, bases, dict): ... print "Инициализация класса", name ... return super(My_Type, cls).__init__(cls, name, bases, dict) ... >>> my = My_Type("X", (), {}) Выделение памяти под класс X Инициализация класса X
В этом примере не происходит вмешательство в создание класса. Но в __new__() и __init__() имеется полный программный контроль над создаваемым классом в период выполнения.
Примечание: Следует заметить, что в метаклассах принято называть первый аргумент методов не self, а cls, чтобы напомнить, что экземпляр, над которым работает программист, является не просто объектом, а классом. |
Метод класса
Если статический метод имеет свои аналоги в C++ и Java, то метод класса основан на том, что в Python классы являются объектами. В отличие от статического метода, в метод класса первым параметром передается объект-класс. Вместо self для подчеркивания принадлежности метода к методам класса принято использовать cls.
Пример использования метода класса можно найти в модуле tree пакета nltk (Natural Language ToolKit, набор инструментов для естественного языка). Ниже приведен лишь фрагмент определения класса Tree (базового класса для других подклассов). Метод convert класса Tree определяет процедуру преобразования дерева одного типа в дерево другого типа. Эта процедура абстрагируется от деталей реализации конкретных типов, описывая обобщенный алгоритм преобразования:
class Tree: # ... def convert(cls, val):
if isinstance(val, Tree): children = [cls.convert(child) for child in val] return cls(val.node, children) else: return val convert = classmethod(convert)
Пример использования (взят из строки документации метода convert()):
>>> # Преобразовать tree в экземпляр класса Tree >>> tree = Tree.convert(tree) >>> # " " " " " ParentedTree >>> tree = ParentedTree.convert(tree) >>> # " " " " " MultiParentedTree >>> tree = MultiParentedTree.convert(tree)
Метод класса позволяет более естественно описывать действия, которые связаны в основном с классами, а не с методами экземпляра класса.
Множественное наследование
В отличие, например, от Java, в языке Python можно наследовать класс от нескольких классов. Такая ситуация называется множественным наследованием (multiple inheritance).
Класс, получаемый при множественном наследовании, объединяет поведение своих надклассов, комбинируя стоящие за ними абстракции.
Использовать множественное наследование следует очень осторожно, а необходимость в нем возникает реже одиночного.
Множественное наследование можно применить для получения класса с заданными общедоступными методами, причем методы задает один родительский класс, а реализуются они на основе методов второго класса. Первый класс может быть полностью абстрактным.Множественное наследование применяется для добавления примесей (mixins). Примесь - специально сконструированный класс, добавляющий в некоторый класс какую-либо черту поведения (привнесением атрибутов). Примеси обычно являются абстрактными классами.Изредка множественное наследование применяется в своем основном смысле, когда объекты класса, получающегося в результате множественного наследования, предназначаются для использования в качестве объектов всех родительских классов.
В случае с Python наследование можно считать одним из способов собрать нужные комбинации методов в серии классов:
class A: def a(self): return 'a' class B: def b(self): return 'b' class C: def c(self): return 'c'
class AB(A, B): pass class BC(B, C): pass class ABC(A, B, C): pass
Впрочем, собрать нужные методы можно и по-другому, без использования наследования:
def ma(self): return 'a' def mb(self): return 'b' def mc(self): return 'c'
class AB: a = ma b = mb
class BC: b = mb c = mc
class ABC: a = ma b = mb c = mc
Мультиметоды
Некоторые объектно-ориентированные "штучки" не входят в стандартный Python или стандартную библиотеку. Ниже будут рассмотрены мультиметоды - методы, сочетающие объекты сразу нескольких различных классов. Например, сложение двух чисел различных типов фактически требует использования мультиметода. Если "одиночный" метод достаточно задать для каждого класса, то мультиметод требует задания для каждого сочетания классов, которые он обслуживает:
>>> import operator >>> operator.add(1, 2) 3 >>> operator.add(1.0, 2) 3.0 >>> operator.add(1, 2.0) 3.0 >>> operator.add(1, 1+2j) (2+2j) >>> operator.add(1+2j, 1) (2+2j)
В этом примере operator.add ведет себя как мультиметод, выполняя разные действия для различных комбинаций параметров.
Для организации собственных мультиметодов можно воспользоваться модулем Multimethod (автор Neel Krishnaswami), который легко найти в Интернете. Следующий пример, адаптированный из документации модуля, показывает построение собственного мультиметода:
from Multimethod import Method, Generic, AmbiguousMethodError
# классы, для которых будет определен мультиметод class A: pass class B(A): pass
# функции мультиметода def m1(a, b): return 'AA' def m2(a, b): return 'AB' def m3(a, b): return 'BA'
# определение мультиметода (без одной функции) g = Generic() g.add_method(Method((A, A), m1)) g.add_method(Method((A, B), m2)) g.add_method(Method((B, A), m3))
# применение мультиметода try: print 'Типы аргументов:', 'Результат' print 'A, A:', g(A(), A()) print 'A, B:', g(A(), B()) print 'B, A:', g(B(), A()) print 'B, B:', g(B(), B()) except AmbiguousMethodError: print 'Неоднозначный выбор метода'
Наследование
На практике часто возникает ситуация, когда в предметной области выделены очень близкие, но вместе с тем неодинаковые классы. Одним из способов сокращения описания классов за счет использования их сходства является выстраивание классов в иерархию. В корне этой иерархии стоит базовый класс, от которого нижележащие классы иерархии наследуют свои атрибуты, уточняя и расширяя поведение вышележащего класса. Обычно принципом построения классификации является отношение "IS-A" ("ЕСТЬ"). Например, класс Окружность в программе - графическом редакторе может быть унаследован от класса Геометрическая Фигура. При этом Окружность будет являться подклассом (или субклассом) для класса Геометрическая Фигура, а Геометрическая Фигура - надклассом (или суперклассом) для класса Окружность.
В языке Python во главе иерархии ("новых") классов стоит класс object. Для ориентации в иерархии существуют некоторые встроенные функции, которые будут рассмотрены ниже. Функция issubclass(x, y) может сказать, является ли класс x подклассом класса y:
>>> class A(object): pass ... >>> class B(A): pass ... >>> issubclass(A, object) True >>> issubclass(B, A) True >>> issubclass(B, object) True >>> issubclass(A, str) False >>> issubclass(A, A) # класс является подклассом самого себя True
В основе построения классификации всегда стоит принцип, играющий наиболее важную роль в анализируемой и моделируемой системе. Следует заметить, что одним из "перегибов" при использовании ОО методологии является искусственное выстраивание иерархии классов. Например, не стоит наследовать класс Машина от класса Колесо (внимательные заметят, что здесь отношение другое: колесо является частью машины).
Класс называется абстрактным, если он предназначен только для наследования. Экземпляры абстрактного класса обычно не имеют большого смысла. Классы с рабочими экземплярами называются конкретными.
В Python примером абстрактного класса является встроенный тип basestring, у которого есть конкретные подклассы str и unicode.
Объекты
До этой лекции объекты Python встречались много раз: ведь каждое число, строка, функция, модуль и т.п. - это объекты. Некоторые встроенные объекты имеют в Python синтаксическую поддержку (для задания литералов). Таковы числа, строки, списки, кортежи и некоторые другие типы.
Теперь следует посмотреть на них в свете только что приведенных определений. Пример:
a = 3 b = 4.0 c = a + b
Здесь происходит следующее. Сначала имя "a" связывается в локальном пространстве имен с объектом-числом 3 (целое число). Затем "b" связывается с объектом-числом 4.0 (число с плавающей точкой). После этого над объектами 3 и 4.0 выполняется операция сложения, и имя "c" связывается с получившимся объектом. Кстати, операциями, в основном, будут называться методы, которые имеют в Python синтаксическую поддержку, в данном случае - инфиксную запись. То же самое можно записать как:
c = a.__add__(b)
Здесь __add__() - метод объекта a, который реализует операцию + между этим объектом и другим объектом.
Узнать набор методов некоторого объекта можно с помощью встроенной функции dir():
>>> dir(a) ['__abs__', '__add__', '__and__', '__class__', '__cmp__', '__coerce__', '__delattr__', '__div__', '__divmod__', '__doc__', '__float__', '__floordiv__', '__getattribute__', '__getnewargs__', '__hash__', '__hex__', '__init__', '__int__', '__invert__', '__long__', '__lshift__', '__mod__', '__mul__', '__neg__', '__new__', '__nonzero__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__str__', '__sub__', '__truediv__', '__xor__']
Здесь стоит указать на еще одну особенность Python. Не только инфиксные операции, но и встроенные функции ожидают наличия некоторых методов у объекта. Например, можно записать:
abs(c)
А функция abs() на самом деле использует метод переданного ей объекта:
c.__abs__()
Объекты появляются в результате вызова функций-фабрик или конструкторов классов (об этом ниже), а заканчивают свое существование при удалении последней ссылки на объект. Оператор del удаляет имя (а значит, и одну ссылку на объект) из пространства имен:
a = 1 # ... del a # имени a больше нет
Определение класса
Пусть в ходе анализа данной предметной области необходимо определить класс Граф. Граф - это множество вершин и набор ребер, попарно соединяющий эти вершины. Над графом можно проделывать операции, такие как добавление вершины, ребра, проверка наличия ребра в графе и т.п. На языке Python определение класса может выглядеть так:
from sets import Set as set # тип для множества
class G: def __init__(self, V, E): self.vertices = set(V) self.edges = set(E)
def add_vertex(self, v): self.vertices.add(v)
def add_edge(self, (v1, v2)): self.vertices.add(v1) self.vertices.add(v2) self.edges.add((v1, v2))
def has_edge(self, (v1, v2)): return (v1, v2) in self.edges
def __str__(self): return "%s; %s" % (self.vertices, self.edges)
Использовать класс можно следующим образом:
g = G([1, 2, 3, 4], [(1, 2), (2, 3), (2, 4)])
print g g.add_vertex(5) g.add_edge((5,6)) print g.has_edge((1,6)) print g
что даст в результате
Set([1, 2, 3, 4]); Set([(2, 4), (1, 2), (2, 3)]) False Set([1, 2, 3, 4, 5, 6]); Set([(2, 4), (1, 2), (5, 6), (2, 3)])
Как видно из предыдущего примера, определить класс не так уж сложно. Конструктор класса имеет специальное имя __init__. (Деструктор здесь не нужен, но он бы имел имя __del__.) Методы класса определяются в пространстве имен класса. В качестве первого формального аргумента метода принято использовать self. Кроме методов в объекте класса имеются два атрибута: vertices (вершины) и edges (ребра). Для представления объекта G в виде строки используется специальный метод __str__().
Принадлежность классу можно выяснить с помощью встроенной функции isinstance():
print isinstance(g, G)
Основные понятия
При процедурном программировании программа разбивается на части в соответствии с алгоритмом: каждая часть (подпрограмма, функция, процедура) является составной частью алгоритма.
При объектно-ориентированном программировании программа строится как совокупность взаимодействующих объектов.
С точки зрения объектно-ориентированного подхода, объект - это нечто, обладающее значением (состоянием), типом (поведением) и индивидуальностью. Когда программист выделяет объекты в предметной области, он обычно абстрагируется (отвлекается) от большинства их свойств, концентрируясь на существенных для задачи свойствах. Над объектами можно производить операции (посылая им сообщения). В языке Python все данные представлены в виде объектов.
Взаимодействие объектов заключается в вызове методов одних объектов другими. Иногда говорят, что объекты посылают друг другу сообщения. Сообщения - это запросы к объекту выполнить некоторые действия. (Сообщения, методы, операции, функции-члены являются синонимами).
Каждый объект хранит свое состояние (для этого у него есть атрибуты) и имеет определенный набор методов. (Синонимы: атрибут, поле, слот, объект-член, переменная экземпляра). Методы определяют поведение объекта. Объекты класса имеют общее поведение.
Объекты описываются не индивидуально, а с помощью классов. Класс - объект, являющийся шаблоном объекта. Объект, созданный на основе некоторого класса, называется экземпляром класса. Все объекты определенных пользователем классов являются экземплярами класса. Тем не менее, объекты даже с одним и тем же состоянием могут быть разными объектами. Говорят, что они имеют разную индивидуальность.
В языке Python для определения класса используется оператор class:
class имя_класса(класс1, класс2, ...): # определения методов
Класс определяет тип объекта, то есть его возможные состояния и набор операций.
Полиморфизм
В переводе с греческого полиморфизм означает "многоформие". Так в информатике называют возможность использования одного имени для выполнения различных действий.
Можно встретить множество определений полиморфизма (также есть несколько видов полиморфизма) в зависимости от языка программирования. Как правило, в качестве примера проявления полиморфизма приводят переопределение методов в подклассах. При этом можно создать функцию, требующую формального аргумента - экземпляра базового класса, а в качестве фактического аргумента давать экземпляр подкласса. Функция будет вызывать метод объекта с именем, а за именем будут скрываться различные действия. В связи с этим полиморфизм обычно связывают с иерархией наследования.
В Python полиморфизм связан не с наследованием, а с набором и смыслом доступных методов в экземпляре класса. Ниже будет показано, что, имея определенные методы, можно воссоздать класс для строки или любого другого встроенного типа. Для этого необходимо определить свойственный типу набор методов. Конечно, нужный набор методов можно получить и с помощью наследования, но в Python это не только не обязательно, но иногда и противоречит здравому смыслу.
При написании функции в Python обычно не проверяется, к какому типу (классу) относится тот или иной аргумент: некоторые методы просто применяются к переданному объекту. Тем самым функции получаются максимально обобщенными: они не требуют от объектов-параметров большего, чем наличие методов с определенным именем, набором аргументов и семантикой.
Следующий пример показывает полиморфизм в том виде, в котором он свойственен Python:
def get_last(x): return x[-1]
print get_last([1, 2, 3]) print get_last("abcd")
Описанной функции будет подходить в качестве аргумента все, от чего можно взять индекс -1 (последний элемент). Однако семантика "взятие последнего элемента" выполняется только для последовательностей. Функция будет работать и для словарей, но смысл при этом будет немного другой.
Порядок разрешения методов
В случае, когда надклассы имеют одинаковые методы, использование того или иного метода определяется порядком разрешения методов (method resolution order). Для "новых" классов узнать этот порядок очень просто с помощью атрибута __mro__:
>>> str.__mro__ (<type 'str'>, <type 'basestring'>, <type 'object'>)
Это означает, что сначала методы ищутся в классе str, затем в basestring, а уже потом - в object.
Для "классических" классов порядок несколько отличается от порядка разрешения методов в "новых" классах. Нужно стараться избегать множественного наследования или применять его очень аккуратно.
Слабые ссылки
Для обеспечения ассоциаций объектов без свойственных ссылкам проблем с возможностью образования циклических ссылок, в Python для сложных структур данных и других видов использования, при которых ссылки не должны мешать удалению объекта, предлагается механизм слабых ссылок. Такая ссылка не учитывается при подсчете ссылок на объект, а значит, объект удаляется с исчезновением последней "сильной" ссылки.
Для работы со слабыми ссылками применяется модуль weakref. Основные принципы его работы станут понятны из следующего примера:
>>> import weakref >>> >>> class MyClass(object): ... def __str__(self): ... return "MyClass" ... >>> >>> s = MyClass() # создается экземпляр класса >>> print s MyClass >>> s1 = weakref.proxy(s) # создается прокси-объект >>> print s1 # прокси-объект работает как исходный MyClass >>> ss = weakref.ref(s) # создается слабая ссылка на него >>> print ss() # вызовом ссылки получается исходный объект MyClass >>> del s # удаляется единственная сильная ссылка на объект >>> print ss() # теперь исходного объекта не существует None >>> print s1 Traceback (most recent call last): File "<stdin>", line 1, in ? ReferenceError: weakly-referenced object no longer exists
К сожалению, поведение прокси-объекта не совсем такое, как у исходного: он не может быть ключом словаря, так как является нехэшируемым.
Сокрытие данных
Подчеркивание ("_") в начале имени атрибута указывает на то, что он не входит в общедоступный интерфейс. Обычно применяется одиночное подчеркивание, которое в языке не играет особой роли, но как бы говорит программисту: "этот метод только для внутреннего использования". Двойное подчеркивание работает как указание на то, что атрибут - приватный. При этом атрибут все же доступен, но уже под другим именем, что и иллюстрируется ниже:
>>> class X: ... x = 0 ... _x = 0 ... __x = 0 ... >>> dir(X) ['_X__x', '__doc__', '__module__', '_x', 'x']
Ссылки
Дэвид Мертц http://www-106.ibm.com/developerworks/linux/library/l-pymeta.html
Статический метод
Иногда необходимо использовать метод, принадлежащий классу, а не его экземпляру. В этом случае можно описать статический метод. До появления декораторов (до Python 2.4) определять статический метод приходилось следующим образом:
class A(object): def name(): return A.__name__ name = staticmethod(name)
print A.name() a = A() print a.name()
Статическому методу не передается параметр с экземпляром класса. Он ему попросту не нужен.
В Python 2.4 для применения описателей (descriptors) был придуман новый синтаксис - декораторы:
class A(object):
@staticmethod def name(): return A.__name__
Смысл декоратора в том, что он "пропускает" определяемую функцию (или метод) через заданную в нем функцию. Теперь писать name три раза не потребовалось. Декораторов может быть несколько, и применяются они в обратном порядке.
Типы и классы
Тип определяет область допустимых значений объекта и набор операций над ним. В ООП тип тесно связан с поведением - действиями объекта, состоящими в изменении внутреннего состояния и вызовами методов других объектов.
Ранее в языке Python встроенные типы данных не являлись экземплярами класса, поэтому считалось, что это были просто объекты определенного типа. Теперь ситуация изменилась, и объекты встроенных типов имеют классы, к которым они принадлежат. Таким образом, тип и класс в Python становятся синонимами.
Интерпретатор языка Python всегда может сказать, к какому типу относится объект. Однако с точки зрения применимости объекта в операции его принадлежность к классу не играет решающей роли: гораздо важнее, какие методы поддерживает объект.
Примечание: Пока что в Python есть "классические" и "новые" классы. Первые классы определяются сами по себе, а вторые обязательно ведут свою родословную от класса object. Для целей данного изложения разница между этими видами классов не имеет значения. |
Экземпляры классов могут появляться в программе не только из литералов или в результате операций. Обычно для получения объекта класса достаточно вызвать конструктор этого класса с некоторыми параметрами. Объект-класс, как и объект-функция, может быть вызван. Это и будет вызовом конструктора:
>>> import sets >>> s = sets.Set([1, 2, 3])
В этом примере модуль sets содержит определение класса Set. Вызывается конструктор этого класса с параметром [1, 2, 3]. В результате с именем s будет связан объект-множество из трех элементов 1, 2, 3.
Следует заметить, что, кроме конструктора, определенные классы имеют и деструктор - метод, который вызывается при уничтожении объекта. В языке Python объект уничтожается в случае удаления последней ссылки на него либо в результате сборки мусора, если объект оказался в неиспользуемом цикле ссылок. Так как Python сам управляет распределением памяти, деструкторы в нем нужны очень редко. Обычно в том случае, когда объект управляет ресурсом, который нужно корректно вернуть в определенное состояние.
Еще один способ получить объект некоторого типа - использование функций-фабрик. По синтаксису вызов функции-фабрики не отличается от вызова конструктора класса.
Устойчивые объекты
Для того чтобы объекты жили дольше, чем создавшая их программа, необходим механизм их представления в виде последовательности байтов. Во второй лекции уже рассматривался модуль pickle, который позволяет сериализовать объекты.
Здесь же будет показано, как класс может способствовать более качественному консервированию объекта. Следующие методы, если их определить в классе, позволяют управлять работой модуля pickle и рассмотренной ранее функции глубокого копирования. Другими словами, правильно составленные методы дают возможность воссоздать объект, передав самую суть - состояние объекта.
__getinitargs__() | Должен возвращать кортеж из аргументов, который будет передаваться на вход метода __init__() при создании объекта. |
__getstate__() | Должен возвращать словарь, в котором выражено состояние объекта. Если этот метод в классе определен, то используется атрибут __dict__, который есть у каждого объекта. |
__setstate__(state) | Должен восстанавливать объекту ранее сохраненное состояние state. |
В следующем примере класс CC управляет своим копированием (точно так же экземпляры этого класса смогут консервироваться и расконсервироваться при помощи модуля pickle):
from time import time, gmtime import copy class CC: def __init__(self, created=time()): self.created = created self.created_gmtime = gmtime(created) self._copied = 1 print id(self), "init", created def __getinitargs__(self): print id(self), "getinitargs", self.created return (self.created,) def __getstate__(self): print id(self), "getstate", self.created return {'_copied': self._copied} def __setstate__(self, dict): print id(self), "setstate", dict self._copied = dict['_copied'] + 1 def __repr__(self): return "%s obj: %s %s %s" % (id(self), self._copied, self.created, self.created_gmtime)
a = CC() print a b = copy.deepcopy(a) print b
В результате будет получено
1075715052 init 1102751640.91 1075715052 obj: 1 1102751640.91 (2004, 12, 11, 7, 54, 0, 5, 346, 0) 1075715052 getinitargs 1102751640.91 1075729452 init 1102751640.91 1075715052 getstate 1102751640.91 1075729452 setstate {'copied': 1} 1075729452 obj: 2 1102751640.91 (2004, 12, 11, 7, 54, 0, 5, 346, 0)
Состояние объекта состоит из трех атрибутов: created, created_gmtime, copied. Первый из этих атрибутов может быть восстановлен передачей параметра конструктору. Второй - вычислен в конструкторе на основе первого. А вот третий не входит в интерфейс класса и может быть передан только через механизм getstate/setstate. Причем, по смыслу этого атрибута при каждом копировании он должен увеличиваться на единицу (хотя в разных случаях атрибут может требовать других действий или не требовать их вообще). Следует включить отладочные операторы вывода, чтобы отследить последовательность вызовов методов при копировании.
Механизм getstate/setstate позволяет передавать при копировании только то, что нужно для воссоздания объекта, тогда как атрибут __dict__ может содержать много лишнего. Более того, __dict__ может содержать объекты, которые просто так сериализации не поддаются, и поэтому getstate/setstate - единственная возможность обойти подобные ограничения.
Примечание: Следует заметить, что сериализация функций и классов - лишь кажущаяся: на принимающей стороне должны быть определения функций и классов, передаются же только их имена и принадлежность модулям. |
Это также касается передачи объектов по сетям передачи данных. Если простейшие объекты (вроде строк или чисел) можно передавать напрямую через HTTP, XML-RPC, SOAP и т.д., где они имеют собственный тип, то произвольные объекты необходимо консервировать на передающей стороне и расконсервировать на принимающей.
в ООП потребовало определения большого
Даже достаточно неформальное введение в ООП потребовало определения большого количества терминов. В лекции была сделана попытка с помощью примеров передать не столько букву, сколько дух терминологии ООП. Были рассмотрены все базовые понятия: объект, тип, класс и виды отношений между объектами (IS-A, HAS-A, USE-A). Слушатели получили представление о том, что такое инкапсуляция и полиморфизм в стиле ООП, а также наследование - продление времени жизни объекта за рамками исполняющейся программы, известное как устойчивость объекта (object persistence). Были указаны недостатки ООП, но при этом весь предыдущий материал объективно свидетельствовал о достоинствах этого подхода.
Возможно, что именно эта лекция приведет слушателей к пониманию ООП, пригодному и удобному для практической работы.
Функции для работы с массивами
Функций достаточно много, поэтому подробно будут рассмотрены только две из них, а остальные сведены в таблицу.
Функции модуля Numeric
Следующие функции модуля Numeric являются краткой записью некоторых наиболее употребительных сочетаний функций и методов:
sum(a, axis) | add.reduce(a, axis) |
cumsum(a, axis) | add.accumulate(a, axis) |
product(a, axis) | multiply.reduce(a, axis) |
cumproduct(a, axis) | multiply.accumulate(a, axis) |
alltrue(a, axis) | logical_and.reduce(a, axis) |
sometrue(a, axis) | logical_or.reduce(a, axis) |
Примечание: Параметр axis указывает размерность. |
Функции Numeric.diagonal() и Numeric.trace()
Функция Numeric.diagonal() возвращает диагональ матрицы. Она имеет следующие аргументы:
a | Исходный массив. |
offset | Смещение вправо от "главной" диагонали (по умолчанию 0). |
axis1 | Первое из измерений, на которых берется диагональ (по умолчанию 0). |
axis2 | Второе измерение, образующее вместе с первым плоскость, на которой и берется диагональ. По умолчанию axis2=1. |
Функция Numeric.trace() (для вычисления следа матрицы) имеет те же аргументы, но суммирует элементы на диагонали. В примере ниже рассмотрены обе эти функции:
>>> import Numeric >>> a = Numeric.reshape(Numeric.arrayrange(16), (4, 4)) >>> print a [[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11] [12 13 14 15]] >>> for i in range(-3, 4): ... print "Sum", Numeric.diagonal(a, i), "=", Numeric.trace(a, i) ... Sum [12] = 12 Sum [ 8 13] = 21 Sum [ 4 9 14] = 27 Sum [ 0 5 10 15] = 30 Sum [ 1 6 11] = 18 Sum [2 7] = 9 Sum [3] = 3
Функция Numeric.choose()
Эта функция использует один массив с целыми числами от 0 до n для выбора значения из одного из заданных массивов:
>>> a = Numeric.identity(4) >>> b0 = Numeric.reshape(Numeric.arrayrange(16), (4, 4)) >>> b1 = -Numeric.reshape(Numeric.arrayrange(16), (4, 4)) >>> print Numeric.choose(a, (b0, b1)) [[ 0 1 2 3] [ 4 -5 6 7] [ 8 9 -10 11] [ 12 13 14 -15]]
Функция Numeric.take()
Функция Numeric.take() позволяет взять часть массива по заданным на определенном измерении индексам. По умолчанию номер измерения (третий аргумент) равен нулю.
>>> import Numeric >>> a = Numeric.reshape(Numeric.arrayrange(25), (5, 5)) >>> print a [[ 0 1 2 3 4] [ 5 6 7 8 9] [10 11 12 13 14] [15 16 17 18 19] [20 21 22 23 24]] >>> print Numeric.take(a, [1], 0) [ [5 6 7 8 9]] >>> print Numeric.take(a, [1], 1) [[ 1] [ 6] [11] [16] [21]] >>> print Numeric.take(a, [[1,2],[3,4]]) [[[ 5 6 7 8 9] [10 11 12 13 14]] [[15 16 17 18 19] [20 21 22 23 24]]]
В отличие от среза, функция Numeric.take() сохраняет размерность массива, если конечно, структура заданных индексов одномерна. Результат Numeric.take(a, [[1,2],[3,4]]) показывает, что взятые по индексам части помещаются в массив со структурой самих индексов, как если бы вместо 1 было написано [5 6 7 8 9], а вместо 2 - [10 11 12 13 14] и т.д.
Методы массивов
Придать нужную форму массиву можно функцией Numeric.reshape(). Эта функция сразу создает объект-массив нужной формы из последовательности.
>>> import Numeric >>> print Numeric.reshape("абракадабр", (5, -1)) [[а б] [р а] [к а] [д а] [б р]]
В этом примере -1 в указании формы говорит о том, что соответствующее значение можно вычислить. Общее количество элементов массива известно (10), поэтому длину вдоль одной из размерностей задавать не обязательно.
Через атрибут flat можно получить одномерное представление массива:
>>> a = array([[1, 2], [3, 4]]) >>> b = a.flat >>> b array([1, 2, 3, 4]) >>> b[0] = 9 >>> b array([9, 2, 3, 4]) >>> a array([[9, 2], [3, 4]])
Следует заметить, что это новый вид того же массива, поэтому присваивание значений его элементам приводит к изменениям в исходном массиве.
Функция Numeric.resize()похожа на Numeric.reshape, но может подстраивать число элементов:
>>> print Numeric.resize("NUMERIC", (3, 2)) [[N U] [M E] [R I]] >>> print Numeric.resize("NUMERIC", (3, 4)) [[N U M E] [R I C N] [U M E R]]
Функция Numeric.zeros() порождает массив из одних нулей, а Numeric.ones() - из одних единиц. Единичную матрицу можно получить с помощью функции Numeric.identity(n):
>>> print Numeric.zeros((2,3)) [[0 0 0] [0 0 0]] >>> print Numeric.ones((2,3)) [[1 1 1] [1 1 1]] >>> print Numeric.identity(4) [[1 0 0 0] [0 1 0 0] [0 0 1 0] [0 0 0 1]]
Для копирования массивов можно использовать метод copy():
>>> import Numeric >>> a = Numeric.arrayrange(9) >>> a.shape = (3, 3) >>> print a [[0 1 2] [3 4 5] [6 7 8]] >>> a1 = a.copy() >>> a1[0, 1] = -1 # операция над копией >>> print a [[0 1 2] [3 4 5] [6 7 8]]
Массив можно превратить обратно в список с помощью метода tolist():
>>> a.tolist() [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
Модуль LinearAlgebra
Модуль LinearAlgebra содержит алгоритмы линейной алгебры, в частности нахождение определителя матрицы, решений системы линейных уравнений, обращение матрицы, нахождение собственных чисел и собственных векторов матрицы, разложение матрицы на множители: Холецкого, сингулярное, метод наименьших квадратов.
Функция LinearAlgebra.determinant() находит определитель матрицы:
>>> import Numeric, LinearAlgebra >>> print LinearAlgebra.determinant( ... Numeric.array([[1, -2], ... [1, 5]])) 7
Функция LinearAlgebra.solve_linear_equations() решает линейные уравнения вида ax=b по заданным аргументам a и b:
>>> import Numeric, LinearAlgebra >>> a = Numeric.array([[1.0, 2.0], [0.0, 1.0]]) >>> b = Numeric.array([1.2, 1.5]) >>> x = LinearAlgebra.solve_linear_equations(a, b) >>> print "x =", x x = [-1.8 1.5] >>> print "Проверка:", Numeric.dot(a, x) - b Проверка: [ 0. 0.]
Когда матрица a имеет нулевой определитель, система имеет не единственное решение и возбуждается исключение LinearAlgebraError:
>>> a = Numeric.array([[1.0, 2.0], [0.5, 1.0]]) >>> x = LinearAlgebra.solve_linear_equations(a, b) Traceback (most recent call last): File "<stdin>", line 1, in ? File "/usr/local/lib/python2.3/site-packages/Numeric/LinearAlgebra.py", line 98, in solve_linear_equations raise LinAlgError, 'Singular matrix' LinearAlgebra.LinAlgError: Singular matrix
Функция LinearAlgebra.inverse() находит обратную матрицу. Однако не следует решать линейные уравнения с помощью LinearAlgebra.inverse() умножением на обратную матрицу, так как она определена через LinearAlgebra.solve_linear_equations():
def inverse(a): return solve_linear_equations(a, Numeric.identity(a.shape[0]))
Функция LinearAlgebra.eigenvalues() находит собственные значения матрицы, а LinearAlgebra.eigenvectors() - пару: собственные значения, собственные вектора:
>>> from Numeric import array, dot >>> from LinearAlgebra import eigenvalues, eigenvectors >>> a = array([[-5, 2], [2, -7]]) >>> lmd = eigenvalues(a) >>> print "Собственные значения:", lmd Собственные значения: [-3.76393202 -8.23606798] >>> (lmd, v) = eigenvectors(a) >>> print "Собственные вектора:" Собственные вектора: >>> print v [[ 0.85065081 0.52573111] [-0.52573111 0.85065081]] >>> print "Проверка:", dot(a, v[0]) - v[0] * lmd[0] Проверка: [ -4.44089210e-16 2.22044605e-16]
Проверка показывает, что тождество выполняется с достаточно большой точностью (числа совсем маленькие, практически нули): собственные числа и векторы найдены верно.
Модуль Numeric
Модуль Numeric определяет полноценный тип-массив и содержит большое число функций для операций с массивами. Массив - это набор однородных элементов, доступных по индексам. Массивы модуля Numeric могут быть многомерными, то есть иметь более одной размерности.
Модуль RandomArray
В этом модуле собраны функции для генерации массивов случайных чисел различных распределений и свойств. Их можно применять для математического моделирования.
Функция RandomArray.random() создает массивы из псевдослучайных чисел, равномерно распределенных в интервале (0, 1):
>>> import RandomArray >>> print RandomArray.random(10) # массив из 10 псевдослучайных чисел [ 0.28374212 0.19260929 0.07045474 0.30547682 0.10842083 0.14049676 0.01347435 0.37043894 0.47362471 0.37673479] >>> print RandomArray.random([3,3]) # массив 3x3 из псевдослучайных чисел [[ 0.53493741 0.44636754 0.20466961] [ 0.8911635 0.03570878 0.00965272] [ 0.78490953 0.20674807 0.23657821]]
Функция RandomArray.randint() для получения массива равномерно распределенных чисел из заданного интервала и заданной формы:
>>> print RandomArray.randint(1, 10, [10]) [8 1 9 9 7 5 2 5 3 2] >>> print RandomArray.randint(1, 10, [10]) [2 2 5 5 7 7 3 4 3 7]
Можно получать и случайные перестановки с помощью RandomArray.permutation():
>>> print RandomArray.permutation(6) [4 0 1 3 2 5] >>> print RandomArray.permutation(6) [1 2 0 3 5 4]
Доступны и другие распределения для получения массива нормально распределенных величин с заданным средним и стандартным отклонением:
>>> print RandomArray.normal(0, 1, 30) [-1.0944078 1.24862444 0.20415567 -0.74283403 0.72461408 -0.57834256 0.30957144 0.8682853 1.10942173 -0.39661118 1.33383882 1.54818618 0.18814971 0.89728773 -0.86146659 0.0184834 -1.46222591 -0.78427434 1.09295738 -1.09731364 1.34913492 -0.75001568 -0.11239344 2.73692131 -0.19881676 -0.49245331 1.54091263 -1.81212211 0.46522358 -0.08338884]
Следующая таблица приводит функции для других распределений:
F(dfn, dfd, shape=[]) | F-распределение |
beta(a, b, shape=[]) | Бета-распределение |
binomial(trials, p, shape=[]) | Биномиальное распределение |
chi_square(df, shape=[]) | Распределение хи-квадрат |
exponential(mean, shape=[]) | Экспоненциальное распределение |
gamma(a, r, shape=[]) | Гамма-распределение |
multivariate_normal(mean, cov, shape=[]) | Многомерное нормальное распределение |
negative_binomial(trials, p, shape=[]) | Негативное биномиальное |
noncentral_F(dfn, dfd, nconc, shape=[]) | Нецентральное F-распределение |
noncentral_chi_square(df, nconc, shape=[]) | Нецентральное хи-квадрат распределение |
normal(mean, std, shape=[]) | Нормальное распределение |
permutation(n) | Случайная перестановка |
poisson(mean, shape=[]) | Пуассоновское распределение |
randint(min, max=None, shape=[]) | Случайное целое |
random(shape=[]) | Равномерное распределение на интервале (0, 1) |
random_integers(max, min=1, shape=[]) | Случайное целое |
standard_normal(shape=[]) | Стандартное нормальное распределение |
uniform(min, max, shape=[]) | Равномерное распределение |
Создание массива
Для создания массива можно использовать функцию array() с указанием содержимого массива (в виде вложенных списков) и типа. Функция array() делает копию, если ее аргумент - массив. Функция asarray() работает аналогично, но не создает нового массива, когда ее аргумент уже является массивом:
>>> from Numeric import * >>> print array([[1, 2], [3, 4], [5, 6]]) [[1 2] [3 4] [5 6]] >>> print array([[1, 2, 3], [4, 5, 6]], Float) [[ 1. 2. 3.] [ 4. 5. 6.]] >>> print array([78, 85, 77, 69, 82, 73, 67], 'c') [N U M E R I C]
В качестве элементов массива можно использовать следующие типы: Int8-Int32, UnsignedInt8-UnsignedInt32, Float8-Float64, Complex8-Complex64 и PyObject. Числа 8, 16, 32 и 64 показывают количество битов для хранения величины. Типы Int, UnsignedInteger, Float и Complex соответствуют наибольшим принятым на данной платформе значениям. В массиве можно также хранить ссылки на произвольные объекты.
Количество размерностей и длина массива по каждой оси называются формой массива (shape). Доступ к форме массива реализуется через атрибут shape:
>>> from Numeric import * >>> a = array(range(15), Int) >>> print a.shape (15,) >>> print a [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14] >>> a.shape = (3, 5) >>> print a.shape (3, 5) >>> print a [[ 0 1 2 3 4] [ 5 6 7 8 9] [10 11 12 13 14]]
Срезы
Объекты-массивы Numeric используют расширенный синтаксис выделения среза. Следующие примеры иллюстрируют различные варианты записи срезов. Функция Numeric.arrayrange() является аналогом range() для массивов.
>>> import Numeric >>> a = Numeric.arrayrange(24) + 1 >>> a.shape = (4, 6) >>> print a # исходный массив [[ 1 2 3 4 5 6] [ 7 8 9 10 11 12] [13 14 15 16 17 18] [19 20 21 22 23 24]] >>> print a[1,2] # элемент 1,2 9 >>> print a[1,:] # строка 1 [ 7 8 9 10 11 12] >>> print a[1] # тоже строка 1 [ 7 8 9 10 11 12] >>> print a[:,1] # столбец 1 [ 2 8 14 20] >>> print a[-2,:] # предпоследняя строка [13 14 15 16 17 18] >>> print a[0:2,1:3] # окно 2x2 [[2 3] [8 9]] >>> print a[1,::3] # каждый третий элемент строки 1 [ 7 10] >>> print a[:,::-1] # элементы строк в обратном порядке [[ 6 5 4 3 2 1] [12 11 10 9 8 7] [18 17 16 15 14 13] [24 23 22 21 20 19]]
Срез не копирует массив (как это имеет место со списками), а дает доступ к некоторой части массива. Далее в примере меняется на 0 каждый третий элемент строки 1:
>>> a[1,::3] = Numeric.array([0,0]) >>> print a [[ 1 2 3 4 5 6] [ 0 8 9 0 11 12] [13 14 15 16 17 18] [19 20 21 22 23 24]]
В следующих примерах находит применение достаточно редкая синтаксическая конструкция: срез с многоточием (Ellipsis). Многоточие ставится для указания произвольного числа пропущенных размерностей (:,:,...,:):
>>> import Numeric >>> a = Numeric.arrayrange(24) + 1 >>> a.shape = (2,2,2,3) >>> print a [[[[ 1 2 3] [ 4 5 6]] [[ 7 8 9] [10 11 12]]] [[[13 14 15] [16 17 18]] [[19 20 21] [22 23 24]]]] >>> print a[0,...] # 0-й блок [[[ 1 2 3] [ 4 5 6]] [[ 7 8 9] [10 11 12]]] >>> print a[0,:,:,0] # срез по первой и последней размерностям [[ 1 4] [ 7 10]] >>> print a[0,...,0] # то же, но с использованием многоточия [[ 1 4] [ 7 10]]
Свод функций модуля Numeric
Следующая таблица приводит описания функций модуля Numeric.
allclose(a, b[, eps[, A]]) | Сравнение a и b с заданными относительными eps и абсолютными A погрешностями. По умолчанию eps равен 1.0e-1, а A = 1.0e-8. |
alltrue(a[, axis]) | Логическое И по всей оси axis массива a |
argmax(a[, axis]) | Индекс максимального значения в массиве по заданному измерению axis |
argmin(a[, axis]) | Индекс минимального значения в массиве по заданному измерению axis |
argsort(a[, axis]) | Индексы отсортированного массива, такие, что take(a,argsort(a, axis),axis) дает отсортированный массив a, как если бы было выполнено sort(a, axis) |
array(a[, type]) | Создание массива на основе последовательности a данного типа type |
arrayrange(start[, stop[, step[, type]]]) | Аналог range() для массивов |
asarray(a[, type[, savespace]]) | То же, что и array(), но не создает новый массив, если a уже является массивом. |
choose(a, (b0,...,bn)) | Создает массив на основе элементов, взятых по индексам из a (индексы от 0 до n включительно). Формы массивов a, b1, ..., bn должны совпадать |
clip(a, a_min, a_max) | Обрубает значения массива a так, чтобы они находились между значениями из a_min и a_max поэлементно |
compress(cond, a[, axis]) | Возвращает массив только из тех элементов массива a, для которых условие cond истинно (не нуль) |
concatenate(a[, axis]) | Соединение двух массивов (конкатенация) по заданному измерению axis (по умолчанию - по нулевой) |
convolve(a, b[, mode]) | Свертка двух массивов. Аргумент mode может принимать значения 0, 1 или 2 |
cross_correlate(a, b[, mode]) | Взаимная корреляция двух массивов. Параметр mode может принимать значения 0, 1 или 2 |
cumproduct(a[, axis]) | Произведение по измерению axis массива a с промежуточными результатами |
cumsum(a[, axis]) | Суммирование с промежуточными результатами |
diagonal(a[, k[, axis1[, axis2]]]) | Взятие k-й диагонали массива a в плоскости измерений axis1 и axis2 |
dot(a, b) | Внутреннее (матричное) произведение массивов. По определению: innerproduct(a, swapaxes(b, -1, -2)), т.е. с переставленными последними измерениями, как и должно быть при перемножении матриц |
dump(obj, file) | Запись массива a (в двоичном виде) в открытый файловый объект file. Файл должен быть открыт в бинарном режиме. В файл можно записать несколько объектов подряд |
dumps(obj) | Строка с двоичным представлением объекта obj |
fromfunction(f, dims) | Строит массив, получая информацию от функции f(), в качестве аргументов которой выступают значения кортежа индексов. Фактически является сокращением для f(*tuple(indices(dims))) |
fromstring(s[, count[, type]]) | Создание массива на основе бинарных данных, хранящихся в строке |
identity(n) | Возвращает двумерный массив формы (n, n) |
indices(dims[, type]) | Возвращает массив индексов заданной длины по каждому измерению с изменением поочередно по каждому изменению. Например, indices([2, 2])[1] дает двумерный массив [[0, 1], [0, 1]]. |
innerproduct(a, b) | Внутреннее произведение двух массивов (по общему измерению). Для успешной операции a.shape[-1] должен быть равен b.shape[-1]. Форма результата будет a.shape[:-1] + b.shape[:-1]. Элементы пропадающего измерения попарно умножаются и получающиеся произведения суммируются |
load(file) | Чтение массива из файла file. Файл должен быть открыт в бинарном режиме |
loads(s) | Возвращает объект, соответствующий бинарному представлению, заданному в строке |
nonzero(a) | Возвращает индексы ненулевых элементов одномерного массива |
ones(shape[, type]) | Массив из единиц заданной формы shape и обозначения типа type |
outerproduct(a, b) | Внешнее произведение a и b |
product(a[, axis]) | Произведение по измерению axis массива a |
put(a, indices, b) | Присваивание частям массива, a[n] = b[n] для всех индексов indices |
putmask(a, mask, b) | Присваивание a элементов из b, для которых маска mask имеет значение истина |
ravel(a) | Превращение массива в одномерный. Аналогично reshape(a, (-1,)) |
repeat(a, n[, axis]) | Повторяет элементы массива a n раз по измерению axis |
reshape(a, shape) | Возвращает массив нужной формы (нового массива не создает). Количество элементов в исходном и новом массивах должно совпадать |
resize(a, shape) | Возвращает массив с произвольной новой формой shape. Размер исходного массива не важен |
searchsorted(a, i) | Для каждого элемента из i найти место в массиве a. Массив a должен быть одномерным и отсортированным. Результат имеет форму массива i |
shape(a) | Возвращает форму массива a |
sometrue(a[, axis]) | Логическое ИЛИ по всему измерению axis массива a |
sort(a[, axis]) | Сортировка элементов массива по заданному измерению |
sum(a[, axis]) | Суммирование по измерению axis массива a |
swapaxes(a, axis1, axis1) | Смена измерений (частный случай транспонирования) |
take(a, indices[, axis]) | Выбор частей массива a на основе индексов indices по измерению axis |
trace(a[, k[, axis1[, axis2]]]) | Сумма элементов вдоль диагонали, то есть add.reduce(diagonal(a, k, axis1, axis2)) |
transpose(a[, axes]) | Перестановка измерений в соответствии с axes, либо, если axes не заданы - расположение их в обратном порядке |
where(cond, a1, a2) | Выбор элементов на основании условия cond из a1 (если не нуль) и a2 (при нуле) поэлементно. Равносилен choose(not_equal(cond, 0), (y, x)). Формы массивов-аргументов a1 и a2 должны совпадать |
zeros(shape[, type]) | Массив из нулей заданной формы shape и обозначения типа type |
В этой таблице в качестве обозначения типа type можно указывать рассмотренные выше константы: Int, Float и т.п.
Модуль Numeric также определяет константы e (число e) и pi (число пи).
Универсальные функции
Модуль Numeric определяет набор функций для применения к элементам массива. Функции применимы не только к массивам, но и к последовательностям (к сожалению, итераторы пока не поддерживаются). В результате получаются массивы.
add(x, y), subtract(x, y) | Сложение и вычитание |
multiply(x, y), divide(x, y) | Умножение и деление |
remainder(x, y), fmod(x, y) | Получение остатка от деления (для целых чисел и чисел с плавающей запятой) |
power(x, y) | Возведение в степень |
sqrt(x) | Извлечение корня квадратного |
negative(x), absolute(x), fabs(x) | Смена знака и абсолютное значение |
ceil(x), floor(x) | Наименьшее (наибольшее) целое, большее (меньшее) или равное аргументу |
hypot(x, y) | Длина гипотенузы (даны длины двух катетов) |
sin(x), cos(x), tan(x) | Тригонометрические функции |
arcsin(x), arccos(x), arctan(x) | Обратные тригонометрические функции |
arctan2(x, y) | Арктангенс от частного аргумента |
sinh(x), cosh(x), tanh(x) | Гиперболические функции |
arcsinh(x), arccosh(x), arctanh(x) | Обратные гиперболические функции |
exp(x) | Экспонента (ex) |
log(x), log10(x) | Натуральный и десятичный логарифмы |
maximum(x, y), minimum(x, y) | Максимум и минимум |
conjugate(x) | Сопряжение (для комплексных чисел) |
equal(x, y), not_equal(x, y) | Равно, не равно |
greater(x, y), greater_equal(x, y) | Больше, больше или равно |
less(x, y), less_equal(x, y) | Меньше, меньше или равно |
logical_and(x, y), logical_or(x, y) | Логические И, ИЛИ |
logical_xor(x, y) | Логическое исключающее ИЛИ |
logical_not(x) | Логические НЕ |
bitwise_and(x, y), bitwise_or(x, y) | Побитовые И, ИЛИ |
bitwise_xor(x, y) | Побитовое исключающее ИЛИ |
invert(x) | Побитовая инверсия |
left_shift(x, n), right_shift(x, n) | Побитовые сдвиги влево и вправо на n битов |
Перечисленные функции являются объектами типа ufunc и применяются к массивам поэлементно. Эти функции имеют специальные методы:
accumulate() | Аккумулирование результата. |
outer() | Внешнее "произведение". |
reduce() | Сокращение. |
reduceat() | Сокращение в заданных точках. |
Пример с функцией add() позволяет понять смысл универсальной функции и ее методов:
>>> from Numeric import add >>> add([[1, 2], [3, 4]], [[1, 0], [0, 1]]) array([[2, 2], [3, 5]]) >>> add([[1, 2], [3, 4]], [1, 0]) array([[2, 2], [4, 4]]) >>> add([[1, 2], [3, 4]], 1) array([[2, 3], [4, 5]]) >>> add.reduce([1, 2, 3, 4]) # т.е. 1+2+3+4 10 >>> add.reduce([[1, 2], [3, 4]], 0) # т.е. [1+3 2+4] array([4, 6]) >>> add.reduce([[1, 2], [3, 4]], 1) # т.е. [1+2 3+4] array([3, 7]) >>> add.accumulate([1, 2, 3, 4]) # т.е. [1 1+2 1+2+3 1+2+3+4] array([ 1, 3, 6, 10]) >>> add.reduceat(range(10), [0, 3, 6]) # т.е. [0+1+2 3+4+5 6+7+8+9] array([ 3, 12, 30]) >>> add.outer([1,2], [3,4]) # т.е. [[1+3 1+4] [2+3 2+4]] array([[4, 5], [5, 6]])
Методы accumulate(), reduce() и reduceat() принимают необязательный аргумент - номер размерности, используемой для соответствующего действия. По умолчанию применяется нулевая размерность.
Универсальные функции, помимо одного или двух необходимых параметров, позволяют задавать и еще один аргумент, для приема результата функции. Тип третьего аргумента должен строго соответствовать типу результата. Например, функция sqrt() даже от целых чисел имеет тип Float.
>>> from Numeric import array, sqrt, Float >>> a = array([0, 1, 2]) >>> r = array([0, 0, 0], Float) >>> sqrt(a, r) array([ 0. , 1. , 1.41421356]) >>> print r [ 0. 1. 1.41421356]
Предупреждение: Не следует использовать в качестве приемника результата массив, который фигурирует в предыдущих аргументах функции, так как при этом результат может быть испорчен. Следующий пример показывает именно такой вариант: >>> import Numeric >>> m = Numeric.array([0, 0, 0, 1, 0, 0, 0, 0]) >>> add(m[:-1], m[1:], m[1:]) array([0, 0, 1, 1, 1, 1, 1]) В таких неоднозначных случаях необходимо использовать промежуточный массив. |
В этой лекции рассматривался набор
В этой лекции рассматривался набор модулей для численных вычислений. Модуль Numeric определяет тип многомерный массив и множество функций для работы с массивами. Также были представлены модули для линейной алгебры и моделирования последовательностей случайных чисел различных распределений.
Анализ записи числа
Хороший пример регулярного выражения можно найти в модуле fpformat. Это регулярное выражение позволяет разобрать запись числа (в том виде, в каком числовой литерал принято записывать в Python):
decoder = re.compile(r'^([-+]?)0*(\d*)((?:\.\d*)?)(([eE][-+]?\d+)?)$') # Следующие части числового литерала выделяются с помощью групп: # \0 - весь литерал # \1 - начальный знак или пусто # \2 - цифры слева от точки # \3 - дробная часть (пустая или начинается с точки) # \4 - показатель (пустой или начинается с 'e' или 'E')
Например:
import re decoder = re.compile(r'^([-+]?)0*(\d*)((?:\.\d*)?)((?:[eE][-+]?\d+)?)$')
print decoder.match("12.234").groups() print decoder.match("-0.23e-7").groups() print decoder.match("1e10").groups()
Получим
('', '12', '.234', '') ('-', '', '.23', 'e-7') ('', '1', '', 'e10')
Индексы и срезы
Следует напомнить, что строки являются неизменчивыми последовательностями, поэтому к ним можно применять операции взятия элемента по индексу и срезы:
>>> s = "транспорт" >>> print s[0], s[-1] т т >>> print s[-4:] порт >>> print s[:5] транс >>> print s[4:8] спор
Примечание: При выделении среза нумеруются не символы строки, а промежутки между ними. |
Кодировка Python-программы
Для того чтобы Unicode-литералы в Python-программе воспринимались интерпретатором правильно, необходимо указать кодировку в начале программы, записав в первой или второй строке примерно следующее (для Unix/Linux):
# -*- coding: koi8-r -*-
или (под Windows):
# -*- coding: cp1251 -*-
Могут быть и другие варианты:
# -*- coding: latin-1 -*- # -*- coding: utf-8 -*- # -*- coding: mac-cyrillic -*- # -*- coding: iso8859-5 -*-
Полный перечень кодировок (и их псевдонимов):
>>> import encodings.aliases >>> print encodings.aliases.aliases {'iso_ir_6': 'ascii', 'maccyrillic': 'mac_cyrillic', 'iso_celtic': 'iso8859_14', 'ebcdic_cp_wt': 'cp037', 'ibm500': 'cp500', ...
Если кодировка не указана, то считается, что используется us-ascii. При этом интерпретатор Python будет выдавать предупреждения при запуске модуля:
sys:1: DeprecationWarning: Non-ASCII character '\xf0' in file example.py on line 2, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
Методы объекта-шаблона
В результате успешной компиляции шаблона функцией re.compile() получается шаблон-объект (он именуется SRE_Pattern), который имеет несколько методов, некоторые из них будут рассмотрены. Как обычно, подробности и информация о дополнительных аргументах - в документации по Python.
match(s)
Сопоставляет строку s с шаблоном, возвращая в случае удачного сопоставления объект с результатом сравнения (объект SRE_Match). В случае неудачи возвращает None. Сопоставление начинается от начала строки.
search(s)
Аналогичен match(s), но ищет подходящую подстроку по всей строке s.
split(s[, maxsplit=0])
Разбивает строку на подстроки, разделенные подстроками, заданными шаблоном. Если в шаблоне выделены группы, они попадут в результирующий список, перемежаясь с подстроками между разделителями. Если указан maxsplit, будет произведено не более maxsplit разбиений.
findall(s)
Ищет все неперекрывающиеся подстроки s, удовлетворяющие шаблону.
finditer(s)
Возвращает итератор по объектам с результатами сравнения для всех неперекрывающихся подстрок, удовлетворяющих шаблону.
sub(repl, s)
Заменяет в строке s все (или только count, если он задан) вхождения неперекрывающихся подстрок, удовлетворяющих шаблону, на строку, заданную с помощью repl. В качестве replможет выступать строка или функция. Возвращает строку с выполненными заменами. В первом случае строка repl подставляется не просто так, а интерпретируется с заменой вхождений "\номер" на группу с соответствующим номером и вхождений "\g<имя>" на группу с номером или именем имя. В случае, когда repl - функция, ей передается объект с результатом каждого успешного сопоставления, а из нее возвращается строка для замены.
subn(repl, s)
Аналогичен sub(), но возвращает кортеж из строки с выполненными заменами и числа замен.
В следующем примере строка разбивается на подстроки по заданному шаблону:
>>> import re >>> delim_re = re.compile(r"[:,;]") >>> text = "This,is;example" >>> print delim_re.split(text) ['This', 'is', 'example']
А теперь можно узнать, чем именно были разбиты строки:
>>> delim_re = re.compile(r"([:,;])") >>> print delim_re.split(text) ['This', ',', 'is', ';', 'example']
Методы строк
В таблице ниже приведены некоторые наиболее употребительные методы объектов-строк и unicode-объектов.
center(w) | Центрирует строку в поле длины w |
count(sub) | Число вхождений строки sub в строке |
encode([enc[, errors]]) | Возвращает строку в кодировке enc. Параметр errors может принимать значения "strict" (по умолчанию), "ignore", "replace" или "xmlcharrefreplace" |
endswith(suffix) | Оканчивается ли строка на suffix |
expandtabs([tabsize]) | Заменяет символы табуляции на пробелы. По умолчанию tabsize=8 |
find(sub [,start [,end]]) | Возвращает наименьший индекс, с которого начинается вхождение подстроки sub в строку. Параметры start и end ограничивают поиск окном start:end, но возвращаемый индекс соответствует исходной строке. Если подстрока не найдена, возвращается -1 |
index(sub[, start[, end]]) | Аналогично find(), но возбуждает исключение ValueError в случае неудачи |
alnum() | Возвращает True, если строка содержит только буквы и цифры и имеет ненулевую длину. Иначе -- False |
isalpha() | Возвращает True, если строка содержит только буквы и длина ненулевая |
isdecimal() | Возвращает True, если строка содержит только десятичные знаки (только для строк Unicode) и длина ненулевая |
isdigit() | Возвращает True, если содержит только цифры и длина ненулевая |
islower() | Возвращает True, если все буквы строчные (и их более одной), иначе -- False |
isnumeric() | Возвращает True, если в строке только числовые знаки (только для Unicode) |
isspace() | Возвращает True, если строка состоит только из пробельных символов. Внимание! Для пустой строки возвращается False |
join(seq) | Соединение строк из последовательности seq через разделитель, заданный строкой |
lower() | Приводит строку к нижнему регистру букв |
lstrip() | Удаляет пробельные символы слева |
replace(old, new[, n]) | Возвращает копию строки, в которой подстроки old заменены new. Если задан параметр n, то заменяются только первые n вхождений |
rstrip() | Удаляет пробельные символы справа |
split([sep[, n]]) | Возвращает список подстрок, получающихся разбиением строки a разделителем sep. Параметр n определяет максимальное количество разбиений (слева) |
startswith(prefix) | Начинается ли строка с подстроки prefix |
strip() | Удаляет пробельные символы в начале и в конце строки |
translate(table) | Производит преобразование с помощью таблицы перекодировки table, содержащей словарь для перевода кодов в коды (или в None, чтобы удалить символ). Для Unicode-строк |
translate(table[, dc]) | То же, но для обычных строк. Вместо словаря - строка перекодировки на 256 символов, которую можно сформировать с помощью функции string.maketrans(). Необязательный параметр dc задает строку с символами, которые необходимо удалить |
upper() | Переводит буквы строки в верхний регистр |
В следующем примере применяются методы split() и join() для разбиения строки в список (по разделителям) и обратное объединение списка строк в строку
>>> s = "This is an example." >>> lst = s.split(" ") >>> print lst ['This', 'is', 'an', 'example.'] >>> s2 = "\n".join(lst) >>> print s2 This is an example.
Для проверки того, оканчивается ли строка на определенное сочетание букв, можно применить метод endswith():
>>> filenames = ["file.txt", "image.jpg", "str.txt"] >>> for fn in filenames: ... if fn.lower().endswith(".txt"): ... print fn ... file.txt str.txt
Поиск в строке можно осуществить с помощью метода find(). Следующая программа выводит все функции, определенные в модуле оператором def:
import string text = open(string.__file__[:-1]).read() start = 0 while 1: found = text.find("def ", start) if found == -1: break print text[found:found + 60].split("(")[0] start = found + 1
Важным для преобразования текстовой информации является метод replace(), который рассматривается ниже:
>>> a = "Это текст , в котором встречаются запятые , поставленные не так." >>> b = a.replace(" ,", ",") >>> print b Это текст, в котором встречаются запятые, поставленные не так.
Множественная замена
В некоторых приложениях требуется производить в тексте сразу несколько замен. Для решения этой задачи можно использовать метод sub() вместе со специальной функцией, которая и будет управлять заменами:
import re
def multisub(subs_dict, text): def _multisub(match_obj): return str(subs_dict[match_obj.group()])
multisub_re = re.compile("|".join(subs_dict.keys())) return multisub_re.sub(_multisub, text)
repl_dict = {'one': 1, 'two': 2, 'three': 3}
print multisub(repl_dict, "One, two, three")
Будет выведено
One, 2, 3
В качестве упражнения предлагается сделать версию, которая бы не учитывала регистр букв.
В приведенной программе вспомогательная функция _multisub() по полученному объекту с результатом сравнения возвращает значение из словаря с описаниями замен subs_dict.
Модуль difflib
Для приблизительного сравнения двух строк в стандартной библиотеке предусмотрен модуль difflib.
Функция difflib.get_close_matches() позволяет выделить n близких строк к заданной строке:
get_close_matches(word, possibilities, n=3, cutoff=0.6)
где
word
Строка, к которой ищутся близкие строки.
possibilities
Список возможных вариантов.
n
Требуемое количество ближайших строк.
cutoff
Коэффициент (из диапазона [0, 1]) необходимого уровня совпадения строк. Строки, которые при сравнении с word дают меньшее значение, игнорируются.
Следующий пример показывает функцию difflib.get_close_matches() в действии:
>>> import unicodedata >>> names = [unicodedata.name(unicode(chr(i))) for i in range(40, 127)] >>> print difflib.get_close_matches("LEFT BRACKET", names) ['LEFT CURLY BRACKET', 'LEFT SQUARE BRACKET']
В списке names - названия Unicode-символов с ASCII-кодами от 40 до 127.
Модуль string
До того как у строк появились методы, для операций над строками применялся модуль string. Приведенный пример демонстрирует, как вместо функции из string использовать метод (кстати, последнее более эффективно):
>>> import string >>> s = "one,two,three" >>> print string.split(s, ",") ['one', 'two', 'three'] >>> print s.split(",") ['one', 'two', 'three']
В версии Python 3.0 функции, которые доступны через методы, более не будут дублироваться в модуле string.
В Python 2.4 появилась альтернатива использованию операции форматирования: класс Template. Пример:
>>> import string >>> tpl = string.Template("$a + $b = ${c}") >>> a = 2 >>> b = 3 >>> c = a + b >>> print tpl.substitute(vars()) 2 + 3 = 5 >>> del c # удаляется имя c >>> print tpl.safe_substitute(vars()) 2 + 3 = $c >>> print tpl.substitute(vars(), c=a+b) 2 + 3 = 5 >>> print tpl.substitute(vars()) Traceback (most recent call last): File "/home/rnd/tmp/Python-2.4b2/Lib/string.py", line 172, in substitute return self.pattern.sub(convert, self.template) File "/home/rnd/tmp/Python-2.4b2/Lib/string.py", line 162, in convert val = mapping[named] KeyError: 'c'
Объект-шаблон имеет два основных метода: substitute() и safe_substitute(). Значения для подстановки в шаблон берутся из словаря (vars() содержит словарь со значениями переменных) или из именованных фактических параметров. Если есть неоднозначность в задании ключа, можно использовать фигурные скобки при написании ключа в шаблоне.
Модуль StringIO
В некоторых случаях желательно работать со строкой как с файлом. Модуль StringIO как раз дает такую возможность.
Открытие "файла" производится вызовом StringIO(). При вызове без аргумента - создается новый "файл", при задании строки в качестве аргумента - "файл" открывается для чтения:
import StringIO my_string = "1234567890" f1 = StringIO.StringIO() f2 = StringIO.StringIO(my_string)
Далее с файлами f1 и f2 можно работать как с обычными файловыми объектами.
Для получения содержимого такого файла в виде строки применяется метод getvalue():
f1.getvalue()
Противоположный вариант (представление файла на диске в виде строки) можно реализовать на платформах Unix и Windows с использованием модуля mmap. Здесь этот модуль рассматриваться не будет.
Обработка лога
Предыдущий пример регулярного выражения позволит выделить из лога записи с определенной меткой и подать их в сокращенном виде:
import re log_re = re.compile(r"""(?P<date>[A-Za-z]{3}\s+\d+\s+\d\d:\d\d:\d\d) \S+ kernel: PAY: .+ DST=(?P<dst>\S+).* LEN=(?P<len>\d+).* DPT=(?P<dpt>\d+) """)
for line in open("message.log"): m = log_re.match(line) if m: print "%(date)s %(dst)s:%(dpt)s size=%(len)s" % m.groupdict()
В результате получается
Nov 27 15:57:59 192.168.1.115:1039 size=1500 Nov 27 15:57:59 192.168.1.200:8080 size=40 Nov 27 15:57:59 192.168.1.115:1039 size=515 Nov 27 15:57:59 192.168.1.200:8080 size=40 Nov 27 15:57:59 192.168.1.115:1039 size=40 Nov 27 15:57:59 192.168.1.200:8080 size=40 Nov 27 15:57:59 192.168.1.115:1039 size=40
Операции над строками
К операциям над строками, которые имеют специальную синтаксическую поддержку в языке, относятся, в частности конкатенация (склеивание) строк, повторение строки, форматирование:
>>> print "A" + "B", "A"*5, "%s" % "A" AB AAAAA A
В операции форматирования левый операнд является строкой формата, а правый может быть либо кортежем, либо словарем, либо некоторым значением другого типа:
>>> print "%i" % 234 234 >>> print "%i %s %3.2f" % (5, "ABC", 23.45678) 5 ABC 23.46 >>> a = 123 >>> b = [1, 2, 3] >>> print "%(a)i: %(b)s" % vars() 123: [1, 2, 3]
Операция форматирования
В строке формата кроме текста могут употребляться спецификации, регламентирующие формат выводимого значения. Спецификация имеет синтаксис
"%" [ключ][флаг*][шир][.точность][длина_типа]спецификатор ключ: "(" символ за исключением круглых скобок* ")" флаг: "+" | "-" | пробел | "#" | "0" шир: ("1" ... "9")("0" ... "9")* | "*" точность: ("1" ... "9")* | "*" длина_типа: "a" ... "z" | "A" ... "Z" спецификатор: "a" ... "z" | "A" ... "Z" | "%"
Где символы обозначают следующее:
ключ
Ключ из словаря.
флаги
Дополнительные свойства преобразования.
шир
Минимальная ширина поля.
точность
Точность (для чисел с плавающей запятой).
длина_типа
Модификатор типа.
спецификатор
Тип представления выводимого объекта.
В следующей таблице приведены некоторые наиболее употребительные значения для спецификации форматирования.
0 | флаг | Заполнение нулями слева |
- | флаг | Выравнивание по левому краю |
+ | флаг | Обязательный вывод знака числа |
пробел | флаг | Использовать пробел на месте знака числа |
d, i | спецификатор | Знаковое целое |
u | спецификатор | Беззнаковое целое |
o | спецификатор | Восьмеричное беззнаковое целое |
x, X | спецификатор | Шестнадцатеричное беззнаковое целое (со строчными или прописными латинскими буквами) |
e, E | спецификатор | Число с плавающей запятой в формате с экспонентой |
f, F | спецификатор | Число с плавающей запятой |
g, G | спецификатор | Число с плавающей точкой в более коротком написании (автоматически выбирается e или f) |
с | спецификатор | Одиночный символ (целое число или односимвольная строка) |
r | спецификатор | Любой объект, приведенный к строке функцией repr() |
s | спецификатор | Любой объект, приведенный к строке функцией str() |
% | спецификатор | Знак процента. Для задания одиночного процента необходимо записать %% |
Отладка регулярных выражений
Следующий небольшой сценарий позволяет отлаживать регулярное выражение, при условии, что есть пример строки, которой шаблон должен удовлетворять. Взят кусочек лога iptables, его необходимо разобрать для получения полей. Интересны строки, в которых после kernel: стоит PAY:, а в этих строках нужно получить дату, значения DST, LEN и DPT:
import re
def debug_regex(regex, example): """Отладка рег. выражения. Перед отладкой лучше убрать лишние скобки """ last_good = "" for i in range(1, len(regex)): try: if re.compile(regex[:i]).match(example): last_good = regex[:i] except: continue return last_good
example = """Nov 27 15:57:59 lap kernel: PAY: IN=eth0 OUT= MAC=00:50:da:d9:df:a2:00:00:1c:b0:c9:db:08:00 SRC=192.168.1.200 DST=192.168.1.115 LEN=1500 TOS=0x00 PREC=0x00 TTL=64 ID=31324 DF PROTO=TCP SPT=8080 DPT=1039 WINDOW=17520 RES=0x00 ACK PSH URGP=0"""
log_re = r"""[A-Za-z]{3}\s+\d+\s+\d\d\d\d:\d\d) \S+ kernel: PAY: .+ DST=(?P<dst>\S+).* LEN=(?P<len>\d+).* DPT=(?P<dpt>\d+) """
print debug_regex(log_re, example)
Функция debug_regex() пробует сопоставлять пример с увеличивающимися порциями регулярного выражения и возвращает последнее удавшееся сопоставление:
[A-Za-z]{3}\s+\d+\s+\d\d
Сразу видно, что не поставлен символ :.
Примеры шаблонов
Владение регулярными выражениями может существенно ускорить построение алгоритмов для обработки данных. Лучше всего познакомиться с шаблонами на конкретных примерах:
r"\b\w+\b"
Соответствует слову из букв и знаков подчеркивания.
r"[+-]?\d+"
Соответствует целому числу. Возможно, со знаком.
r"\([+-]?\d+\)"
Число, стоящее в скобках. Скобки используются в самих регулярных выражениях, поэтому они экранируются "\".
r"[a-cA-C]{2}"
Соответствует строке из двух букв "a", "b" или "c". Например, "Ac", "CC", "bc".
r"aa|bb|cc|AA|BB|CC"
Строка из двух одинаковых букв.
r"([a-cA-C])\1"
Строка из двух одинаковых букв, но шаблон задан с использованием групп
r"aa|bb".
Соответствует "aa" или "bb"
r"a(a|b)b"
Соответствует "aab" или "abb"
r"^(?:\d{8}|\d{4}):\s*(.*)$"
Соответствует строке, которая начинается с набора из восьми или четырех цифр и двоеточия. Все, что идет после двоеточия и после следующих за ним пробелов, выделяется в группу с номером 1, тогда как набор цифр в группу не выделен.
r"(\w+)=.*\b\1\b"
Слова слева и справа от знака равенства присутствуют. Операнд "\1" соответствует группе с номером 1, выделенной с помощью скобок.
r"(?P<var>\w+)=.*\b(?P=var)\b"
То же самое, но теперь используется именованная группа var.
r"\bregular(?=\s+expression)".
Соответствует слову "regular" только в том случае, если за ним после пробелов следует "expression"
r"(?<=regular )expression"
Соответствует слову "expression", перед которым стоит "regular"и один пробел.
Следует заметить, что примеры со взглядом назад могут сильно влиять на производительность, поэтому их не стоит использовать без особой необходимости.
Работа с несколькими файлами
Для упрощения работы с несколькими файлами можно использовать модуль fileinput. Он позволяет обработать в одном цикле строки всех указанных в командной строке файлов:
import fileinput for line in fileinput.input(): process(line)
В случае, когда файлов не задано, обрабатывается стандартный ввод.
Работа с Unicode
До появления Unicode символы в компьютере кодировались одним байтом (а то и только семью битами). Один байт охватывает диапазон кодов от 0 до 255 включительно, а это значит, что больше двух алфавитов, цифр, знаков пунктуации и некоторого набора специальных символов в одном байте не помещается. Каждый производитель использовал свою кодировку для одного и того же алфавита. Например, до настоящего времени дожили целых пять кодировок букв кириллицы, и каждый пользователь не раз видел в своем браузере или электронном письме пример несоответствия кодировок.
Стандарт Unicode - единая кодировка для символов всех языков мира. Это большое облегчение и некоторое неудобство одновременно. Плюс состоит в том, что в одной Unicode-строке помещаются символы совершенно различных языков. Минус же в том, что пользователи привыкли применять однобайтовые кодировки, большинство приложений ориентировано на них, во многих системах поддержка Unicode осуществляется лишь частично, так как требует огромной работы по разработке шрифтов. Правда, символы одной кодировки можно перевести в Unicode и обратно.
Здесь же следует заметить, что файлы по-прежнему принято считать последовательностью байтов, поэтому для хранения текста в файле в Unicode требуется использовать одну из транспортных кодировок Unicode (utf-7, utf-8, utf-16,...). В некоторых их этих кодировок имеет значение принятый на данной платформе порядок байтов (big-endian, старшие разряды в конце или little-endian, младшие в конце). Узнать порядок байтов можно, прочитав атрибут из модуля sys. На платформе Intel это выглядит так:
>>> sys.byteorder 'little'
Для исключения неоднозначности документ в Unicode может быть в самом начале снабжен BOM (byte-order mark - метка порядка байтов) - Unicode-символом с кодом 0xfeff. Для данной платформы строка байтов для BOM будет такой:
>>> codecs.BOM_LE '\xff\xfe'
Для преобразования строки в Unicode необходимо знать, в какой кодировке закодирован текст. Предположим, что это cp1251. Тогда преобразовать текст в Unicode можно следующим способом:
>>> s = "Строка в cp1251" >>> s.decode("cp1251") u'\u0421\u0442\u0440\u043e\u043a\u0430 \u0432 cp1251'
То же самое с помощью встроенной функции unicode():
>>> unicode(s, 'cp1251') u'\u0421\u0442\u0440\u043e\u043a\u0430 \u0432 cp1251'
Одной из полезных функций этого модуля является функция codecs.open(), позволяющая открыть файл в другой кодировке:
codecs.open(filename, mode[, enc[, errors[, buffer]]])
Здесь:
filename
Имя файла.
mode
Режим открытия файла
enc
Кодировка.
errors
Режим реагирования на ошибки кодировки ('strict' - возбуждать исключение, 'replace' - заменять отсутствующие символы, 'ignore' - игнорировать ошибки).
buffer
Режим буферизации (0 - без буферизации, 1 - построчно, n - байт буфера).
Регулярные выражения
Рассмотренных стандартных возможностей для работы с текстом достаточно далеко не всегда. Например, в методах find() и replace() задается всего одна строка. В реальных задачах такая однозначность встречается довольно редко, чаще требуется найти или заменить строки, отвечающие некоторому шаблону.
Регулярные выражения (regular expressions) описывают множество строк, используя специальный язык, который сейчас и будет рассмотрен. (Строка, в которой задано регулярное выражение, будет называться шаблоном.)
Для работы с регулярными выражениями в Python используется модуль re. В следующем примере регулярное выражение помогает выделить из текста все числа:
>>> import re >>> pattern = r"[0-9]+" >>> number_re = re.compile(pattern) >>> number_re.findall("122 234 65435") ['122', '234', '65435']
В этом примере шаблон pattern описывает множество строк, которые состоят из одного или более символов из набора "0", "1" , ..., "9" . Функция re.compile() компилирует шаблон в специальный Regex-объект, который имеет несколько методов, в том числе метод findall() для получения списка всех непересекающихся вхождений строк, удовлетворяющих шаблону, в заданную строку.
То же самое можно было сделать и так:
>>> import re >>> re.findall(r"[0-9]+", "122 234 65435") ['122', '234', '65435']
Предварительная компиляция шаблона предпочтительнее при его частом использовании, особенно внутри цикла.
Примечание: Следует заметить, что для задания шаблона использована необработанная строка. В данном примере она не требовалась, но в общем случае лучше записывать строковые литералы именно так, чтобы исключить влияние специальных последовательностей, записываемых через обратную косую черту. |
Рекомендации по эффективности
При работе с очень длинными строками или большим количеством строк, применяемые операции могут по-разному влиять на быстродействие программы.
Например, не рекомендуется многократно использовать операцию конкатенации для склеивания большого количества строк в одну. Лучше накапливать строки в списке, а затем с помощью join() собирать в одну строку:
>>> a = "" >>> for i in xrange(1000): ... a += str(i) # неэффективно! ... >>> a = "".join([str(i) for i in xrange(1000)]) # более эффективно
Конечно, если строка затем обрабатывается, можно применять итераторы, которые позволят свести использование памяти к минимуму.
Синтаксис регулярного выражения
Синтаксис регулярных выражений в Python почти такой же, как в Perl, grep и некоторых других инструментах. Часть символов (в основном буквы и цифры) обозначают сами себя. Строка удовлетворяет (соответствует) шаблону, если она входит во множество строк, которые этот шаблон описывает.
Здесь стоит также отметить, что различные операции используют шаблон по-разному. Так, search() ищет первое вхождение строки, удовлетворяющей шаблону, в заданной строке, а match() требует, чтобы строка удовлетворяла шаблону с самого начала.
Символы, имеющие специальное значение в записи регулярных выражений:
"." | Любой символ |
"^" | Начало строки |
"$" | Конец строки |
"*" | Повторение фрагмента нуль или более раз (жадное) |
"+" | Повторение фрагмента один или более раз (жадное) |
"?" | Предыдущий фрагмент либо присутствует, либо отсутствует |
"{m,n}" | Повторение предыдущего фрагмента от m до n раз включительно (жадное) |
"[...]" | Любой символ из набора в скобках. Можно задавать диапазоны символов с идущими подряд кодами, например: a-z |
"[^...]" | Любой символ не из набора в скобках |
"\" | Обратная косая черта отменяет специальное значение следующего за ней символа |
"|" | Фрагмент справа или фрагмент слева |
"*?" | Повторение фрагмента нуль или более раз (не жадное) |
"+?" | Повторение фрагмента один или более раз (не жадное) |
"{m,n}?" | Повторение предыдущего фрагмента от m до n раз включительно (не жадное) |
Если A и B - регулярные выражения, то их конкатенация AB является новым регулярным выражением, причем конкатенация строк a и b будет удовлетворять AB, если a удовлетворяет A и b удовлетворяет B. Можно считать, что конкатенация - основной способ составления регулярных выражений.
Скобки, описанные ниже, применяются для задания приоритетов и выделения групп (фрагментов текста, которые потом можно получить по номеру или из словаря, и даже сослаться в том же регулярном выражении).
Алгоритм, который сопоставляет строки с регулярным выражением, проверяет соответствие того или иного фрагмента строки регулярному выражению. Например, строка "a" соответствует регулярному выражению "[a-z]", строка "fruit" соответствует "fruit|vegetable", а вот строка "apple" не соответствует шаблону "pineapple".
В таблице ниже вместо регвыр может быть записано регулярное выражение, вместо имя - идентификатор, а флаги будут рассмотрены ниже.
"(регвыр)" | Обособляет регулярное выражение в скобках и выделяет группу |
"(?:регвыр)" | Обособляет регулярное выражение в скобках без выделения группы |
"(?=регвыр)" | Взгляд вперед: строка должна соответствовать заданному регулярному выражению, но дальнейшее сопоставление с шаблоном начнется с того же места |
"(?!регвыр)" | То же, но с отрицанием соответствия |
"(?<=регвыр)" | Взгляд назад: строка должна соответствовать, если до этого момента соответствует регулярному выражению. Не занимает места в строке, к которой применяется шаблон. Параметр регвыр должен быть фиксированной длины (то есть, без "+" и "*") |
"(?<!регвыр)" | То же, но с отрицанием соответствия |
"(?P<имя>регвыр)" | Выделяет именованную группу с именем имя |
"(?P=имя)" | Точно соответствует выделенной ранее именованной группе с именем имя |
"(?#регвыр)" | Комментарий (игнорируется) |
"(?(имя)рв1|рв2)" | Если группа с номером или именем имя оказалась определена, результатом будет сопоставление с рв1, иначе - c рв2. Часть |рв2 может отсутствовать |
"(?флаг)" | Задает флаг для всего данного регулярного выражения. Флаги необходимо задавать в начале шаблона |
В таблице ниже описаны специальные последовательности, использующие обратную косую черту:
"\1" - "\9" | Группа с указанным номером. Группы нумеруются, начиная с 1 |
"\A" | Промежуток перед началом всей строки (почти аналогично "^") |
"\Z" | Промежуток перед концом всей строки (почти аналогично "$") |
"\b" | Промежуток между символами перед словом или после него |
"\B" | Наоборот, не соответствует промежутку между символами на границе слова |
"\d" | Цифра. Аналогично "[0-9]" |
"\s" | Любой пробельный символ. Аналогично "[\t\n\r\f\v]" |
"\S" | Любой непробельный символ. Аналогично "[^\t\n\r\f\v]" |
"\w" | Любая цифра или буква (зависит от флага LOCALE) |
"\W" | Любой символ, не являющийся цифрой или буквой (зависит от флага LOCALE) |
Флаги, используемые с регулярными выражениями:
"(?i)", re.I, re.IGNORECASE
Сопоставление проводится без учета регистра букв.
"(?L)", re.L, re.LOCALE
Влияет на определение буквы в "\w", "\W", "\b", "\B" в зависимости от текущей культурной среды (locale).
"(?m)", re.M, re.MULTILINE
Если этот флаг задан, "^" и "$" соответствуют началу и концу любой строки.
"(?s)", re.S, re.DOTALL
Если задан, "." соответствует также и символу конца строки "\n".
"(?x)", re.X, re.VERBOSE
Если задан, пробельные символы, не экранированные в шаблоне обратной косой чертой, являются незначащими, а все, что расположено после символа "#", -- комментарии. Позволяет записывать регулярное выражение в несколько строк для улучшения его читаемости и записи комментариев.
"(?u)", re.U, re.UNICODE
В шаблоне и в строке использован Unicode.
Строки
Строки в языке Python являются типом данных, специально предназначенным для обработки текстовой информации. Строка может содержать произвольно длинный текст (ограниченный имеющейся памятью).
В новых версиях Python имеются два типа строк: обычные строки (последовательность байтов) и Unicode-строки (последовательность символов). В Unicode-строке каждый символ может занимать в памяти 2 или 4 байта, в зависимости от настроек периода компиляции. Четырехбайтовые знаки используются в основном для восточных языков.
Примечание: В языке и стандартной библиотеке за некоторыми исключениями строки и Unicode-строки взаимозаменяемы, в собственных приложениях для совместимости с обоими видами строк следует избегать проверок на тип. Если это необходимо, можно проверять принадлежность базовому (для строк и Unicode-строк) типу с помощью isinstance(s, basestring). |
При использовании Unicode-строк, следует мысленно принять точку зрения, относительно которой именно Unicode-представление является главным, а все остальные кодировки - лишь частные случаи представления текста, которые не могут передать всех символов. Без такой установки будет непонятно, почему преобразование из восьмибитной кодировки называется decode (декодирование). Для внешнего представления можно с успехом использовать кодировку UTF-8, хотя, конечно, это зависит от решаемых задач.
Строковые литералы
Строки можно задать в программе с помощью строковых литералов. Литералы записываются с использованием апострофов ', кавычек " или этих же символов, взятых трижды. Внутри литералов обратная косая черта имеет специальное значение. Она служит для ввода специальных символов и для указания символов через коды. Если перед строковым литералом поставлено r, обратная косая черта не имеет специального значения (r от английского слова raw, строка задается "как есть"). Unicode-литералы задаются с префиксом u. Вот несколько примеров:
s1 = "строка 1" s2 = r'\1\2' s3 = """apple\ntree""" # \n - символ перевода строки s4 = """apple tree""" # строка в утроенных кавычках может иметь внутри переводы строк s5 = '\x73\65' u1 = u"Unicode literal" u2 = u'\u0410\u0434\u0440\u0435\u0441'
Примечание: Обратная косая черта не должна быть последним символом в литерале, то есть, "str\" вызовет синтаксическую ошибку. |
Указание кодировки позволяет применять в Unicode-литералах указанную в начале программы кодировку. Если кодировка не указана, можно пользоваться только кодами символов, заданными через обратную косую черту.
В этой лекции были рассмотрены
В этой лекции были рассмотрены основные типы для манипулирования текстом: строки и Unicode-строки. Достаточно подробно описаны регулярные выражения - один из наиболее эффективных механизмов для анализа текста. В конце приведены некоторые функции для работы с Unicode.