Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
417 result = price * ((100 percentage) / 100) if not (0 < result <= price): raise ValueError("invalid price") return result if not make_integer else int(round(result)) Если интерпретатор выполняет программу в отладочном режиме (обычный режим), то при каждом вызове функции discounted_price() в файл logged.log, находящийся во временном каталоге, будет запи сываться сообщение, как показано в следующей выдержке из этого файла: called: discounted_price(100, 10) > 90.0 called: discounted_price(210, 5) > 199.5 called: discounted_price(210, 5, make_integer=True) > 200 called: discounted_price(210, 14, True) > 181 called: discounted_price(210, 8) Если интерпретатор выполняет программу в оптимизированном режи ме (используется ключ командной строки – O или переменная окруже ния PYTHONOPTIMIZE содержит значение – O 1 ), то регистрация отключает ся. Ниже приводится программный код, выполняющий настройку ме ханизма регистрации и определение самого декоратора: if __debug__: logger = logging.getLogger("Logger") logger.setLevel(logging.DEBUG) handler = logging.FileHandler(os.path.join( tempfile.gettempdir(), "logged.log")) logger.addHandler(handler) def logged(function): @functools.wraps(function) def wrapper(*args, **kwargs): log = "called: " + function.__name__ + "(" log += ", ".join(["{0!r}".format(a) for a in args] + ["{0!s}={1!r}".format(k, v) for k, v in kwargs.items()]) result = exception = None try: result = function(*args, **kwargs) return result except Exception as err: exception = err finally: log += ((") > " + str(result)) if exception is None else ") {0}: {1}".format(type(exception), exception)) logger.debug(log) if exception is not None: raise exception 1 В обоих случаях это буква O, а не цифра 0. – Прим. перев. 418 Глава 8. Усовершенствованные приемы программирования return wrapper else: def logged(function): return function При работе в отладочном режиме переменная __debug__ имеет значение True . В этом случае выполняется настройка механизма регистрации с использованием модуля logging, и затем создается декоратор @logged. Модуль logging обладает очень широкими возможностями – он может записывать сообщения в файлы, выполнять ротацию файлов, отправ лять сообщения по электронной почте, через сетевые соединения, сер верам HTTP и многое другое. Здесь задействованы только самые ос новные средства – создается объектрегистратор, устанавливается уро вень регистрации (поддерживается несколько уровней) и в качестве устройства вывода выбирается файл. Функцияобертка начинает с того, что создает текст со общения, включив в него имя функции и значения аргу ментов. После этого предпринимается попытка вызвать функцию и сохранить результат. Если возникло какое либо исключение, оно сохраняется. В любом случае вы полняется блок finally, и здесь в текст сообщения для регистрации добавляется результат (или исключение), после чего производится вывод сообщения в устройство регистрации. Если никаких исключений не возникло, результат возвращается вызывающей программе; в про тивном случае повторно возбуждается исключение, ими тируя поведение оригинальной функции. При работе в оптимизированном режиме переменная __debug__ прини мает значение False. В этом случае используется определение функции logged() , которая просто возвращает указанную ей функцию, поэтому, кроме некоторой крошечной задержки, обусловленной созданием функции, во время выполнения никакого снижения производительно сти не наблюдается. Обратите внимание, что в стандартной библиотеке присутствуют моду ли trace и profile, которые могут запускать и анализировать ход вы полнения программ и модулей, а также воспроизводить различные от четы трассировки и профилирования. Оба они используют механизмы интроспекции, поэтому, в отличие от декоратора @logged, ни модуль trace , ни модуль profile не требуют вносить изменения в исходные тексты. Аннотации функций Функции и методы могут определяться с помощью аннотаций – выра жений, которые могут использоваться в сигнатурах функций. Ниже приводится общий синтаксис: Генераторы словарей, стр. 160 Улучшенные приемы процедурного программирования 419 def functionName(par1 : exp1, par2 : exp2, ..., parN : expN) > rexp: suite Каждое выражение, следующее за двоеточием (: expX ), является необя зательным, как и выражение возвращаемого значения, следующее за стрелкой ( – > rexp ). Последний (или единственный) позиционный пара метр (если таковой имеется) может иметь форму *args, с аннотацией или без. Точно так же последний (или единственный) именованный параметр (если таковой имеется), может иметь форму **kwargs, и тоже с аннотацией или без. Если аннотации присутствуют в заголовке функции, они добавляются в словарь __annotations__ этой функции. Если аннотации отсутствуют, словарь остается пустым. Ключами словаря служат имена парамет ров, а значениями – соответствующие им выражения. Синтаксис до пускает возможность аннотировать все параметры, некоторые из них или ни одного и, кроме того, аннотировать или не аннотировать воз вращаемое значение. Аннотации не имеют специального значения для интерпретатора. Единственное, что интерпретатор делает, когда встречает аннотации, – помещает их в словарь __annotations__, остав ляя за нами любые действия с ними. Ниже приводится пример анно тированной функции из модуля Util: def is_unicode_punctuation(s : str) > bool: for c in s: if unicodedata.category(c)[0] != "P": return False return True Каждый символ Юникода принадлежит какойто конкретной катего рии, а каждая категория идентифицируется идентификатором из двух символов. Все категории, имена которых начинаются с символа P, со держат знаки пунктуации. В данном примере в качестве выражений аннотации мы использовали имена типов данных языка Python. Но они не имеют никакого значе ния для интерпретатора, что наглядно показывают следующие вызо вы функции: Util.is_unicode_punctuation("zebr\a") # вернет: False Util.is_unicode_punctuation(s="!@#?") # вернет: True Util.is_unicode_punctuation(("!", "@")) # вернет: True В первом вызове используется позиционный аргумент, а во втором – именованный, просто для демонстрации, что оба варианта работают так, как и ожидается. В последнем вызове вместо строки передается кортеж, и это вполне допустимо, потому что интерпретатор никак не учитывает аннотации, кроме как записывает их в словарь __annotati ons__ Если мы хотим извлечь толк из аннотаций, чтобы, например, выпол нить проверку типов, можно предусмотреть декорирование требуемой 420 Глава 8. Усовершенствованные приемы программирования функции соответствующим декоратором. Ниже приводится очень про стой декоратор, выполняющий проверку типов: def strictly_typed(function): annotations = function.__annotations__ arg_spec = inspect.getfullargspec(function) assert "return" in annotations, "missing type for return value" for arg in arg_spec.args + arg_spec.kwonlyargs: assert arg in annotations, ("missing type for parameter '" + arg + "'") @functools.wraps(function) def wrapper(*args, **kwargs): for name, arg in (list(zip(arg_spec.args, args)) + list(kwargs.items())): assert isinstance(arg, annotations[name]), ( "expected argument '{0}' of {1} got {2}".format( name, annotations[name], type(arg))) result = function(*args, **kwargs) assert isinstance(result, annotations["return"]), ( "expected return of {0} got {1}".format( annotations["return"], type(result))) return result return wrapper Данный декоратор требует, чтобы все аргументы и возвращаемое зна чение были аннотированы соответствующими типами данных. Он про веряет наличие в указанной функции аннотаций с типами для всех ар гументов и возвращаемого значения и во время выполнения проверяет соответствие фактических аргументов ожидаемым типам данных. Модуль inspect содержит мощные средства интроспекции для объек тов. Здесь мы использовали лишь малую часть спецификаций аргу ментов, возвращаемых модулем, извлекая имена всех позиционных и именованных аргументов – в правильном порядке следования в слу чае позиционных аргументов. Затем эти имена и словарь с аннотация ми используются для проверки наличия аннотации у каждого пара метра и возвращаемого значения. Функцияобертка, созданная внутри декоратора, сначала выполняет итерации по всем парам имяаргумент для всех позиционных и имено ванных аргументов. Так как функция zip() возвращает итератор, а ме тод dict.items() возвращает представление словаря, мы не можем объ единить их непосредственно, поэтому сначала каждый из них преобра зуется в список. Если тип фактического аргумента отличается от типа, указанного в аннотации, инструкция assert терпит неудачу; в против ном случае вызывается фактическая функция, после чего выполняет ся проверка типа возвращаемого значения, и если это значение имеет требуемый тип, оно возвращается вызывающей программе. В конце функция strictly_typed() как обычно возвращает функциюобертку. Улучшенные приемы объектно/ориентированного программирования 421 Обратите внимание, что проверка выполняется только в отладочном режиме (который является режимом выполнения по умолчанию и за дается ключом командной строки – O или переменной окружения PY THONOPTIMIZE ). Если функцию is_unicode_punctuation() декорировать декоратором @strictly_typed и попытаться выполнить те же вызовы, что и прежде, но уже для декорированной версии, то аннотации вступят в силу, как показано ниже: is_unicode_punctuation("zebr\a") # вернет: False is_unicode_punctuation(s="!@#?") # вернет: True is_unicode_punctuation(("!", "@")) # возбудит исключение AssertionError Теперь проверка типов аргументов выполняется, поэтому в последнем случае возбуждается исключение AssertionError, так как кортеж не яв ляется строкой или подклассом класса str. Теперь рассмотрим совершенно иное применение аннотаций. Ниже приводится маленькая функция, которая повторяет функциональ ность встроенной функции range(), за исключением того, что она все гда возвращает число с плавающей точкой. def range_of_floats(*args) > "author=Reginald Perrin": return (float(x) for x in range(*args)) Сама функция никак не использует аннотацию, но совсем несложно создать инструмент, который будет импортировать все модули проек та и выводить список функций с именами авторов, извлекая имена функций из атрибута __name__, а имена авторов – из элемента словаря __annotations__ с ключом "return". Аннотации – это совершенно новая особенность языка Python, и язы ком не предусматривается какогото предопределенного назначения для них, поэтому область применения аннотаций ограничивается только воображением программиста. С идеями, касающимися воз можного использования аннотаций, можно ознакомиться в предложе нии по расширению PEP 3107 «Function Annotations» по адресу: www.python.org/dev/peps/pep3107 ; там же можно найти несколько по лезных ссылок. Улучшенные приемы объектноориентированного программирования В этом разделе мы более подробно рассмотрим поддержку объектно ориентированного программирования в языке Python. Познакомимся со множеством приемов, которые помогают уменьшить объем про граммного кода, а также с приемами, расширяющими существующие возможности программирования. Но сначала мы рассмотрим одну но вую, очень маленькую и очень простую особенность. Ниже приводится 422 Глава 8. Усовершенствованные приемы программирования начало определения класса Point, обладающего теми же возможностя ми, что и версия этого класса из главы 6: class Point: __slots__ = ("x", "y") def __init__(self, x=0, y=0): self.x = x self.y = y Когда класс создается без использования частной пере менной __slots__, для каждого экземпляра класса интер претатор создает словарь с именем __dict__, и этот сло варь используется для хранения атрибутов данных эк земпляра, поэтому имеется возможность добавлять и уда лять атрибуты объектов. (Например, благодаря этой особенности мы смогли добавить атрибут cache к функ ции get_function() ранее в этой главе.) Если нам требуется, чтобы объект просто лишь обеспечивал доступ к своим атрибутам и не позволял добавлять или удалять атрибуты, можно создать класс, экземпляры которого не будут иметь словарь __dict__ . Этого легко добиться, просто определив атрибут класса с име нем __slots__, значением которого является кортеж с именами атрибу тов. Каждый объект такого класса будет иметь атрибуты с указанны ми именами, и в них будет отсутствовать словарь __dict__. Объекты та ких классов не допускают возможность добавления или удаления ат рибутов. По сравнению с обычными объектами такие объекты занимают меньший объем памяти, хотя это вряд ли имеет большое значение в программах, которые не создают большого числа объектов. Управление доступом к атрибутам Иногда бывает удобно иметь в классе такие атрибуты, значения кото рых не хранятся в памяти, а вычисляются в момент обращения к ним. Ниже приводится полная реализация такого класса: class Ord: def __getattr__(self, char): return ord(char) Имея класс Ord, можно создать экземпляр этого класса ord = Ord() и получить альтернативу встроенной функции ord(), работающей с любыми символами, допустимыми для использования в идентифи каторах. Например, обращение к атрибуту ord.a вернет число 97, ord.Z вернет 90, а ord.е вернет 229. (Но обращение к атрибуту ord.! и подоб ным ему будет вызывать синтаксическую ошибку.) Обратите внимание, что если ввести определение класса Ord в среде IDLE, он не будет работать, если выполнить выражение ord = Ord() . Это Возможность доступа к атрибутам функций, стр. 406 Улучшенные приемы объектно/ориентированного программирования 423 обусловлено тем, что экземпляр класса имеет то же имя, что и встроенная функция ord(), которая используется методом класса Ord . В этом случае вызов ord() будет интерпретироваться как попытка вызова экземпляра ord, что будет вызывать исключение TypeError. Эта проблема не проявляется при импортировании модуля с определением класса Ord, потому что в этом случае экземпляр ord, создаваемый в ин терактивной оболочке, и функция ord(), используемая классом Ord, будут находиться в разных модулях, и потому не произойдет замеще ния одного другим. Если же действительно необходимо создать этот класс в интерактивной оболочке и использовать в нем встроенную функцию, это можно реализовать, вынудив класс вызывать именно встроенную функцию – в данном случае, импортировав модуль buil tins , обеспечивающий однозначный доступ ко всем встроенным функ циям, и вызывая встроенную функцию как builtins.ord(), а не просто ord() Ниже приводится другой пример небольшого, но законченного клас са. Он позволяет создавать «константы». Даже при использовании это го класса совсем несложно изменить значение такой «константы», но он хотя бы предотвращает самые простые ошибки: class Const: def __setattr__(self, name, value): if name in self.__dict__: raise ValueError("cannot change a const attribute") self.__dict__[name] = value def __delattr__(self, name): if name in self.__dict__: raise ValueError("cannot delete a const attribute") raise AttributeError("'{0}' object has no attribute '{1}'" .format(self.__class__.__name__, name)) С помощью этого класса можно создавать объекты констант, скажем, так: const=Const(), и устанавливать любые их атрибуты, какие только потребуется, например, const.limit = 591 . Но, как только значение ат рибута будет установлено, оно будет доступно только для чтения – лю бые попытки изменить или удалить атрибут будут возбуждать исклю чение ValueError. Мы не стали переопределять метод __getattr__(), по тому что метод object.__getattr__() базового класса реализует все, что нам необходимо, – возвращает значение требуемого атрибута или воз буждает исключение AttributeError, если указанный атрибут отсутст вует. В методе __delattr__() имитируется сообщение об ошибке, кото рое выводится методом __getattr__() при попытке обратиться к несу ществующему атрибуту, для чего нам потребовалось получить имя класса и имя несуществующего атрибута. Работа класса основана на использовании атрибута __dict__ объекта, который также использует ся следующими методами базового класса: __getattr__(), __setattr__() и __delattr__(); в данном случае мы используем только метод __get 424 Глава 8. Усовершенствованные приемы программирования attr__() базового класса. Все специальные методы, используемые для доступа к атрибутам, перечислены в табл. 8.2. Таблица 8.2. Специальные методы доступа к атрибутам Существует еще один способ реализации констант – с использованием именованных кортежей. Ниже приводится пара примеров: Const = collections.namedtuple("_", "min max")(191, 591) Const.min, Const.max # вернет: (191, 591) Offset = collections.namedtuple("_", "id name description")(*range(3)) Offset.id, Offset.name, Offset.description # вернет: (0, 1, 2) В обоих случаях мы использовали ничего не значащее имя для имено ванного кортежа, потому что каждый раз нам необходим лишь один экземпляр кортежа, а не подкласс, который мог бы использоваться для создания нескольких экземпляров именованных кортежей. Язык Python не поддерживает такой тип данных, как перечисления, тем не менее мы можем использовать именованные кортежи для достижения того же эффекта. Заканчивая рассмотрение специальных методов доступа к атрибутам, вернемся к примеру, который первый раз был продемонстрирован в главе 6. В этой главе мы созда ли класс Image, который имел фиксированные ширину, высоту и цвет фона, задававшиеся в момент создания эк земпляра Image (и которые могли изменяться при загруз ке изображения из файла). Мы обеспечили доступ к этим атрибутам с помощью свойств, доступных только для чтения. Например: @property def width(self): return self.__width Такой способ отличается простотой, но он может оказаться утомитель ным, если потребуется реализовать достаточно много свойств, доступ ных только для чтения. Ниже приводится другое решение, которое за |