Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
450 Глава 8. Усовершенствованные приемы программирования self.filename = filename self.__attribute_names = [] for name in attribute_names: if name.startswith("__"): name = "_" + self.__class__.__name__ + name self.__attribute_names.append(name) def save(self): with open(self.filename, "wb") as fh: data = [] for name in self.__attribute_names: data.append(getattr(self, name)) pickle.dump(data, fh, pickle.HIGHEST_PROTOCOL) def load(self): with open(self.filename, "rb") as fh: data = pickle.load(fh) for name, value in zip(self.__attribute_names, data): setattr(self, name, value) Класс имеет два атрибута: filename – который является общедоступ ным и может изменяться в любой момент, и __attribute_names – кото рый доступен только для чтения и может устанавливаться только в момент создания экземпляра. Метод save() выполняет итерации по всем именам атрибутов и создает список с именем data, в котором запо минаются значения всех сохраняемых атрибутов, после чего данные записываются в файл средствами модуля pickle. Инструкция with га рантирует, что открытый файл будет закрыт и любое возникшее ис ключение будет передано вверх по стеку вызовов. Метод load() выпол няет итерации по именам атрибутов и соответствующим им элементам данных, и в каждый из атрибутов записывается его значение, загру женное из файла. Ниже приводится начало определения класса FileStack, наследующего класс Undo из предыдущего подраздела и класс LoadSave из этого подраз дела: class FileStack(Undo, LoadSave): def __init__(self, filename): Undo.__init__(self) LoadSave.__init__(self, filename, "__stack") self.__stack = [] def load(self): super().load() self.clear() Остальная часть класса совпадает с определением класса Stack, поэто му мы не стали воспроизводить ее здесь. Мы используем метод __init__() , в котором задаем инициализируемые базовые классы, вме сто использования функции super(), которая не способна предпола гать, метод какого из базовых классов следует вызывать. Методу ини Улучшенные приемы объектно/ориентированного программирования 451 циализации класса LoadSave передаются имя файла и имена сохраняе мых атрибутов – в данном случае это единственный атрибут, частный атрибут __stack. (Мы не предполагаем (и не могли бы) сохранять значе ние атрибута __undos, потому что его значением является список мето дов, которые невозможно сохранить в файле.) Класс FileStack содержит все необходимые методы отмены, а класс LoadSave – методы save() и load(). Мы не переопределяем метод save(), поскольку его реализация в базовом классе вполне отвечает нашим требованиям, но в методе load() сразу после загрузки нам требуется до полнительно очистить список отмен. Это необходимо, потому что по сле сохранения стека в файле мы могли выполнить некоторые опера ции над ним, а затем загрузить сохраненные ранее данные. Операция загрузки затирает данные, которые раньше находились в стеке, поэто му наличие какихлибо методов отмены в списке теряет всякий смысл. Оригинальный класс Undo не имеет метода clear(), поэтому мы добавили свой: def clear(self): # В классе Undo self.__undos = [] В методе Stack.load() мы использовали функцию super() для вызова унаследованного метода LoadSave.load(), потому что в классе Undo от сутствует метод load(), который мог бы быть причиной неоднозначно сти. Если бы в обоих базовых классах имелся метод load(), то выбор вызываемого метода зависел бы от того, в каком порядке интерпрета тор осуществляет поиск методов в базовых классах. Предпочтительно использовать функцию super() только при отсутствии неоднозначно сти, а в противном случае прямо указывать имя базового класса, что бы не зависеть от того, в каком порядке интерпретатор просматривает базовые классы при поиске методов. В случае с вызовом self.clear() тоже нет никакой неоднозначности, потому что метод clear() имеется только в классе Undo, при этом нам не требуется использовать функцию super() , потому что в классе FileStack (в отличие от метода load()) от сутствует собственный метод clear(). Что произойдет, если позднее в класс FileStack будет добавлен метод clear() ? Это может нарушить работу метода load(). Одно из решений этой проблемы состоит в том, чтобы внутри метода load() вместо self.clear() производить вызов метода как super().clear(). Но это в свою очередь может привести к тому, что будет вызван первый метод clear() , найденный в базовых классах. Чтобы защитить себя от этих проблем, при использовании множественного наследования можно выбрать тактику прямого обращения к базовым классам (в данном примере мы могли бы прямо вызывать метод Undo.clear()). Или можно вообще отказаться от использования множественного наследования и применить прием агрегирования, например, наследовать класс Undo и определить класс LoadSave так, чтобы он мог использоваться для оп ределения атрибутов. 452 Глава 8. Усовершенствованные приемы программирования В этом примере множественное наследование позволило нам получить смесь двух очень разных классов и избежать необходимости самим реализовывать отмену изменений или сохранение и загрузку данных, вместо этого опираясь исключительно на возможности базовых клас сов. Это может быть очень удобно и оправданно, особенно, если насле дуемые классы не реализуют перекрывающиеся API. Метаклассы Метакласс – это класс, экземплярами которого являются другие клас сы, то есть метаклассы используются для создания классов так же, как классы используются для создания объектов. И так же, как имеет ся возможность определить, какому классу принадлежит объект, ис пользуя функцию instance(), имеется возможность определить, насле дует ли объект класса (такой как dict, int или SortedList) другой класс, используя для этого функцию issubclass(). Самый простой способ использования метаклассов заключается в том, чтобы поместить собственный класс в стандартную иерархию абст рактных базовых классов Python. Например, чтобы сделать класс SortedList наследником collections.Sequence, вместо наследования аб страктного базового класса можно просто зарегистрировать класс SortedList , как collections.Sequence: class SortedList: collections.Sequence.register(SortedList) После того как класс будет определен обычным способом, его можно зарегистрировать как подкласс абстрактного базового класса collec tions.Sequence . Операция регистрации, как показано выше, превраща ет класс в виртуальный подкласс. 1 Виртуальный подкласс сообщает (например, с помощью функций isinstance() или issubclass()), что он является подклассом класса или классов и был зарегистрирован с их помощью, но не наследует никаких данных или методов любого из этих классов. Регистрация класса – это своего рода обещание, что класс реализует API классов, с помощью которых он был зарегистрирован, но при этом нет никаких гарантий, что обещания будут выполнены. Одно из пред назначений метаклассов состоит в том, чтобы обеспечить возможность дать обещания и гарантии их соблюдения относительно API класса. Другое предназначение состоит в том, чтобы обеспечить возможность модификации класса (подобно декораторам классов). И, конечно, ме таклассы могут использоваться для достижения обеих указанных це лей одновременно. 1 В терминологии языка Python слово виртуальный означает не совсем то, что оно означает в терминологии языка C++. Улучшенные приемы объектно/ориентированного программирования 453 Предположим, что нам требуется создать группу классов, реализую щих методы load() и save(). Сделать это можно, создав класс, а затем используя его как метакласс для проверки наличия этих методов: class LoadableSaveable(type): def __init__(cls, classname, bases, dictionary): super().__init__(classname, bases, dictionary) assert hasattr(cls, "load") and \ isinstance(getattr(cls, "load"), collections.Callable), ("class '" + classname + "' must provide a load() method") assert hasattr(cls, "save") and \ isinstance(getattr(cls, "save"), collections.Callable), ("class '" + classname + "' must provide a save() method") Классы, играющие роль метаклассов, должны наследовать общий ба зовый класс type или один из его подклассов. Обратите внимание, что этот класс вызывается, когда создаются опре деления классов, использующие его, что происходит достаточно ред ко, поэтому затраты на метаклассы во время выполнения чрезвычайно низки. Обратите также внимание на то, что проверки должны выпол няться после создания класса (вызов функции super()), поскольку только после этого атрибуты класса будут доступны. (Атрибуты нахо дятся в словаре, но при выполнении проверок мы предпочитаем рабо тать с фактически инициализированным классом.) Можно было бы проверить, являются ли атрибуты load и save вызываемыми, используя функцию hasattr() для проверки наличия атрибута __call__, но вместо этого мы предпочли проверить, являются ли они экземплярами collections.Callable . Абстрактный базовый класс collec tions.Callable обещает (но не гарантирует), что экземпля ры его подклассов (или виртуальных подклассов) смогут вызываться. После создания класса (вызовом type.__new__() или переопределенным методом __new__()) выполняется инициализация метакласса вызовом метода __init__(). Методу __init__() передаются: в аргументе cls – толь ко что созданный класс; в аргументе classname – имя класса (доступно также в виде атрибута cls.__name__); в аргументе bases – список базовых классов (кроме класса object, вследствие чего список может быть пус тым); в аргументе dictionary – словарь с атрибутами, которые стали ат рибутами класса после создания класса cls, при условии, что мы не вмешивались в переопределение метода __new__() метакласса. Ниже приводится пара примеров, выполненных в интерактивной обо лочке, которые демонстрируют, что происходит при создании новых классов, использующих метакласс LoadableSaveable: Абстракт ные базо вые классы модуля, стр. 443 454 Глава 8. Усовершенствованные приемы программирования >>> class Bad(metaclass=Meta.LoadableSaveable): ... def some_method(self): pass Traceback (most recent call last): AssertionError: class 'Bad' must provide a load() method (AssertionError: класс 'Bad' должен иметь реализацию метода load()) Метакласс требует, чтобы класс, использующий его, реализовал опре деленные методы; в противном случае, как в данном примере, возбуж дается исключение AssertionError. >>> class Good(metaclass=Meta.LoadableSaveable): ... def load(self): pass ... def save(self): pass >>> g = Good() Класс Good соблюдает требования к API, предъявляемые метаклассом, несмотря на то, что реализация не соответствует нашим представлени ям о том, каким поведением она должна обладать. Метаклассы могут также применяться для изменения классов, ис пользующих их. Если изменяется имя, список базовых классов или словарь создаваемого класса (например, его слоты), то необходимо бу дет переопределить метод __new__() метакласса; но в случае других из менений, например, при добавлении новых методов или атрибутов данных, достаточно будет переопределить метод __init__(), хотя все необходимые действия можно было бы реализовать и в методе __new__() . Теперь перейдем к рассмотрению метакласса, который мо дифицирует классы, использующие его исключительно посредством метода __new__(). Вместо использования декораторов @property и @name.setter мы могли бы создать классы, применяющие простые соглашения об именах, ис пользуемых для идентификации свойств. Например, если класс имеет методы get_name() и set_name(), в соответствии с соглашениями можно было бы ожидать, что класс имеет частное свойство __name, доступное как instance.name. Реализовать это можно с помощью метакласса. Ни же приводится пример класса, в котором используется данное согла шение: class Product(metaclass=AutoSlotProperties): def __init__(self, barcode, description): self.__barcode = barcode self.description = description def get_barcode(self): return self.__barcode def get_description(self): return self.__description def set_description(self, description): Улучшенные приемы объектно/ориентированного программирования 455 if description is None or len(description) < 3: self.__description = " else: self.__description = description Мы вынуждены выполнить присваивание частному атрибуту __barcode в методе инициализации, поскольку для него отсутствует метод запи си; другое следствие этого – то, что свойство barcode доступно только для чтения. С другой стороны, свойство description доступно для чте ния и для записи. Ниже приводятся несколько примеров использова ния этого класса в интерактивной оболочке: >>> product = Product("101110110", "8mm Stapler") >>> product.barcode, product.description ('101110110', '8mm Stapler') >>> product.description = "8mm Stapler (long)" >>> product.barcode, product.description ('101110110', '8mm Stapler (long)') Если попытаться присвоить новое значение свойству barcode, будет возбуждено исключение AttributeError с текстом сообщения «can’t set attribute» (невозможно установить значение атрибута). Если попытаться получить перечень атрибутов класса Product (напри мер, с помощью функции dir()), будут обнаружены только общедос тупные свойства barcode и description. Методы get_name() и set_name() не попадут в этот список – их заменит свойство name. А переменные, хранящие штрихкод и описание (__barcode и __description), будут до бавлены как слоты, чтобы минимизировать объем памяти, используе мой экземплярами класса. Все это реализуется средствами метакласса AutoSlotProperties , в котором имеется единственный метод: class AutoSlotProperties(type): def __new__(mcl, classname, bases, dictionary): slots = list(dictionary.get("__slots__", [])) for getter_name in [key for key in dictionary if key.startswith("get_")]: if isinstance(dictionary[getter_name], collections.Callable): name = getter_name[4:] slots.append("__" + name) getter = dictionary.pop(getter_name) setter_name = "set_" + name setter = dictionary.get(setter_name, None) if (setter is not None and isinstance(setter, collections.Callable)): del dictionary[setter_name] dictionary[name] = property(getter, setter) dictionary["__slots__"] = tuple(slots) return super().__new__(mcl, classname, bases, dictionary) 456 Глава 8. Усовершенствованные приемы программирования При вызове методу __new__() метакласса передаются имена метакласса и класса, список базовых классов и словарь класса, который должен быть создан. Поскольку перед созданием класса нам необходимо изме нить словарь, следует переопределить не метод __init__(), а метод __new__() Реализация метода начинается с копирования коллекции __slots__, с созданием пустой коллекции, если коллекция __slots__ отсутствова ла. Попутно кортеж преобразуется в список, чтобы впоследствии име лась возможность изменять его. Из всех атрибутов, находящихся в словаре, мы выбираем те, что начинаются с префикса "get_" и явля ются вызываемыми, то есть те, которые представляют методы чтения. Для каждого метода чтения в список slots добавляется частное имя ат рибута, который будет хранить соответствующие данные, например, при наличии метода get_name() в список slots добавляется имя __name. После этого из словаря извлекается и удаляется ссылка на метод чте ния по его оригинальному имени (обе эти операции выполняются за один раз, с помощью вызова метода dict.pop()). То же самое выполня ется для метода записи, если таковой присутствует, и затем создается новый элемент словаря с соответствующим именем свойства в качест ве ключа, например, для метода чтения с именем get_name() свойство получит имя name. Значением элемента будет свойство с методами чте ния и записи (который может отсутствовать), которые были найдены и удалены из словаря. В конце оригинальный кортеж __slots__ замещается модифицирован ным списком, в который были включены частные имена для каждого добавленного свойства, и вызывается метод базового класса, чтобы создать действительный класс, но уже с использованием модифициро ванного словаря. Обратите внимание, что в данном случае мы должны явно передать метакласс методу базового класса – это необходимо де лать всегда, когда вызывается метод __new__(), потому что это метод класса, а не метод экземпляра. В этом примере нам не потребовалось переопределять метод __init__(), потому что все необходимое было реализовано в методе __new__(), одна ко вполне возможно переопределить оба метода: __new__() и __init__() и в каждом из них выполнить свою часть работы. Если использование механизма наследования и приема агрегирования сравнить с ручной дрелью, а использование декораторов и дескрипто ров – с электрической дрелью, то использование метаклассов можно сравнить с лазерным лучом, дающим непревзойденную мощность и гибкость. Метаклассы не являются инструментом первой необходи мости, исключая, разве что, разработчиков прикладных платформ, ко торым необходимо предоставить своим пользователям мощные средст ва, не заставляя их проходить многочисленные этапы, чтобы оценить предлагаемые преимущества. Функциональное программирование 457 Функциональное программирование Функциональный стиль программирования – это подход к программи рованию, когда вычисления программируются путем комбинирова ния функций, которые не изменяют свои аргументы, не обращаются к переменным, определяющим состояние программы, и не изменяют их, а результаты своей работы поставляют в виде возвращаемых зна чений. Основное преимущество этого подхода к программированию состоит в том, что при его использовании (теоретически) намного про ще разрабатывать функции по отдельности и проще отлаживать функ циональные программы. Здесь также положительно сказывается тот факт, что функциональные программы не изменяют свое состояние, поэтому вполне возможно рассуждать об их функциях с математиче ской точки зрения. С функциональным программированием тесно связаны три понятия: отображение , фильтрация и упрощение. Отображение предполагает совместное использование функции и итерируемого объекта и получе ние нового итерируемого объекта (или списка), каждый элемент кото рого представляет результат вызова функции для соответствующего элемента в оригинальном итерируемом объекте. Понятие отображе ния поддерживается встроенной функцией map(), например: list(map(lambda x: x ** 2, [1, 2, 3, 4])) # вернет: [1, 4, 9, 16] Функция map() принимает в виде аргументов функцию и итерируемый объект и для большей эффективности возвращает итератор, а не спи сок. В данном примере мы принудительно преобразовали итерируе мый объект в список, чтобы результат выглядел более понятно. [x ** 2 for x in [1, 2, 3, 4]] # вернет: [1, 4, 9, 16] Часто вместо функции map() можно использовать выражениягенера торы. Здесь был использован генератор списков, чтобы избежать необ ходимости применять функцию list(), а чтобы получить генератор списков, оказалось достаточно заменить внешние круглые скобки квадратными. Фильтрация предполагает совместное использование функции и ите рируемого объекта и получение нового итерируемого объекта, в состав которого включаются все те элементы оригинального итерируемого объекта, для которых функция вернула значение True. Это понятие поддерживается встроенной функцией filter(): list(filter(lambda x: x > 0, [1, 2, 3, 4])) # вернет: [1, 3] Функция filter() принимает в виде аргументов функцию и итерируе мый объект и возвращает итератор. [x for x in [1, 2, 3, 4] if x > 0] # вернет: [1, 3] |