Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python - using 'partial' for late-binding problems not amenable to a lambda - costs vs benefits?

Note: I'm asking if there's One Pythonic Way to do this (using default args seems less Pythonic than using partial) and if there are significant limitations to either method (the "cost" - I wouldn't expect timing to differ significantly but perhaps there are other limitations I'm not seeing that tilt the balance towards one methodology versus the other).

I'm trying to understand the costs of using 'partial' in late-binding situations where a lambda is not feasible. I've created some example code based on this guide to exemplify this.

The following doesn't work as intended due to late binding:

def create_thingies():
    thingies = []
    for i in range(1,6):
        def thingy(x):
            print("Some output", i)
            return i ** (x * i)
        thingies.append(thingy)
    return thingies

results=[]
for thingy in create_thingies():
    results.append(thingy(2))
print(results)

Output:

Some output 5
Some output 5
Some output 5
Some output 5
Some output 5
[9765625, 9765625, 9765625, 9765625, 9765625]

Using 'partial' we avoid that problem, but at what cost?

from functools import partial
def create_thingies():
    thingies = []
    for i in range(1,6):
        def thingy(i, x):
            print("Some output", i)
            return i ** (x * i)
        thingies.append(partial(thingy, i))
    return thingies

results=[]
for thingy in create_thingies():
    results.append(thingy(2))
print(results)

Output:

Some output 1
Some output 2
Some output 3
Some output 4
Some output 5
[1, 16, 729, 65536, 9765625]

I've seen much discussion about lambda vs partial here, but in cases where a lambda wouldn't work well (a very complex function) if at all (function with more than expressions) is partial the way to go or is there a better way short of coercing this into a lambda expression?

like image 778
MartyMacGyver Avatar asked Apr 21 '26 12:04

MartyMacGyver


1 Answers

Using partial, there is no need to define thingy once for each value of i, since thingy does not use any free/global variables, but just its parameters.

from functools import partial

def thingy(i, x):
    print("Some output", i)
    return i ** (x * i)

thingies = [partial(thingy, i) for i in range(1,6)]
results = [th(2) for th in thingies]
print(results)

As for cost, you should profile to see if the performance is acceptable.


Here's a quick test to compare 3 options:

import timeit

# The fastest: define a function using a default parameter value
print timeit.timeit('results = [ th(2) for th in create_thingies()]', '''
def create_thingies():
    thingies = []
    for i in range(1,6):
        def thingy(x,i=i):
            #print("Some output", i)
            return i ** (x * i)
        thingies.append(thingy)
    return thingies
''')

# The slowest, but IMO the easiest to read.
print timeit.timeit('results = [ th(2) for th in create_thingies()]', '''
def create_thingies():
    from functools import partial
    def thingy(i,x):
        #print("Some output", i)
        return i ** (x * i)
    return [partial(thingy, i) for i in range(1,6)]
''')

# Only a little slower than the first
print timeit.timeit('results = [ th(2) for th in create_thingies()]', '''
def create_thingies():
    def make_thingy(i):
        def thingy(x):
            #print("Some output", i)
            return i ** (x * i)
        return thingy
    thingies = [make_thingy(i) for i in range(1,6)]
    return thingies
''')
like image 90
chepner Avatar answered Apr 24 '26 00:04

chepner