Как стать автором
Обновить

Книга «Python. Лучшие практики и инструменты»

Издательский дом «Питер»PythonПрофессиональная литература
image Привет, Хаброжители! Python — это динамический язык программирования, используемый в самых разных предметных областях. Хотя писать код на Python просто, гораздо сложнее сделать этот код удобочитаемым, пригодным для многократного использования и легким в поддержке. Третье издание «Python. Лучшие практики и инструменты» даст вам инструменты для эффективного решения любой задачи разработки и сопровождения софта. Авторы начинают с рассказа о новых возможностях Python 3.7 и продвинутых аспектах синтаксиса Python. Продолжают советами по реализации популярных парадигм, в том числе объектно-ориентированного, функционального и событийно-ориентированного программирования. Также авторы рассказывают о наилучших практиках именования, о том, какими способами можно автоматизировать развертывание программ на удаленных серверах. Вы узнаете, как создавать полезные расширения для Python на C, C++, Cython и CFFI.

Для кого эта книга
Книга написана для разработчиков на Python, желающих продвинуться в освоении этого языка. Под разработчиками мы имеем в виду в основном программистов, которые зарабатывают на жизнь программированием на Python. Дело в том, что книга сосредоточена на средствах и методах, наиболее важных для создания производительного, надежного и удобного в сопровождении программного обеспечения на Python.

Это не значит, что в книге нет ничего интересного для любителей. Она отлично подойдет для тех, кто хочет выйти на новый уровень в изучении Python. Базовых навыков языка будет достаточно, чтобы понять изложенный материал, хотя менее опытным программистам придется приложить некоторые усилия. Книга также будет хорошим введением в Python 3.7 для тех, кто слегка отстал от жизни и пользуется версией Python 2.7 или еще более ранней.

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

Паттерны доступа к расширенным атрибутам


Изучая Python, многие программисты C++ и Java удивляются отсутствию ключевого слова private. Наиболее близкая к нему концепция — это искажение (декорирование) имени (name mangling). Каждый раз, когда атрибут получает префикс __, он динамически переименовывается интерпретатором:

class MyClass:
__secret_value = 1

Доступ к атрибуту __secret_value по его изначальному имени приведет к выбрасыванию исключения AttributeError:

>>> instance_of = MyClass()
>>> instance_of.__secret_value
Traceback (most recent call last):
   File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute '__secret_value'
>>> dir(MyClass)
['_MyClass__secret_value', '__class__', '__delattr__', '__dict__',
'__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
'__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> instance_of._MyClass__secret_value
1

Это сделано специально для того, чтобы избежать конфликта имен по наследованию, так как атрибут переименовывается именем класса в качестве префикса. Это не точный аналог private, поскольку атрибут может быть доступен через составленное имя. Данное свойство можно применить для защиты доступа некоторых атрибутов, однако на практике __ не используется никогда. Если атрибут не является публичным, то принято использовать префикс _. Он не вызывает алгоритм декорирования имени, но документирует атрибут как приватный элемент класса и является преобладающим стилем.

В Python есть и другие механизмы, позволяющие отделить публичную часть класса от приватной. Дескрипторы и свойства дают возможность аккуратно оформить такое разделение.

Дескрипторы

Дескриптор позволяет настроить действие, которое происходит, когда вы ссылаетесь на атрибут объекта.

Дескрипторы лежат в основе организации сложного доступа к атрибутам в Python. Они используются для реализации свойств, методов, методов класса, статических методов и надтипов. Это классы, которые определяют, каким образом будет получен доступ к атрибутам другого класса. Иными словами, класс может делегировать управление атрибута другому классу.

Классы дескрипторов основаны на трех специальных методах, которые формируют протокол дескриптора:

__set__(self, obj, value) — вызывается всякий раз, когда задается атрибут. В следующих примерах мы будем называть его «сеттер»;

__get__(self, obj, owner=None) — вызывается всякий раз, когда считывается атрибут (далее геттер);

__delete__(self, object) — вызывается, когда del вызывается атрибутом.

Дескриптор, который реализует __get__ и __set__, называется дескриптором данных. Если он просто реализует __get__, то называется дескриптором без данных.

Методы этого протокола фактически вызываются методом __getattribute__() (не путать с __getattr__(), который имеет другое назначение) при каждом поиске атрибута. Всякий раз, когда такой поиск выполняется с помощью точки или прямого вызова функции, неявно вызывается метод __getattribute__(), который ищет атрибут в следующем порядке.

  1. Проверяет, является ли атрибут дескриптором данных на объекте класса экземпляра.
  2. Если нет, то смотрит, найдется ли атрибут в __dict__ объекта экземпляра.
  3. Наконец, проверяет, является ли атрибут дескриптором без данных на объекте класса экземпляра.

Иными словами, дескрипторы данных имеют приоритет над __dict__, который, в свою очередь, имеет приоритет над дескрипторами без данных.

Для ясности приведем пример из официальной документации Python, в котором показано, как дескрипторы работают в реальном коде:

class RevealAccess(object):
   """Дескриптор данных, который задает и возвращает значения
      и выводит сообщения о попытках доступа
   """
   def __init__(self, initval=None, name='var'):
      self.val = initval
      self.name = name
   def __get__(self, obj, objtype):
      print('Retrieving', self.name)
      return self.val
   def __set__(self, obj, val):
      print('Updating', self.name)
      self.val = val
class MyClass(object):
   x = RevealAccess(10, 'var "x"')
   y = 5

Вот пример его использования в интерактивном режиме:

>>> m = MyClass()
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5

Пример ясно показывает, что если класс имеет дескриптор данных для этого атрибута, то вызывается метод __get__(), чтобы вернуть значение каждый раз, когда извлекается атрибут экземпляра, а __set__() вызывается всякий раз, когда такому атрибуту присваивается значение. Использование метода __del__ в предыдущем примере не показано, но должно быть очевидно: он вызывается всякий раз, когда атрибут экземпляра удаляется с помощью оператора del instance.attribute или delattr(instance, 'attribute').

Разница между дескрипторами с данными и без имеет большое значение по причинам, которые мы упомянули в начале подраздела. В Python используется протокол дескриптора для связывания функций класса с экземплярами через методы. Они также применяются в декораторах classmethod и staticmethod. Это происходит потому, что функциональные объекты по сути также являются дескрипторами без данных:

>>> def function(): pass
>>> hasattr(function, '__get__')
True
>>> hasattr(function, '__set__')
False

Это верно и для функций, созданных с помощью лямбда-выражений:

>>> hasattr(lambda: None, '__get__')
True
>>> hasattr(lambda: None, '__set__')
False

Таким образом, если __dict__ не будет иметь приоритет над дескрипторами без данных, мы не сможем динамически переопределить конкретные методы уже созданных экземпляров во время выполнения. К счастью, благодаря тому, как дескрипторы работают в Python, это возможно; поэтому разработчики могут выбирать, в каких экземплярах что работает, не используя подклассы.

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

class InitOnAccess:
   def __init__(self, klass, *args, **kwargs):
      self.klass = klass
      self.args = args
      self.kwargs = kwargs
      self._initialized = None
   def __get__(self, instance, owner):
      if self._initialized is None:
         print('initialized!')
         self._initialized = self.klass(*self.args, **self.kwargs)
      else:
         print('cached!')
      return self._initialized

Ниже представлен пример использования:

>>> class MyClass:
... lazily_initialized = InitOnAccess(list, "argument")
...
>>> m = MyClass()
>>> m.lazily_initialized
initialized!
['a', 'r', 'g', 'u', 'm', 'e', 'n', 't']
>>> m.lazily_initialized
cached!
['a', 'r', 'g', 'u', 'm', 'e', 'n', 't']

Официальная библиотека OpenGL Python на PyPI под названием PyOpenGL использует такую технику, чтобы реализовать объект lazy_property, который является одновременно декоратором и дескриптором данных:

class lazy_property(object):
   def __init__(self, function):
      self.fget = function
   def __get__(self, obj, cls):
      value = self.fget(obj)
      setattr(obj, self.fget.__name__, value)
      return value

Такая реализация аналогична использованию декоратора property (о нем поговорим позже), но функция, которая оборачивается декоратором, выполняется только один раз, а затем атрибут класса заменяется значением, возвращенным этим свойством функции. Данный метод часто бывает полезен, когда необходимо одновременно выполнить два требования:

  • экземпляр объекта должен быть сохранен как атрибут класса, который распределяется между его экземплярами (для экономии ресурсов);
  • этот объект не может быть инициализирован в момент импорта, поскольку процесс его создания зависит от некоего глобального состояния приложения/контекста.

В случае приложений, написанных с использованием OpenGL, вы будете часто сталкиваться с такой ситуацией. Например, создание шейдеров в OpenGL обходится дорого, поскольку требует компиляции кода, написанного на OpenGL Shading Language (GLSL). Разумно создавать их только один раз и в то же время держать их описание в непосредственной близости от классов, которым они нужны. С другой стороны, шейдерные компиляции не могут быть выполнены без инициализации контекста OpenGL, так что их трудно определить и собрать в глобальном пространстве имен модуля на момент импорта.

В следующем примере показано возможное использование модифицированной версии декоратора lazy_property PyOpenGL (здесь lazy_class_attribute) в некоем абстрактном приложении OpenGL. Изменения оригинального декоратора lazy_property требуются для того, чтобы разрешить совместное использование атрибута различными экземплярами класса:

import OpenGL.GL as gl
from OpenGL.GL import shaders
class lazy_class_attribute(object):
   def __init__(self, function):
      self.fget = function
   def __get__(self, obj, cls):
      value = self.fget(obj or cls)
      # Примечание: хранение объекта не-экземпляра класса
      # независимо от уровня доступа
      setattr(cls, self.fget.__name__, value)
      return value
class ObjectUsingShaderProgram(object):
   # Банальная реализация шейдера-вершины
    VERTEX_CODE = """
      #version 330 core
      layout(location = 0) in vec4 vertexPosition;
      void main(){
         gl_Position = vertexPosition;
      }
"""
# Шейдер грани, который закрашивает все белым
FRAGMENT_CODE = """
   #version 330 core
   out lowp vec4 out_color;
   void main(){
      out_color = vec4(1, 1, 1, 1);
   }
"""
@lazy_class_attribute
def shader_program(self):
   print("compiling!")
   return shaders.compileProgram(
      shaders.compileShader(
         self.VERTEX_CODE, gl.GL_VERTEX_SHADER
      ),
      shaders.compileShader(
         self.FRAGMENT_CODE, gl.GL_FRAGMENT_SHADER
      )
   )

Как и все расширенные функции синтаксиса Python, эту также следует использовать с осторожностью и хорошо документировать в коде. Неопытным разработчикам измененное поведение класса может преподнести сюрпризы, поскольку дескрипторы влияют на поведение класса. Поэтому очень важно убедиться, что все члены вашей команды знакомы с дескрипторами и понимают эту концепцию, если она играет важную роль в кодовой базе проекта.

Свойства

Свойства предоставляют встроенный тип дескриптора, который знает, как связать атрибут с набором методов. Свойство принимает четыре необязательных аргумента: fget, fset, fdel и doc. Последний из них может быть предусмотрен для определения строки документации, связанной с атрибутом, как если бы это был метод. Ниже приведен пример класса Rectangle, которым можно управлять либо путем прямого доступа к атрибутам, хранящим две угловые точки, либо с помощью свойств width и height:

class Rectangle:
   def __init__(self, x1, y1, x2, y2):
      self.x1, self.y1 = x1, y1
      self.x2, self.y2 = x2, y2
   def _width_get(self):
      return self.x2 - self.x1
      def _width_set(self, value):
      self.x2 = self.x1 + value
   def _height_get(self):
      return self.y2 - self.y1
   def _height_set(self, value):
      self.y2 = self.y1 + value
   width = property(
       _width_get, _width_set,
       doc="rectangle width measured from left"
   )
   height = property(
       _height_get, _height_set,
       doc="rectangle height measured from top"
   )
   def __repr__(self):
      return "{}({}, {}, {}, {})".format(
         self.__class__.__name__,
         self.x1, self.y1, self.x2, self.y2
     )

В следующем фрагменте кода приведен пример таких свойств, определенных в интерактивной сессии:

>>> rectangle.width, rectangle.height
(15, 24)
>>> rectangle.width = 100
>>> rectangle
Rectangle(10, 10, 110, 34)
>>> rectangle.height = 100
>>> rectangle
Rectangle(10, 10, 110, 110)
>>> help(Rectangle)
Help on class Rectangle in module chapter3:
class Rectangle(builtins.object)
| Methods defined here:
|
| __init__(self, x1, y1, x2, y2)
| Initialize self. See help(type(self)) for accurate signature.
|
| __repr__(self)
| Return repr(self).
|
| --------------------------------------------------------
| Data descriptors defined here:
| (...)
|
| height
| rectangle height measured from top
|
| width
| rectangle width measured from left

Эти свойства облегчают написание дескрипторов, но с ними следует аккуратно обращаться при использовании наследования по классам. Атрибут создается динамически с помощью методов текущего класса и не будет применять методы, которые переопределены в производных классах.

Приведенный в следующем примере код не сможет переопределить реализацию метода fget из свойства width родительского класса (Rectangle):

>>> class MetricRectangle(Rectangle):
... def _width_get(self):
... return "{} meters".format(self.x2 - self.x1)
...
>>> Rectangle(0, 0, 100, 100).width
100

Чтобы решить эту проблему, все свойство следует перезаписать в производном классе:

>>> class MetricRectangle(Rectangle):
... def _width_get(self):
... return "{} meters".format(self.x2 - self.x1)
... width = property(_width_get, Rectangle.width.fset)
...
>>> MetricRectangle(0, 0, 100, 100).width
'100 meters'

К сожалению, код имеет кое-какие проблемы с сопровождением. Может возникнуть путаница, если разработчик решит изменить родительский класс, но забудет обновить вызов свойства. Именно поэтому не рекомендуется переопределять только части поведения свойств. Вместо того чтобы полагаться на реализацию родительского класса, рекомендуется переписать все методы свойств в производных классах, если нужно изменить способ их работы. Обычно других вариантов нет, поскольку изменение свойств поведения setter влечет за собой изменение в поведении getter.

Лучшим вариантом создания свойств будет использование property в качестве декоратора. Это позволит сократить количество сигнатур методов внутри класса и сделать код более читабельным и удобным в сопровождении:

class Rectangle:
   def __init__(self, x1, y1, x2, y2):
      self.x1, self.y1 = x1, y1
      self.x2, self.y2 = x2, y2
   @property
   def width(self):
      """Ширина прямоугольника измеряется слева направо"""
      return self.x2 - self.x1
   @width.setter
   def width(self, value):
      self.x2 = self.x1 + value
   @property
   def height(self):
      """Высота измеряется сверху вниз"""
      return self.y2 - self.y1
   @height.setter
   def height(self, value):
      self.y2 = self.y1 + value

Слоты

Интересная функция, которая очень редко используются разработчиками, — это слоты. Они позволяют установить статический список атрибутов для класса с помощью атрибута __slots__ и пропустить создание словаря __dict__ в каждом экземпляре класса. Они были созданы для экономии места в памяти для классов с малочисленными атрибутами, так как __dict__ создается не в каждом экземпляре.

Кроме того, они могут помочь в создании классов, чьи сигнатуры нужно заморозить. Например, если вам необходимо ограничить динамические свойства языка для конкретного класса, то слоты могут помочь:

>>> class Frozen:
... __slots__ = ['ice', 'cream']
...
>>> '__dict__' in dir(Frozen)
False
>>> 'ice' in dir(Frozen)
True
>>> frozen = Frozen()
>>> frozen.ice = True
>>> frozen.cream = None
>>> frozen.icy = True
Traceback (most recent call last): File "<input>", line 1, in <module>
AttributeError: 'Frozen' object has no attribute 'icy'

Эту возможность нужно использовать с осторожностью. Когда набор доступных атрибутов ограничен слотами, намного сложнее добавить что-то к объекту динамически. Некоторые известные приемы, например обезьяний патч (monkey patching), не будут работать с экземплярами классов, которые имеют определенные слоты. К счастью, новые атрибуты можно добавить к производным классам, если они не имеют собственных определенных слотов:

>>> class Unfrozen(Frozen):
... pass
...
>>> unfrozen = Unfrozen()
>>> unfrozen.icy = False
>>> unfrozen.icy
False

Об авторах

Михал Яворски — программист на Python с десятилетним опытом. Занимал разные должности в различных компаниях: от обычного фулстек-разработчика, затем архитектора программного обеспечения и, наконец, до вице-президента по разработке в динамично развивающейся стартап-компании. В настоящее время Михал — старший бэкенд-инженер в Showpad. Имеет большой опыт в разработке высокопроизводительных распределенных сервисов. Кроме того, является активным участником многих проектов Python с открытым исходным кодом.
Тарек Зиаде — Python-разработчик. Живет в сельской местности недалеко от города Дижон во Франции. Работает в Mozilla, в команде, отвечающей за сервисы. Тарек основал французскую группу пользователей Python (называется Afpy) и написал несколько книг о Python на французском и английском языках. В свободное от хакинга и тусовок время занимается любимыми хобби: бегом или игрой на трубе.

Вы можете посетить его личный блог (Fetchez le Python) и подписаться на него в Twitter (tarek_ziade).

О научном редакторе

Коди Джексон — кандидат наук, основатель компании Socius Consulting, работающей в сфере IT и консалтинга по управлению бизнесом в Сан-Антонио, а также соучредитель Top Men Technologies. В настоящее время работает в CACI International ведущим инженером по моделированию ICS/SCADA. В IT-индустрии с 1994 года, еще со времен службы в ВМФ в качестве ядерного химика и радиотехника. До CACI он работал в университете в ECPI в должности ассистента профессора по компьютерным информационным системам. Выучился программированию на Python самостоятельно, написал книги Learning to Program Using Python и Secret Recipes of the Python Ninja.

Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок

Для Хаброжителей скидка 25% по купону — Python

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Теги:книга
Хабы: Издательский дом «Питер» Python Профессиональная литература
Всего голосов 4: ↑4 и ↓0 +4
Просмотры4.9K

Похожие публикации

Лучшие публикации за сутки

Информация

Дата основания
Местоположение
Россия
Сайт
piter.com
Численность
201–500 человек
Дата регистрации

Блог на Хабре