Шаг 177.
Python: сборник рецептов.
Метапрограммирование. Избегание повторяющихся методов свойств

    На этом шаге мы рассмотрим решение этой задачи.

Задача

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

Решение

    Рассмотрите простой класс, в котором атрибуты обернуты методами-свойствами:

class Person:
    def __init__(self, name ,age):
        self.name = name
        self.age = age

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('name must be a string')
        self._name = value

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if not isinstance(value, int):
            raise TypeError('age must be an int')
        self._age = value

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

def typed_property(name, expected_type):
    storage_name = '_' + name

    @property
    def prop(self):
        return getattr(self, storage_name)

    @prop.setter
    def prop(self, value):
        if not isinstance(value, expected_type):
            raise TypeError('{} must be a {}'.format(name, expected_type))
        setattr(self, storage_name, value)
    return prop

# Пример
class Person:
    name = typed_property('name', str)
    age = typed_property('age', int)
    def __init__(self, name, age):
        self.name = name
        self.age = age


Обсуждение

    Этот рецепт иллюстрирует важную возможность вложенных функций (замыканий) - с точки зрения работы они во многом похожи на макросы. Функция typed_property() в этом примере может показаться странной, но она просто генерирует для вас код свойства и возвращает получившийся объект свойства. Так что при использовании в классе она работает точно так же, как если бы код, появляющийся внутри typed_property(), был помещен в само определение класса. Хотя методы свойства геттер и сеттер обращаются к локальным переменным, таким как name, expected_type и storage_name, это нормально - эти значения хранятся в замыкании.

    Данный рецепт можно интересным образом "улучшить", используя функцию functools.partial(). Например, вы можете сделать так:

from functools import partial

String = partial(typed_property, expected_type=str)
Integer = partial(typed_property, expected_type=int)

# Пример
class Person:
    name = String('name')
    age = Integer('age')
    def __init__(self, name, age):
        self.name = name
        self.age = age

    Тут код начинает походить на дескриптор системы типов, показанный в 142 шаге.

    На следующем шаге мы рассмотрим легкий способ определения менеджеров контекста.




Предыдущий шаг Содержание