Uso de métodos estáticos en python – mejores prácticas

¿Cuándo y cómo se supone que se usan los métodos estáticos en Python? Ya hemos establecido que utilizar un método de clase como método de fábrica para crear una instancia de un objeto debe evitarse cuando sea posible. En otras palabras, no es una buena práctica usar métodos de clase como un constructor alternativo (vea el método de Fábrica para el objeto python – la mejor práctica ).

Digamos que tengo una clase usada para representar algunos datos de entidad en una base de datos. Imagine que los datos son un objeto dict que contiene nombres de campo y valores de campo y uno de los campos es un número de identificación que hace que los datos sean únicos.

 class Entity(object): def __init__(self, data, db_connection): self._data = data self._db_connection 

Aquí mi método __init__ toma el objeto dict datos de entidad. Digamos que solo tengo un número de ID y quiero crear una instancia de Entity . Primero necesitaré encontrar el rest de los datos, luego crear una instancia de mi objeto Entity . De mi pregunta anterior, establecimos que el uso de un método de clase como método de fábrica probablemente debería evitarse cuando sea posible.

 class Entity(object): @classmethod def from_id(cls, id_number, db_connection): filters = [['id', 'is', id_number]] data = db_connection.find(filters) return cls(data, db_connection) def __init__(self, data, db_connection): self._data = data self._db_connection # Create entity entity = Entity.from_id(id_number, db_connection) 

Lo anterior es un ejemplo de qué no hacer o al menos qué no hacer si hay una alternativa. Ahora me pregunto si editar mi método de clase para que sea más un método de utilidad y menos de un método de fábrica es una solución válida. En otras palabras, el siguiente ejemplo cumple con las mejores prácticas para usar métodos estáticos.

 class Entity(object): @staticmethod def data_from_id(id_number, db_connection): filters = [['id', 'is', id_number]] data = db_connection.find(filters) return data # Create entity data = Entity.data_from_id(id_number, db_connection) entity = Entity(data) 

O tiene más sentido usar una función independiente para encontrar los datos de la entidad a partir de un número de identificación.

 def find_data_from_id(id_number, db_connection): filters = [['id', 'is', id_number]] data = db_connection.find(filters) return data # Create entity. data = find_data_from_id(id_number, db_connection) entity = Entity(data, db_connection) 

Nota: No quiero cambiar mi método __init__ . Anteriormente, la gente ha sugerido que mi método __init__ se vea así __init__(self, data=None, id_number=None) pero podría haber 101 formas diferentes de encontrar los datos de la entidad, por lo que preferiría mantener esa lógica separada hasta cierto punto. ¿Tener sentido?

¿Cuándo y cómo se supone que se usan los métodos estáticos en Python?

La respuesta fácil es: No muy a menudo.

La respuesta uniforme, pero no tan inútil, es: cuando hacen que su código sea más legible.


En primer lugar, vamos a tomar un desvío a la documentación :

Los métodos estáticos en Python son similares a los que se encuentran en Java o C ++. También vea classmethod() para una variante que es útil para crear constructores de clase alternativos.

Entonces, cuando necesitas un método estático en C ++, necesitas un método estático en Python, ¿verdad?

Bueno no.

En Java, no hay funciones, solo métodos, por lo que terminas creando pseudo-clases que son solo paquetes de métodos estáticos. La forma de hacer lo mismo en Python es simplemente usar funciones gratuitas.

Eso es bastante obvio. Sin embargo, es bueno que el estilo de Java sea lo más difícil posible para que una clase apropiada incruste una función, por lo que puede evitar escribir esas pseudo-clases, mientras que hacer lo mismo es malo al estilo de Python. De nuevo, use las funciones gratuitas. Es mucho menos obvio.

C ++ no tiene la misma limitación que Java, pero todos los estilos de C ++ son bastante similares. (Por otro lado, si eres un progtwigdor de “C ++ moderno” que ha internalizado las “funciones gratuitas son parte del lenguaje de la interfaz de una clase, tus instintos para” donde son útiles los métodos estáticos “son probablemente bastante decentes para Python.)


Pero si viene a esto desde los primeros principios, en lugar de otro lenguaje, hay una forma más sencilla de ver las cosas:

Un @staticmethod es básicamente una función global. Si tiene una función foo_module.bar() que sería más legible por alguna razón si se escribiera como foo_module.BazClass.bar() , @staticmethod un @staticmethod . Si no, no lo hagas. Eso es realmente todo lo que hay que hacer. El único problema es crear tus instintos para lo que es más legible para un progtwigdor de Python idiomático.

Y, por supuesto, use un @classmethod cuando necesite acceso a la clase, pero no a la instancia: los constructores alternativos son el caso paradigmático para eso, como lo indican los documentos. Aunque a menudo puede simular un @classmethod con un @staticmethod simplemente haciendo referencia explícita a la clase (especialmente cuando no tiene muchas subclases), no debería hacerlo.


Finalmente, llegando a su pregunta específica:

Si la única razón por la que los clientes necesitan buscar datos por ID es construir una Entity , suena como un detalle de implementación que no debería exponer, y también hace que el código del cliente sea más complejo. Sólo tiene que utilizar un constructor. Si no desea modificar su __init__ (y tiene razón en que hay buenas razones por las que no quiere hacerlo), use un @classmethod como un constructor alternativo: Entity.from_id(id_number, db_connection) .

Por otro lado, si esa búsqueda es algo que es intrínsecamente útil para los clientes en otros casos que no tienen nada que ver con la construcción de la Entity , parece que esto no tiene nada que ver con la clase de Entity (o al menos no más que cualquier otra cosa en el mismo módulo). Por lo tanto, simplemente haz que sea una función gratuita.

La respuesta a la pregunta vinculada específicamente dice esto:

A @classmethod es la forma idiomática de hacer un “constructor alternativo”; hay ejemplos en toda la plantilla: itertools.chain.from_iterable, datetime.datetime.fromordinal, etc.

Así que no sé cómo surgió la idea de que usar un método de clase es inherentemente malo. De hecho, me gusta la idea de usar un método de clase en su situación específica, ya que facilita el seguimiento del código y el uso de la API.

La alternativa sería usar los argumentos del constructor por defecto de esta manera:

 class Entity(object): def __init__(self, id, db_connection, data=None): self.id = id self.db_connection = db_connection if data is None: self.data = self.from_id(id, db_connection) else: self.data = data def from_id(cls, id_number, db_connection): filters = [['id', 'is', id_number]] return db_connection.find(filters) 

Sin embargo, prefiero la versión classmethod que escribiste originalmente. Sobre todo porque los data son bastante ambiguos.

Tu primer ejemplo tiene más sentido para mí: Entity.from_id es bastante Entity.from_id y claro.

Evita el uso de data en los siguientes dos ejemplos, que no describen lo que se está devolviendo; Los data se utilizan para construir una Entity . Si quisiera ser específico acerca de los data se utilizan para construir la Entity , entonces podría nombrar a su método como Entity.with_data_for_id o la función equivalente entity_with_data_for_id .

Usar un verbo como find también puede ser bastante confuso, ya que no da ninguna indicación del valor de retorno. ¿Qué se supone que hace la función cuando encuentra los datos? (Sí, me doy cuenta de que str tiene un método de find ; ¿no sería mejor index_of ? Pero también está el index …) Me recuerda el clásico:

encontrar x

Siempre trato de pensar lo que un nombre indicaría a alguien con (a) desconocimiento del sistema, y ​​(b) conocimiento de otras partes del sistema, ¡para no decir que siempre tengo éxito!

Aquí hay un caso de uso decente para @staticmethod.

He estado trabajando en un juego como proyecto paralelo. Parte de ese juego incluye dados rodantes basados ​​en estadísticas, y la posibilidad de recoger elementos y efectos que impactan las estadísticas de tu personaje (para bien o para mal).

Cuando lanzo los dados en mi juego, básicamente necesito decir … tomar las estadísticas del personaje base y luego agregar las estadísticas de inventario y efecto a esta gran cifra.

No puede tomar estos objetos abstractos y agregarlos sin indicar al progtwig cómo hacerlo. No estoy haciendo nada a nivel de clase o instancia tampoco. No quería definir la función en algún módulo global. La última mejor opción fue utilizar un método estático para sumr estadísticas. Simplemente tiene más sentido de esta manera.

 class Stats: attribs = ['strength', 'speed', 'intellect', 'tenacity'] def __init__(self, strength=0, speed=0, intellect=0, tenacity=0 ): self.strength = int(strength) self.speed = int(speed) self.intellect = int(intellect) self.tenacity = int(tenacity) # combine adds stats objects together and returns a single stats object @staticmethod def combine(*args: 'Stats'): assert all(isinstance(arg, Stats) for arg in args) return_stats = Stats() for stat in Stats.attribs: for _ in args: setattr(return_stats, stat, getattr(return_stats, stat) + getattr(_, stat)) return (return_stats) 

Lo que haría que las combinaciones de estadísticas funcionen así.

 a = Stats(strength=3, intellect=3) b = Stats(strength=1, intellect=-1) c = Stats(tenacity=5) print(Stats.combine(a, b, c).__dict__) 

{‘fuerza’: 4, ‘velocidad’: 0, ‘intelecto’: 2, ‘tenacidad’: 5}