GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing python-course.eu — 11. Currying in Python | Advanced | python-course.eu 11. Currying in PythonBy Bernd Klein. Last modified: 08 Mar 2024. On this page ➤ General IdeaIn mathematics and computer science, currying is the technique of breaking down the evaluation of a function that takes multiple arguments into evaluating a sequence of single-argument functions. Currying is also considered to be a design pattern. Currying is also used in theoretical computer science, because it is often easier to transform multiple argument models into single argument models. Live Python training See our Python training courses Origin of the NameThe "Curry" in "Currying" has nothing to do with the spicy curry powder, even though it can make your Python code more spicy. The name is a reference to the logician and mathematician Haskell Brooks Curry, and he is quoted in "THE KLEENE SYMPOSIUM", (Proceedings of the Symposium held June 18-24, 1978 at Madison, Wisconsin. U.S.A.):
This is why there is also - though seldomly - the name "Schönfinkelisation" being used. There are even further roots of the concept going back to the end of the 19th century and the mathematician Gottlob Frege. Currying FunctionsCurrying is the technique of breaking down the evaluation of a function that takes multiple arguments into evaluating a sequence of single-argument functions. In mathematical notation it looks like this: If we have a function $f$ which takes $n$ arguments, we can 'replace' it by a composition of $n$ functions $f_1, f_2, \ldots f_n$ where each takes only one argument: $$let x = f(a_1, a_2, a_3)$$
we will get the same value x as if we call: $$f_2 = f_1(a_1)$$
$$f_3 = f_2(a_2)$$
and $$x = f_3(a_3)$$
An example implemented in Python could look like this: def f(a1, a2, a3): return a1 * a2 * a3 def f1(a1): def f2(a2): def f3(a3): return f(a1, a2, a3) return f3 return f2 for i in range(1, 10): print(f(i, i+1, i+2), f1(i)(i+1)(i+2)) OUTPUT:6 6 24 24 60 60 120 120 210 210 336 336 504 504 720 720 990 990 Example BMIWe illustrate this with the function BMI, which we had used in our chapter on functions. We write a curried version of BMI: def BMI(weight, height): return weight / height**2 def bmi_curried(height): def bmi_weight(weight): return BMI(weight, height) return bmi_weight bmi_curried(1.76)(72) OUTPUT:23.243801652892564 Using partialThe function We demonstrate the way of working in the following examples: from functools import partial def f(a1, a2): return a1 * a2 partial(f, 3)(4) OUTPUT:12 from functools import partial def f(a1, a2, a3): return a1 * a2 * a3 partial(f, 3, 2)(4) OUTPUT:24 partial(f, 3)(2, 4) OUTPUT:24 partial(partial(f, 3), 2)(4) OUTPUT:24 partial(f, 3)(2, 4) OUTPUT:24 Live Python training See our Python training courses Using partialThe function We demonstrate the way of working in the following examples: Decorator for curryingdef f(x, y, z): return x * y * z The following Python function
The def curry(func): def curried(*args): print(f'{args}') if len(args) == func.__code__.co_argcount: return func(*args) else: return lambda x: curried(*(args + (x,))) return curried @curry def prod3(x, y, z): return x + y + z prod3(3)(4)(5) OUTPUT:(3,) (3, 4) (3, 4, 5) 12 Another exaple for using our def curry(func): def curried(*args): if len(args) == func.__code__.co_argcount: return func(*args) else: return lambda x: curried(*(args + (x,))) return curried Live Python training See our Python training courses Another Example for Currying
Let's consider a function for calculating the total cost of a shopping cart, with the ability to apply various discounts at different stages. This example will use multiple curried functions to apply item prices, quantities, and discounts incrementally. First, we define a curried function for calculating the cost of individual items: @curry def calculate_item_cost(price, quantity): return price * quantity Next, we create a function to apply a percentage discount to the cost. This function takes a percentage and returns a curried function: @curry def subtract_percentage(discount_percentage, cost): #print(f"{cost=}, {discount_percentage=}") discount_amount = (cost * discount_percentage) / 100 return cost - discount_amount For the following, it might be useful to be familiar with to simple forms of price reduction in the business world: Rebates and discounts are two distinct pricing strategies that businesses use to incentivize customers. The main distinction between discounts and rebates lies in the timing of the price reduction. Discounts provide immediate cost savings at the point of sale, while rebates offer savings after a purchase has been made, contingent on meeting specific criteria. Both strategies can be effective in influencing customer behavior and boosting sales, but they are suited to different business scenarios and objectives. Now, let's use these curried functions to calculate the total cost of a customers order list in the end: # Calculate the cost of individual items item1 = calculate_item_cost(10)(2) # Item 1 costs € 10 with a quantity of 2 item2 = calculate_item_cost(5)(3) # Item 2 costs € 5 with a quantity of 3 print(f"{item1=}, {item2=}") # Apply percentage discounts calculate_discounted_price = subtract_percentage(12) calculate_rebated_price = subtract_percentage(3) # Calculate the total cost with discounts total_cost = calculate_rebated_price(calculate_discounted_price(item1 + item2)) print(f"Total cost: €{total_cost:.2f}") OUTPUT:item1=20, item2=15 Total cost: €29.88 We could have done the calculation directly on each item: subtract_percentage(3)(subtract_percentage(12)(calculate_item_cost(10)(2))) OUTPUT:17.072000000000003 In the previous example we used the same rebate and discount on each item. Usually, they differ from product to product. Let's assume that we have the following dictionary as a shopping list for items. Each item consists of a tuple with (quantity, price, discount, rebate). We can do the summation in a clean and readable way: shopping_list = [(2, 10, 12, 3), (3, 5, 12, 3)] total = 0 sub_perc = subtract_percentage # just a shorter name for quantity, price, discount, rebate in shopping_list: subtotal = sub_perc(rebate)(sub_perc(discount)((calculate_item_cost(price)(quantity)))) #print(subtotal) total += subtotal print(f"Total cost: €{total:.2f}") OUTPUT:Total cost: €29.88 Currying Function with an Arbitrary Number of ArgumentsOne interesting question remains: How to curry a function across an arbitrary and unknown number of parameters? We can use a nested function to make it possible to "curry" (accumulate) the arguments. We will need a way to tell the function calculate and return the value. If the funtions is called with arguments, these will be curried, as we have said. What if we call the function without any arguments? Right, this is a fantastic way to tell the function that we finally want to the the result. We can also clean the lists with the accumulated values: def arimean(*args): return sum(args) / len(args) def curry(func): # to keep the name of the curried function: curry.__curried_func_name__ = func.__name__ f_args, f_kwargs = [], {} def f(*args, **kwargs): nonlocal f_args, f_kwargs if args or kwargs: f_args += args f_kwargs.update(kwargs) return f else: result = func(*f_args, *f_kwargs) f_args, f_kwargs = [], {} return result return f curried_arimean = curry(arimean) curried_arimean(2)(5)(9)(5) OUTPUT:<function __main__.curry.<locals>.f(*args, **kwargs)> We have to call the function again with an empty argument, because otherwise it doesn't know that it's at the end. this is not "proper" currying, but it works: curried_arimean(2)(5)(9)(5)() OUTPUT:5.25 arimean(2, 5, 9, 5) OUTPUT:5.25 Including some prints might help to understand what's going on: def arimean(*args): return sum(args) / len(args) def curry(func): # to keep the name of the curried function: curry.__curried_func_name__ = func.__name__ f_args, f_kwargs = [], {} def f(*args, **kwargs): nonlocal f_args, f_kwargs if args or kwargs: print("Calling curried function with:") print("args: ", args, "kwargs: ", kwargs) f_args += args f_kwargs.update(kwargs) print("Currying the values:") print("f_args: ", f_args) print("f_kwargs:", f_kwargs) return f else: print("Calling " + curry.__curried_func_name__ + " with:") print(f_args, f_kwargs) result = func(*f_args, *f_kwargs) f_args, f_kwargs = [], {} return result return f curried_arimean = curry(arimean) curried_arimean(2)(5)(9)(7) # it will keep on currying: curried_arimean(5, 9) print(curried_arimean()) OUTPUT:Calling curried function with:
args: (2,) kwargs: {}
Currying the values:
f_args: [2]
f_kwargs: {}
Calling curried function with:
args: (5,) kwargs: {}
Currying the values:
f_args: [2, 5]
f_kwargs: {}
Calling curried function with:
args: (9,) kwargs: {}
Currying the values:
f_args: [2, 5, 9]
f_kwargs: {}
Calling curried function with:
args: (7,) kwargs: {}
Currying the values:
f_args: [2, 5, 9, 7]
f_kwargs: {}
Calling curried function with:
args: (5, 9) kwargs: {}
Currying the values:
f_args: [2, 5, 9, 7, 5, 9]
f_kwargs: {}
Calling arimean with:
[2, 5, 9, 7, 5, 9] {}
6.166666666666667
def arimean(*args): return sum(args) / len(args) def curry(func): f_args, f_kwargs = [], {} def f(*args, **kwargs): nonlocal f_args, f_kwargs if args or kwargs: f_args += args f_kwargs.update(kwargs) return f else: result = func(*f_args, **f_kwargs) f_args, f_kwargs = [], {} return result return f curried_arimean = curry(arimean) result = curried_arimean(2)(5)(9)(7)(5, 9)() print("Result:", result) We can write it also as a def arimean(*args): return sum(args) / len(args) class Curry: def __init__(self, func, *args): self.func = func self.args = args def __call__(self, *args): print(args, self.args) if not args: return self.func(*self.args) return Curry(self.func, *(self.args + args)) curried_arimean = Curry(arimean) result = curried_arimean(2)(5)(9)(7)(5, 9) print("Result:", result, type(result)) OUTPUT:(2,) () (5,) (2,) (9,) (2, 5) (7,) (2, 5, 9) (5, 9) (2, 5, 9, 7) Result: <__main__.Curry object at 0x7fdfe3c103d0> <class '__main__.Curry'> c = curried_arimean(2)(5)(9)(7)(5)(9)() c OUTPUT:(2,) () (5,) (2,) (9,) (2, 5) (7,) (2, 5, 9) (5,) (2, 5, 9, 7) (9,) (2, 5, 9, 7, 5) () (2, 5, 9, 7, 5, 9) 6.166666666666667 |
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |