Tengo algunos cálculos sobre datos biológicos. Cada función calcula los valores total, promedio, mínimo, máximo para una lista de objetos. La idea es que tengo muchas listas diferentes, cada una es para un tipo de objeto diferente. ¡No quiero repetir mi código para cada función simplemente cambiando la línea "for" y la llamada del método del objeto!

Por ejemplo:

Función de volumen:

def calculate_volume(self):
    total = 0
    min = sys.maxint
    max = -1
    compartments_counter = 0

    for n in self.nodes:

        compartments_counter += 1
        current = n.get_compartment_volume()
        if min > current:
            min = current
        if max < current:
            max = current

        total += current

    avg = float(total) / compartments_counter
    return total, avg, min, max

Función de contracción:

def get_contraction(self):
    total = 0
    min = sys.maxint
    max = -1
    branches_count = self.branches.__len__()

    for branch in self.branches:

        current = branch.get_contraction()
        if min > current:
            min = current
        if max < current:
            max = current

        total += current

    avg = float(total) / branches_count

    return total, avg, min, max

Ambas funciones se ven casi iguales, ¡solo una pequeña modificación!

Sé que puedo usar la suma, el mínimo, el máximo, etc., pero cuando los aplico para mis valores, toman más tiempo que hacerlo en el ciclo porque no se pueden llamar a la vez.

Solo quiero saber si es la forma correcta de escribir una función para cada cálculo. (es decir, una forma profesional?) O tal vez pueda escribir una función y pasar la lista, el tipo de objeto y el método para llamar.

0
The Maestro 10 may. 2016 a las 16:43

5 respuestas

La mejor respuesta

Es difícil de decir sin ver el resto del código, pero desde la vista limitada dada, creo que no debería tener estas funciones en los métodos. Tampoco entiendo realmente su razonamiento para no usar los builtins ("¿no se pueden llamar de inmediato?"). Si estás insinuando que implementar los 4 métodos estadísticos en una sola pasada en python es más rápido que 4 pasadas en la construcción (C), entonces me temo que tienes una suposición muy errónea.

Dicho esto, aquí está mi opinión sobre el problema:

def get_stats(l):
    s = sum(l)
    return (
        s,
        float(s) / len(l),
        min(l),
        max(l))

# then create numeric lists from your data and send 'em through:

node_volumes = [n.get_compartment_volume() for n in self.nodes]
branches = [b.get_contraction() for b in self.branches]

# ...

total_1, avg_1, min_1, max_1 = get_stats(node_volumes)
total_2, avg_2, min_2, max_2 = get_stats(branches)

EDITAR

Algunos puntos de referencia para demostrar que construir es ganar:

MINE.py

import sys

def get_stats(l):
    s = sum(l)
    return (
        s,
        float(s) / len(l),
        min(l),
        max(l)
    )


branches = [i for i in xrange(10000000)]

print get_stats(branches)

Versus YOURS.py

import sys

branches = [i for i in xrange(10000000)]

total = 0
min = sys.maxint
max = -1
branches_count = branches.__len__()

for current in branches:
    if min > current:
        min = current
    if max < current:
        max = current

    total += current

avg = float(total) / branches_count

print total, avg, min, max

Y finalmente con algunos temporizadores:

smassey@hacklabs:/tmp $ time python mine.py 
(49999995000000, 4999999.5, 0, 9999999)

real    0m1.225s
user    0m0.996s
sys 0m0.228s
smassey@hacklabs:/tmp $ time python yours.py 
49999995000000 4999999.5 0 9999999

real    0m2.369s
user    0m2.180s
sys 0m0.180s

Salud

2
smassey 10 may. 2016 a las 14:23

O tal vez pueda escribir una función y pasar la lista, el tipo de objeto y el método para llamar.

Aunque definitivamente puede pasar una función a otra, y en realidad es una forma muy común de evitar repetirse, en este caso no puede porque cada objeto en la lista tiene su propio método. Entonces, en cambio, paso el nombre de la función como una cadena, luego uso getattr para obtener el método invocable real del objeto. También tenga en cuenta que estoy usando len() en lugar de llamar explícitamente __len()__.

def handle_list(items_list, func_to_call):
    total = 0
    min = sys.maxint
    max = -1
    count = len(items_list)

    for item in items_list:

        current = getattr(item, func_to_call)()
        if min > current:
            min = current
        if max < current:
            max = current

        total += current

    avg = float(total) / count

    return total, avg, min, max
1
DeepSpace 11 may. 2016 a las 06:27

Primero, observe que si bien es probable que sea más eficiente llamar a len(self.branches) (no llame a __len__ directamente), es más general aumentar un contador en el bucle como lo haces con calculate_volume. Con ese cambio, puede refactorizar de la siguiente manera:

def _stats(self, iterable, get_current):
    total = 0.0
    min_value = None  # Slightly better
    max_value = -1
    counter = 0
    for n in iterable:
        counter += 1
        current = get_current(n)
        if min_value is None or min_value > current:
            min_value = current
        if max_value < current:
            max_value = current
        total += current
    avg = total / denom
    return total, avg, min_value, max_value

Ahora, cada uno de los dos se puede implementar en términos de _stats:

import operator

def calculate_volume(self):
    return self._stats(self.nodes, operator.methodcaller('get_compartment_volume'))

def get_contraction(self):
    return self.refactor(self.branches, operator.methodcaller('get_contraction'))

methodcaller proporciona una función f tal que f('method_name')(x) es equivalente a x.method_name(), que le permite descifrar la llamada al método.

1
chepner 10 may. 2016 a las 14:13

Puede usar getattr( instance, methodname) para escribir una función para procesar listas de objetos arbitrarios.

def averager( things, methodname):
    count,total,min,max = 0,0,sys.maxint,-1
    for thing in things:
         current = getattr(thing, methodname)()  

         count += 1
         if min > current:
             min = current
         if max < current:
             max = current
         total += current

    avg = float(total) / branches_count
    return total, avg, min, max

Luego, dentro de las definiciones de clase, solo necesitas

    def calculate_volume(self): return averager( self.nodes, 'get_compartment_volume')

    def get_contraction(self): return averager( self.branches, 'get_contraction' )
1
nigel222 10 may. 2016 a las 14:19

Escribir una función que tome otra función que sepa cómo extraer valores de la lista es muy común. De hecho, min y max tienen argumentos a tal efecto.

Eg.

items = [1, 0, -2]
print(max(items, key=abs)) # prints -2

Por lo tanto, es perfectamente aceptable escribir su propia función que haga lo mismo. Normalmente, simplemente crearía una nueva lista de todos los valores que desea examinar y luego trabajaría con eso (por ejemplo, [branch.get_contraction() for branch in branches]). Pero quizás el espacio sea un problema para usted, así que aquí hay un ejemplo usando un generador.

def sum_avg_min_max(iterable, key=None):
    if key is not None:
        iter_ = (key(item) for item in iterable)
    else:
        # if there is no key, just use the iterable itself
        iter_ = iter(iterable)

    try:
        # We don't know sensible starting values for total, min or max. So use 
        # the first value.
        total = min_ = max_ = next(iter_)
    except StopIteration:
        # can't have a min or max if we have no items in the iterable...
        raise ValueError("empty iterable") from None
    count = 1

    for item in iter_:
        total += item
        min_ = min(min_, item)
        max_ = max(max_, item)
        count += 1

    return total, float(total) / count, min_, max_

Entonces puedes usarlo así:

class MyClass(int):
    def square(self):
        return self ** 2

items = [MyClass(i) for i in range(10)]
print(sum_avg_min_max(items, key=MyClass.square)) # prints (285, 28.5, 0, 81)

Esto funciona porque cuando obtiene un método de instancia de la clase, le da a su función subyacente (sin self enlazado). Entonces podemos usarlo como la clave. p.ej.

str.upper("hello world") == "hello world".upper()

Con un ejemplo más concreto (suponiendo que los elementos en branches son instancias de Branch):

def get_contraction(self):
    result = sum_avg_min_max(self.branches, key=Branch.get_contraction)
    return result
1
Dunes 10 may. 2016 a las 15:00