El decorador de propiedades de Python

A menudo se considera una buena práctica crear getters y setters para las propiedades públicas de una clase. Muchos lenguajes te permiten implementar esto de diferentes maneras...

A menudo se considera una buena práctica crear captadores y definidores para las propiedades públicas de una clase. Muchos lenguajes te permiten implementar esto de diferentes maneras, ya sea usando una función (como person.getName()), o usando una construcción get o set específica del idioma. En Python, se hace usando @property.

En este artículo, describiré el decorador de propiedades de Python, que quizás hayas visto que se usa con la sintaxis @decorator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Person(object):
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def full_name(self):
        return self.first_name + ' ' + self.last_name

    @full_name.setter
    def full_name(self, value):
        first_name, last_name = value.split(' ')
        self.first_name = first_name
        self.last_name = last_name

    @full_name.deleter
    def full_name(self):
        del self.first_name
        del self.last_name

Esta es la forma en que Python crea getters, setters y deleters (o métodos mutadores) para una propiedad en una clase.

En este caso, el decorador @property hace que llame al método full_name(self) como si fuera una propiedad normal, cuando en realidad es un método que contiene código para ejecutar cuando se establece la propiedad .

Usar un getter/setter/deleter como este nos brinda bastantes ventajas, algunas de las cuales he enumerado aquí:

  • Validación: antes de configurar la propiedad interna, puede validar que el valor proporcionado cumpla con algunos criterios y hacer que arroje un error si no es así.
  • Carga diferida: los recursos pueden [cargado perezosamente] (https://en.wikipedia.org/wiki/Lazy_loading) para diferir el trabajo hasta que realmente se necesite, ahorrando tiempo y recursos
  • Abstracción: los getters y setters le permiten abstraer la representación interna de los datos. Como nuestro ejemplo anterior, por ejemplo, el nombre y el apellido se almacenan por separado, pero los getters y setters contienen la lógica que usa el nombre y el apellido para crear el nombre completo.
  • Depuración: dado que los métodos mutadores pueden encapsular cualquier código, se convierte en un excelente lugar para la intercepción al depurar (o registrar) su código. Por ejemplo, puede registrar o inspeccionar cada vez que se cambia el valor de una propiedad.

Python logra esta funcionalidad con decoradores, que son métodos especiales que se utilizan para cambiar el comportamiento de otra función o clase. Para describir cómo funciona el decorador @property, echemos un vistazo a un decorador más simple y cómo funciona internamente.

Un decorador es simplemente una función que toma otra función como argumento y la agrega a su comportamiento envolviéndola. Aquí hay un ejemplo simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# decorator.py

def some_func():
    print 'Hey, you guys'

def my_decorator(func):
    def inner():
        print 'Before func!'
        func()
        print 'After func!'

    return inner

print 'some_func():'
some_func()

print ''

some_func_decorated = my_decorator(some_func)

print 'some_func() with decorator:'
some_func_decorated()

Ejecutar este código te da:

1
2
3
4
5
6
7
8
$ python decorator.py
some_func():
Hey, you guys

some_func() with decorator:
Before func!
Hey, you guys
After func!

Como puede ver, la función my_decorator() crea dinámicamente una nueva función para regresar usando la función de entrada, agregando código para ejecutar antes y después de que se ejecute la función original.

El decorador property se implementa con un patrón similar a la función my_decorator. Usando la sintaxis @decorator de Python, recibe la función decorada como un argumento, como en mi ejemplo: some_func_decorated = my_decorator(some_func).

Entonces, volviendo a mi primer ejemplo, este código:

1
2
3
@property
def full_name_getter(self):
    return self.first_name + ' ' + self.last_name

Es más o menos equivalente a esto:

1
2
3
4
def full_name_getter(self):
    return self.first_name + ' ' + self.last_name

full_name = property(full_name_getter)

Tenga en cuenta que cambié algunos nombres de funciones para mayor claridad.

Luego, más adelante, cuando quieras usar @full_name.setter como lo hacemos en el ejemplo, lo que realmente estás llamando es:

1
2
3
4
5
6
7
def full_name_setter(self, value):
    first_name, last_name = value.split(' ')
    self.first_name = first_name
    self.last_name = last_name

full_name = property(full_name_getter)
full_name = full_name.setter(full_name_setter)

Ahora, este nuevo objeto full_name (una instancia del objeto property) tiene métodos getter y setter.

Para usarlos con nuestra clase, Persona, el objeto propiedad actúa como un descriptor, lo que significa que tiene su propio [__obtener__()](https://docs.python. org/2/reference/datamodel.html#object.get), __establecer__() y __Eliminar__() métodos. Los métodos __get__() y __set__() se activan en un objeto cuando se recupera o establece una propiedad, y __delete__() se activa cuando se elimina una propiedad con del.

Entonces person.full_name = 'Billy Bob' activa el método __set__(), que fue heredado de object. Esto nos lleva a un punto importante: su clase debe heredar de objeto para que esto funcione. Entonces, una clase como esta no podría usar propiedades de establecimiento ya que no hereda de objeto:

1
2
class Person:
    pass

Gracias a property, estos métodos ahora corresponden a nuestros métodos full_name_getter y full_name_setter de arriba:

1
2
full_name.fget is full_name_getter    # True
full_name.fset is full_name_setter    # True

fget y fset ahora están envueltos por .__get__() y .__set__(), respectivamente.

Y finalmente, se puede acceder a estos objetos descriptores pasando una referencia a nuestra clase, Persona:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> person = Person('Billy', 'Bob')
>>> 
>>> full_name.__get__(person)
Billy Bob
>>>
>>> full_name.__set__(person, 'Timmy Thomas')
>>>
>>> person.first_name
Timmy
>>> person.last_name
Thomas

Así es esencialmente cómo funcionan las propiedades debajo de la superficie.

Licensed under CC BY-NC-SA 4.0