Usando @property versus getters y setters


728

Aquí hay una pregunta de diseño puramente específica de Python:

class MyClass(object):
    ...
    def get_my_attr(self):
        ...

    def set_my_attr(self, value):
        ...

y

class MyClass(object):
    ...        
    @property
    def my_attr(self):
        ...

    @my_attr.setter
    def my_attr(self, value):
        ...

Python nos permite hacerlo de cualquier manera. Si diseñaras un programa Python, ¿qué enfoque usarías y por qué?

Respuestas:


614

Prefiero propiedades . Para eso están allí.

La razón es que todos los atributos son públicos en Python. Iniciar nombres con un guión bajo o dos es solo una advertencia de que el atributo dado es un detalle de implementación que puede no ser el mismo en futuras versiones del código. No le impide realmente obtener o establecer ese atributo. Por lo tanto, el acceso a atributos estándar es la forma normal y pitónica de, bueno, acceder a los atributos.

La ventaja de las propiedades es que son sintácticamente idénticas al acceso a los atributos, por lo que puede cambiar de una a otra sin ningún cambio en el código del cliente. Incluso podría tener una versión de una clase que use propiedades (por ejemplo, para código por contrato o depuración) y una que no lo haga para producción, sin cambiar el código que la usa. Al mismo tiempo, no tiene que escribir getters y setters para todo en caso de que necesite controlar mejor el acceso más adelante.


90
Python maneja especialmente los nombres de atributo con un doble guión bajo; no es solo una mera convención. Ver docs.python.org/py3k/tutorial/classes.html#private-variables
6502

63
Se manejan de manera diferente, pero eso no le impide acceder a ellos. PD: AD 30 C0
poco el

44
y porque los caracteres "@" son feos en el código de Python, y la desreferenciación de @decorators da la misma sensación de código de espagueti.
Berry Tsakala

18
No estoy de acuerdo ¿Cómo es el código estructurado igual al código de espagueti? Python es un lenguaje hermoso. Pero sería aún mejor con un mejor soporte para cosas simples como encapsulación adecuada y clases estructuradas.

69
Si bien estoy de acuerdo en la mayoría de los casos, tenga cuidado al ocultar métodos lentos detrás de un decorador @property. El usuario de su API espera que el acceso a la propiedad funcione como acceso variable, y alejarse demasiado de esa expectativa puede hacer que su API sea desagradable de usar.
defrex

154

En Python no usas getters o setters o propiedades solo por el gusto de hacerlo. Primero solo usa atributos y luego, solo si es necesario, eventualmente migra a una propiedad sin tener que cambiar el código usando sus clases.

De hecho, hay una gran cantidad de código con extensión .py que usa getters y setters y herencia y clases sin sentido en todas partes donde, por ejemplo, una simple tupla funcionaría, pero es código de personas que escriben en C ++ o Java usando Python.

Ese no es el código Python.


46
@ 6502, cuando dijo "[…] clases sin sentido en todas partes donde, por ejemplo, una simple tupla sería suficiente": la ventaja de una clase sobre una tupla es que una instancia de clase proporciona nombres explícitos para acceder a sus partes, mientras que una tupla no . Los nombres son mejores en legibilidad y evitan errores, que las suscripciones de tuplas, especialmente cuando esto se va a pasar fuera del módulo actual.
Hibou57

15
@ Hibou57: No digo que las clases sean inútiles. Pero a veces una tupla es más que suficiente. Sin embargo, el problema es que quien viene de Java o C ++ no tiene más remedio que crear clases para todo porque otras posibilidades son molestas de usar en esos idiomas. Otro síntoma típico de la programación Java / C ++ que usa Python es la creación de clases abstractas y jerarquías de clases complejas sin ninguna razón en la que en Python se puedan usar clases independientes gracias a la escritura de pato.
6502

39
@ Hibou57 para eso también puedes usar namedtuple: doughellmann.com/PyMOTW/collections/namedtuple.html
hugo24

55
@JonathonReinhart: está en la biblioteca estándar desde 2.6 ... ver docs.python.org/2/library/collections.html
6502

1
Cuando "eventualmente migre a una propiedad si es necesario", es muy probable que rompa el código usando sus clases. Las propiedades a menudo introducen restricciones: cualquier código que no esperaba estas restricciones se romperá tan pronto como las introduzca.
yaccob

118

El uso de propiedades le permite comenzar con los accesos normales a los atributos y luego respaldarlos con getters y setters luego, según sea necesario .


3
@GregKrsak Parece extraño porque lo es. La "cosa de los adultos que consienten" era un meme de Python de antes de que se agregaran las propiedades. Fue la respuesta de stock a la gente que se quejaba de la falta de modificadores de acceso. Cuando se agregaron las propiedades, de repente la encapsulación se volvió deseable. Lo mismo sucedió con las clases base abstractas. "Python siempre estuvo en guerra con la ruptura de la encapsulación. La libertad es esclavitud. Lambdas solo debería caber en una línea".
johncip

71

La respuesta corta es: las propiedades ganan sin dudas . Siempre.

A veces hay necesidad de captadores y colocadores, pero aun así, los "escondería" al mundo exterior. Hay un montón de maneras de hacer esto en Python ( getattr, setattr, __getattribute__, etc ..., pero muy concisa y limpia es:

def set_email(self, value):
    if '@' not in value:
        raise Exception("This doesn't look like an email address.")
    self._email = value

def get_email(self):
    return self._email

email = property(get_email, set_email)

Aquí hay un breve artículo que presenta el tema de getters y setters en Python.


1
@BasicWolf - ¡Pensé que estaba implícitamente claro que estoy en el lado de la propiedad de la cerca! :) Pero agrego un párrafo a mi respuesta para aclarar eso.
mac

99
SUGERENCIA: La palabra "siempre" es una pista de que el autor intenta convencerlo con una afirmación, no un argumento. Así es la presencia de la fuente en negrita. (Quiero decir, si ve CAPS en su lugar, entonces, vaya, debe ser correcto.) Mire, la característica de "propiedad" es diferente de Java (némesis de facto de Python por alguna razón) y, por lo tanto, piensa en el grupo de la comunidad de Python. declara que es mejor. En realidad, las propiedades violan la regla "explícito es mejor que implícito", pero nadie quiere admitirlo. Llegó al lenguaje, por lo que ahora se declara "Pitónico" a través de un argumento tautológico.
Stuart Berg

3
No hay sentimientos heridos. :-P Solo estoy tratando de señalar que las convenciones "pitónicas" son inconsistentes en este caso: "explícito es mejor que implícito" está en conflicto directo con el uso de a property. ( Parece una tarea simple, pero llama a una función.) Por lo tanto, "Pitónico" es esencialmente un término sin sentido, excepto por la definición tautológica: "Las convenciones pitónicas son cosas que hemos definido como pitónicas".
Stuart Berg

1
Ahora, la idea de tener un conjunto de convenciones que sigan un tema es genial . Si existiera tal conjunto de convenciones, entonces podría usarlo como un conjunto de axiomas para guiar su pensamiento, no simplemente una larga lista de verificación de trucos para memorizar, que es significativamente menos útil. Los axiomas podrían usarse para la extrapolación y ayudarlo a abordar problemas que nadie ha visto todavía. Es una pena que la propertycaracterística amenace con hacer que la idea de axiomas pitónicos sea casi inútil. Entonces, todo lo que nos queda es una lista de verificación.
Stuart Berg

1
No estoy de acuerdo Prefiero las propiedades en la mayoría de las situaciones, pero cuando desea enfatizar que establecer algo tiene efectos secundarios además de modificar el selfobjeto , los establecedores explícitos pueden ser útiles. Por ejemplo, user.email = "..."no parece que pueda generar una excepción porque solo parece establecer un atributo, mientras que user.set_email("...")deja en claro que podría haber efectos secundarios como excepciones.
bluenote10

65

[ TL; DR? Puedes saltar al final para ver un ejemplo de código .]

De hecho, prefiero usar un idioma diferente, que es un poco complicado de usar como algo único, pero es bueno si tienes un caso de uso más complejo.

Un poco de historia primero.

Las propiedades son útiles porque nos permiten manejar tanto la configuración como la obtención de valores de una manera programática, pero aún permiten acceder a los atributos como atributos. Podemos convertir 'gets' en 'cálculos' (esencialmente) y podemos convertir 'sets' en 'eventos'. Digamos que tenemos la siguiente clase, que he codificado con getters y setters similares a Java.

class Example(object):
    def __init__(self, x=None, y=None):
        self.x = x
        self.y = y

    def getX(self):
        return self.x or self.defaultX()

    def getY(self):
        return self.y or self.defaultY()

    def setX(self, x):
        self.x = x

    def setY(self, y):
        self.y = y

    def defaultX(self):
        return someDefaultComputationForX()

    def defaultY(self):
        return someDefaultComputationForY()

Tal vez se pregunte por qué no llamé defaultXy defaultYen el __init__método del objeto . La razón es que para nuestro caso quiero suponer que los someDefaultComputationmétodos devuelven valores que varían con el tiempo, digamos una marca de tiempo, y siempre que x(o y) no esté establecido (donde, para el propósito de este ejemplo, "no establecido" significa "establecido" a Ninguno ") Quiero el valor del cálculo predeterminado de x'(o y' s).

Así que esto es lamentable por una serie de razones descritas anteriormente. Lo reescribiré usando propiedades:

class Example(object):
    def __init__(self, x=None, y=None):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self.x or self.defaultX()

    @x.setter
    def x(self, value):
        self._x = value

    @property
    def y(self):
        return self.y or self.defaultY()

    @y.setter
    def y(self, value):
        self._y = value

    # default{XY} as before.

¿Qué hemos ganado? Hemos ganado la capacidad de referirnos a estos atributos como atributos aunque, entre bastidores, terminamos ejecutando métodos.

Por supuesto, el poder real de las propiedades es que generalmente queremos que estos métodos hagan algo además de obtener y establecer valores (de lo contrario, no tiene sentido usar propiedades). Hice esto en mi ejemplo getter. Básicamente, estamos ejecutando un cuerpo de función para seleccionar un valor predeterminado cada vez que no se establece el valor. Este es un patrón muy común.

Pero, ¿qué estamos perdiendo y qué no podemos hacer?

La molestia principal, en mi opinión, es que si define un captador (como lo hacemos aquí) también debe definir un setter. [1] Eso es ruido extra que satura el código.

Otra molestia es que todavía tenemos que inicializar los valores xy . (Bueno, por supuesto, podríamos agregarlos usando, pero eso es más código adicional).y__init__setattr()

Tercero, a diferencia del ejemplo similar a Java, los captadores no pueden aceptar otros parámetros. Ahora puedo oírte decir, bueno, si está tomando parámetros, ¡no es un captador! En un sentido oficial, eso es cierto. Pero en un sentido práctico, no hay razón para que no podamos parametrizar un atributo con nombre, como x, y establecer su valor para algunos parámetros específicos.

Sería bueno si pudiéramos hacer algo como:

e.x[a,b,c] = 10
e.x[d,e,f] = 20

por ejemplo. Lo más cerca que podemos llegar es anular la asignación para implicar una semántica especial:

e.x = [a,b,c,10]
e.x = [d,e,f,30]

y, por supuesto, asegúrese de que nuestro configurador sepa cómo extraer los primeros tres valores como clave para un diccionario y establecer su valor en un número o algo así.

Pero incluso si lo hiciéramos, aún no podríamos admitirlo con propiedades porque no hay forma de obtener el valor porque no podemos pasar parámetros en absoluto al getter. Así que hemos tenido que devolver todo, introduciendo una asimetría.

El getter / setter de estilo Java nos permite manejar esto, pero volvemos a necesitar getter / setter.

En mi opinión, lo que realmente queremos es algo que capture los siguientes requisitos:

  • Los usuarios definen solo un método para un atributo dado y pueden indicar allí si el atributo es de solo lectura o de lectura-escritura. Las propiedades fallan en esta prueba si el atributo se puede escribir.

  • No es necesario que el usuario defina una variable adicional subyacente a la función, por lo que no necesitamos el __init__o setattren el código. La variable simplemente existe por el hecho de que hemos creado este nuevo atributo de estilo.

  • Cualquier código predeterminado para el atributo se ejecuta en el propio cuerpo del método.

  • Podemos establecer el atributo como un atributo y hacer referencia a él como un atributo.

  • Podemos parametrizar el atributo.

En términos de código, queremos una forma de escribir:

def x(self, *args):
    return defaultX()

y luego poder hacer:

print e.x     -> The default at time T0
e.x = 1
print e.x     -> 1
e.x = None
print e.x     -> The default at time T1

Etcétera.

También queremos una manera de hacer esto para el caso especial de un atributo parametrizable, pero aún así permitimos que funcione el caso de asignación predeterminado. Verá cómo abordé esto a continuación.

Ahora al punto (¡sí! ¡El punto!). La solución que encontré para esto es la siguiente.

Creamos un nuevo objeto para reemplazar la noción de una propiedad. El objeto está destinado a almacenar el valor de un conjunto de variables, pero también mantiene un identificador de código que sabe cómo calcular un valor predeterminado. Su trabajo es almacenar el conjunto valueo ejecutar methodsi ese valor no está establecido.

Llamémoslo un UberProperty.

class UberProperty(object):

    def __init__(self, method):
        self.method = method
        self.value = None
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def clearValue(self):
        self.value = None
        self.isSet = False

Supongo que methodaquí hay un método de clase, valuees el valor de UberProperty, y he agregado isSetporque Nonepuede ser un valor real y esto nos permite una forma clara de declarar que realmente no hay "ningún valor". Otra forma es un centinela de algún tipo.

Básicamente, esto nos da un objeto que puede hacer lo que queremos, pero ¿cómo lo ponemos realmente en nuestra clase? Bueno, las propiedades usan decoradores; porque no podemos Veamos cómo podría verse (de aquí en adelante me limitaré a usar solo un 'atributo' x).

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

En realidad, esto aún no funciona, por supuesto. Tenemos que implementar uberPropertyy asegurarnos de que maneja tanto get como sets.

Comencemos con gets.

Mi primer intento fue simplemente crear un nuevo objeto UberProperty y devolverlo:

def uberProperty(f):
    return UberProperty(f)

Rápidamente descubrí, por supuesto, que esto no funciona: Python nunca une lo invocable al objeto y necesito el objeto para llamar a la función. Incluso crear el decorador en la clase no funciona, ya que aunque ahora tenemos la clase, todavía no tenemos un objeto con el que trabajar.

Entonces vamos a necesitar poder hacer más aquí. Sabemos que un método solo necesita ser representado una vez, así que sigamos con nuestro decorador, pero modifiquemos UberPropertypara almacenar solo la methodreferencia:

class UberProperty(object):

    def __init__(self, method):
        self.method = method

Tampoco es invocable, por lo que por el momento nada funciona.

¿Cómo completamos la imagen? Bueno, ¿con qué terminamos cuando creamos la clase de ejemplo usando nuestro nuevo decorador?

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

print Example.x     <__main__.UberProperty object at 0x10e1fb8d0>
print Example().x   <__main__.UberProperty object at 0x10e1fb8d0>

en ambos casos recuperamos lo UberPropertyque, por supuesto, no es invocable, por lo que esto no es de mucha utilidad.

Lo que necesitamos es alguna forma de vincular dinámicamente la UberPropertyinstancia creada por el decorador después de que la clase se haya creado a un objeto de la clase antes de que ese objeto se haya devuelto a ese usuario para su uso. Um, sí, es una __init__llamada, amigo.

Escribamos lo que queremos que nuestro resultado sea primero. Estamos vinculando UberPropertya una instancia, por lo que una cosa obvia para devolver sería una BoundUberProperty. Aquí es donde realmente mantendremos el estado del xatributo.

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

Ahora nosotros la representación; ¿Cómo llevar esto a un objeto? Hay algunos enfoques, pero el más fácil de explicar solo usa el __init__método para hacer ese mapeo. En el momento en que __init__se llama, nuestros decoradores se han ejecutado, por lo que solo debemos mirar a través de los objetos __dict__y actualizar los atributos donde el valor del atributo es de tipo UberProperty.

Ahora, las propiedades súper son geniales y probablemente querremos usarlas mucho, por lo que tiene sentido crear una clase base que haga esto para todas las subclases. Creo que sabes cómo se llamará la clase base.

class UberObject(object):
    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)

Agregamos esto, cambiamos nuestro ejemplo para heredar de UberObject, y ...

e = Example()
print e.x               -> <__main__.BoundUberProperty object at 0x104604c90>

Después de modificar xpara ser:

@uberProperty
def x(self):
    return *datetime.datetime.now()*

Podemos ejecutar una prueba simple:

print e.x.getValue()
print e.x.getValue()
e.x.setValue(datetime.date(2013, 5, 31))
print e.x.getValue()
e.x.clearValue()
print e.x.getValue()

Y obtenemos la salida que queríamos:

2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310

(Gee, estoy trabajando hasta tarde)

Tenga en cuenta que he utilizado getValue, setValuey clearValueaquí. Esto se debe a que aún no he vinculado los medios para devolverlos automáticamente.

Pero creo que este es un buen lugar para parar por ahora, porque me estoy cansando. También puede ver que la funcionalidad principal que queríamos está en su lugar; el resto es escaparatismo. Escaparate de usabilidad importante, pero eso puede esperar hasta que tenga un cambio para actualizar la publicación.

Terminaré el ejemplo en la próxima publicación abordando estas cosas:

  • Necesitamos asegurarnos de que UberObject's __init__siempre sea llamado por subclases.

    • Por lo tanto, forzamos que se llame a alguna parte o evitamos que se implemente.
    • Veremos cómo hacer esto con una metaclase.
  • Necesitamos asegurarnos de manejar el caso común en el que alguien 'alias' una función a otra cosa, como:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
  • Necesitamos e.xregresar e.x.getValue()por defecto.

    • Lo que realmente veremos es que esta es un área donde el modelo falla.
    • Resulta que siempre tendremos que usar una llamada de función para obtener el valor.
    • Pero podemos hacer que parezca una llamada de función normal y evitar tener que usarla e.x.getValue(). (Hacer esto es obvio, si aún no lo ha solucionado).
  • Necesitamos apoyar la configuración e.x directly, como en e.x = <newvalue>. También podemos hacer esto en la clase padre, pero necesitaremos actualizar nuestro __init__código para manejarlo.

  • Finalmente, agregaremos atributos parametrizados. Debería ser bastante obvio cómo haremos esto también.

Aquí está el código tal como existe hasta ahora:

import datetime

class UberObject(object):
    def uberSetter(self, value):
        print 'setting'

    def uberGetter(self):
        return self

    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)


class UberProperty(object):
    def __init__(self, method):
        self.method = method

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

    def uberProperty(f):
        return UberProperty(f)

class Example(UberObject):

    @uberProperty
    def x(self):
        return datetime.datetime.now()

[1] Puedo estar atrasado sobre si este sigue siendo el caso.


53
Sí, esto es 'tldr'. ¿Puede resumir lo que está tratando de hacer aquí?
Poolie

99
@ Adam, return self.x or self.defaultX()este es un código peligroso. Lo que sucede cuando self.x == 0?
Kelly Thomas

Para su información, puede hacerlo para que pueda parametrizar el captador, más o menos. Implicaría hacer de la variable una clase personalizada, de la cual ha anulado el __getitem__método. Sin embargo, sería extraño, ya que entonces tendría una pitón completamente no estándar.
será el

2
@KellyThomas Solo trato de mantener el ejemplo simple. Para hacerlo bien, necesitaría crear y eliminar la entrada x dict por completo, porque incluso un valor None podría haberse establecido específicamente. Pero sí, tiene toda la razón, esto es algo que debería considerar en un caso de uso de producción.
Adam Donahue

Los getters similares a Java te permiten hacer exactamente el mismo cálculo, ¿no?
qed

26

Creo que ambos tienen su lugar. Un problema con el uso @propertyes que es difícil extender el comportamiento de los captadores o establecedores en subclases utilizando mecanismos de clase estándar. El problema es que las funciones getter / setter reales están ocultas en la propiedad.

De hecho, puede obtener las funciones, por ejemplo, con

class C(object):
    _p = 1
    @property
    def p(self):
        return self._p
    @p.setter
    def p(self, val):
        self._p = val

puede acceder a las funciones getter y setter como C.p.fgety C.p.fset, pero no puede usar fácilmente las funciones de herencia del método normal (por ejemplo, super) para extenderlas. Después de algo de investigación en las complejidades de súper, se puede utilizar de hecho súper de esta manera:

# Using super():
class D(C):
    # Cannot use super(D,D) here to define the property
    # since D is not yet defined in this scope.
    @property
    def p(self):
        return super(D,D).p.fget(self)

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for D'
        super(D,D).p.fset(self, val)

# Using a direct reference to C
class E(C):
    p = C.p

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for E'
        C.p.fset(self, val)

Sin embargo, usar super () es bastante complicado, ya que la propiedad debe redefinirse, y debe usar el mecanismo super (cls, cls) ligeramente contraintuitivo para obtener una copia no vinculada de p.


20

Usar propiedades es para mí más intuitivo y se adapta mejor a la mayoría de los códigos.

Comparando

o.x = 5
ox = o.x

vs.

o.setX(5)
ox = o.getX()

es bastante obvio para mí lo que es más fácil de leer. También las propiedades permiten variables privadas mucho más fáciles.


12

Preferiría no usar ninguno en la mayoría de los casos. El problema con las propiedades es que hacen que la clase sea menos transparente. Especialmente, este es un problema si se plantea una excepción de un setter. Por ejemplo, si tiene una propiedad Account.email:

class Account(object):
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if '@' not in value:
            raise ValueError('Invalid email address.')
        self._email = value

entonces el usuario de la clase no espera que asignar un valor a la propiedad pueda causar una excepción:

a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.

Como resultado, la excepción puede pasar desapercibida y propagarse demasiado alto en la cadena de llamadas para ser manejada adecuadamente, o dar como resultado que se presente un rastreo muy inútil al usuario del programa (que lamentablemente es demasiado común en el mundo de Python y Java )

También evitaría usar getters y setters:

  • porque definirlos de antemano para todas las propiedades lleva mucho tiempo,
  • hace que la cantidad de código sea innecesariamente más larga, lo que dificulta la comprensión y el mantenimiento del código,
  • si las definiera para propiedades solo según sea necesario, la interfaz de la clase cambiaría, perjudicando a todos los usuarios de la clase

En lugar de propiedades y captadores / establecedores, prefiero hacer la lógica compleja en lugares bien definidos, como en un método de validación:

class Account(object):
    ...
    def validate(self):
        if '@' not in self.email:
            raise ValueError('Invalid email address.')

o un método similar de Account.save.

Tenga en cuenta que no estoy tratando de decir que no hay casos en que las propiedades sean útiles, solo que puede estar mejor si puede hacer que sus clases sean lo suficientemente simples y transparentes como para no necesitarlas.


3
@ user2239734 Creo que malinterpretas el concepto de propiedades. Aunque puede validar el valor mientras configura la propiedad, no es necesario hacerlo. Puede tener propiedades y un validate()método en una clase. Una propiedad simplemente se usa cuando tiene una lógica compleja detrás de una obj.x = yasignación simple , y depende de cuál sea la lógica.
Zaur Nasibov

12

Siento que las propiedades se tratan de permitirle obtener la sobrecarga de escribir getters y setters solo cuando realmente los necesita.

La cultura de programación de Java recomienda encarecidamente nunca dar acceso a las propiedades y, en su lugar, pasar por getters y setters, y solo aquellos que realmente se necesitan. Es un poco detallado escribir siempre estas piezas de código obvias y observar que el 70% de las veces nunca son reemplazadas por alguna lógica no trivial.

En Python, las personas realmente se preocupan por ese tipo de sobrecarga, para que pueda adoptar la siguiente práctica:

  • No use getters y setters al principio, cuando no sean necesarios
  • Úselo @propertypara implementarlos sin cambiar la sintaxis del resto de su código.

1
"y observe que el 70% del tiempo nunca son reemplazados por alguna lógica no trivial". - ese es un número bastante específico, ¿viene de algún lugar, o lo piensas como una especie de "la gran mayoría" manual (no estoy siendo gracioso, si hay un estudio que cuantifique ese número, estaría genuinamente interesado en leerlo)
Adam Parkin

1
Oh no lo siento Parece que tengo algunos estudios para respaldar este número, pero solo lo dije como "la mayoría de las veces".
fulmicoton

77
No es que la gente se preocupe por los gastos generales, es que en Python puede cambiar el acceso directo a los métodos de acceso sin cambiar el código del cliente, por lo que no tiene nada que perder al exponer directamente las propiedades al principio.
Neil G

10

Me sorprende que nadie haya mencionado que las propiedades son métodos vinculados de una clase de descriptor, Adam Donohue y NeilenMarais captan exactamente esta idea en sus publicaciones: que los captadores y los establecedores son funciones y pueden usarse para:

  • validar
  • alterar datos
  • tipo de pato (tipo coercitivo a otro tipo)

Esto presenta una forma inteligente de ocultar los detalles de implementación y el código cruft como expresiones regulares, tipos de conversión, try .. excepto bloques, aserciones o valores calculados.

En general, hacer CRUD en un objeto a menudo puede ser bastante mundano, pero considere el ejemplo de datos que se conservarán en una base de datos relacional. Los ORM pueden ocultar detalles de implementación de vernáculos SQL particulares en los métodos vinculados a fget, fset, fdel definidos en una clase de propiedad que administrará las terribles si .. elif .. otras escaleras que son tan feas en el código OO - exponiendo lo simple y elegante self.variable = somethingy obvia los detalles para el desarrollador que usa el ORM.

Si uno piensa en las propiedades solo como un vestigio lúgubre de un lenguaje de Bondage y Disciplina (es decir, Java), no tiene en cuenta los descriptores.


6

En proyectos complejos, prefiero usar propiedades de solo lectura (o captadores) con función de establecimiento explícito:

class MyClass(object):
...        
@property
def my_attr(self):
    ...

def set_my_attr(self, value):
    ...

En proyectos de larga duración, la depuración y refactorización lleva más tiempo que escribir el código en sí. Hay varias desventajas para usar @property.setterque hacen que la depuración sea aún más difícil:

1) Python permite crear nuevos atributos para un objeto existente. Esto hace que el siguiente error de imprenta sea muy difícil de rastrear:

my_object.my_atttr = 4.

Si su objeto es un algoritmo complicado, pasará bastante tiempo tratando de descubrir por qué no converge (observe una 't' adicional en la línea de arriba)

2) setter a veces puede evolucionar a un método complicado y lento (por ejemplo, golpear una base de datos). Sería bastante difícil para otro desarrollador descubrir por qué la siguiente función es muy lenta. Podría pasar mucho tiempo en el do_something()método de creación de perfiles , mientras my_object.my_attr = 4.que en realidad es la causa de la desaceleración:

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()

5

Tanto @propertylos getters como los setters tradicionales tienen sus ventajas. Depende de su caso de uso.

Ventajas de @property

  • No tiene que cambiar la interfaz mientras cambia la implementación del acceso a datos. Cuando su proyecto es pequeño, probablemente desee utilizar el acceso directo a los atributos para acceder a un miembro de la clase. Por ejemplo, supongamos que tiene un objeto foode tipo Foo, que tiene un miembro num. Entonces simplemente puede obtener este miembro con num = foo.num. A medida que crece su proyecto, puede sentir que debe haber algunas comprobaciones o depuraciones en el acceso de atributo simple. Entonces puedes hacerlo con un @property dentro de la clase. La interfaz de acceso a datos sigue siendo la misma, por lo que no es necesario modificar el código del cliente.

    Citado de PEP-8 :

    Para atributos de datos públicos simples, es mejor exponer solo el nombre del atributo, sin métodos complicados de acceso / mutación. Tenga en cuenta que Python proporciona un camino fácil para mejoras futuras, en caso de que un atributo de datos simple necesite crecer en el comportamiento funcional. En ese caso, use las propiedades para ocultar la implementación funcional detrás de la sintaxis de acceso a atributos de datos simple.

  • El uso @propertypara el acceso a datos en Python se considera Pythonic :

    • Puede fortalecer su autoidentificación como programador de Python (no Java).

    • Puede ayudar a su entrevista de trabajo si su entrevistador cree que los captadores y establecedores de estilo Java son antipatrones .

Ventajas de los captadores y colocadores tradicionales

  • Los captadores y establecedores tradicionales permiten un acceso a datos más complicado que el simple acceso a atributos. Por ejemplo, cuando configura un miembro de la clase, a veces necesita un indicador que indique dónde desea forzar esta operación, incluso si algo no parece perfecto. Si bien no es obvio cómo aumentar el acceso directo de un miembro foo.num = num, puede aumentar fácilmente su setter tradicional con un forceparámetro adicional :

    def Foo:
        def set_num(self, num, force=False):
            ...
  • Los captadores y establecedores tradicionales hacen explícito que el acceso de un miembro de la clase es a través de un método. Esto significa:

    • Lo que obtienes como resultado puede no ser lo mismo que lo que se almacena exactamente dentro de esa clase.

    • Incluso si el acceso parece un simple acceso de atributo, el rendimiento puede variar mucho de eso.

    A menos que los usuarios de su clase esperen @propertyesconderse detrás de cada declaración de acceso a atributos, hacer explícitas estas cosas puede ayudar a minimizar las sorpresas de los usuarios de su clase.

  • Como mencionó @NeilenMarais y en esta publicación , ampliar los captadores y definidores tradicionales en subclases es más fácil que extender las propiedades.

  • Los captadores y establecedores tradicionales han sido ampliamente utilizados durante mucho tiempo en diferentes idiomas. Si tienes personas de diferentes orígenes en tu equipo, te resultarán más familiares @property. Además, a medida que su proyecto crezca, si necesita migrar de Python a otro idioma que no lo tenga @property, el uso de captadores y establecedores tradicionales facilitaría la migración.

Advertencias

  • Ni @propertylos captadores y definidores tradicionales hacen que el miembro de la clase sea privado, incluso si usa un guión bajo doble antes de su nombre:

    class Foo:
        def __init__(self):
            self.__num = 0
    
        @property
        def num(self):
            return self.__num
    
        @num.setter
        def num(self, num):
            self.__num = num
    
        def get_num(self):
            return self.__num
    
        def set_num(self, num):
            self.__num = num
    
    foo = Foo()
    print(foo.num)          # output: 0
    print(foo.get_num())    # output: 0
    print(foo._Foo__num)    # output: 0

2

Aquí hay un extracto de "Python eficaz: 90 formas específicas de escribir mejor Python" (libro increíble. Lo recomiendo encarecidamente).

Cosas para recordar

✦ Defina nuevas interfaces de clase utilizando atributos públicos simples y evite definir los métodos setter y getter.

@ Use @property para definir un comportamiento especial cuando se acceda a los atributos en sus objetos, si es necesario.

✦ Siga la regla de menor sorpresa y evite efectos secundarios extraños en sus métodos @property.

✦ Asegúrese de que los métodos @property sean rápidos; Para trabajos lentos o complejos, especialmente que involucran E / S o que causan efectos secundarios, utilice métodos normales en su lugar.

Un uso avanzado pero común de @property es la transición de lo que alguna vez fue un atributo numérico simple en un cálculo sobre la marcha. Esto es extremadamente útil porque le permite migrar todo el uso existente de una clase para tener nuevos comportamientos sin requerir que se reescriba ninguno de los sitios de llamadas (lo cual es especialmente importante si hay un código de llamada que no controla). @property también proporciona una importante solución provisional para mejorar las interfaces a lo largo del tiempo.

Especialmente me gusta @property porque te permite hacer progresos incrementales hacia un mejor modelo de datos con el tiempo.
@property es una herramienta para ayudarlo a resolver los problemas que encontrará en el código del mundo real. No lo uses en exceso. Cuando se encuentre extendiendo repetidamente los métodos @property, probablemente sea hora de refactorizar su clase en lugar de pavimentar aún más el mal diseño de su código.

@ Use @property para dar a los atributos de instancia existentes una nueva funcionalidad

✦ Progrese progresivamente hacia mejores modelos de datos utilizando @property.

✦ Considere refactorizar una clase y todos los sitios de llamadas cuando se encuentre usando @property demasiado.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.