Dive into some features of Python function programming

  • 2020-05-09 18:45:28
  • OfStack

The binding

Careful readers may remember the limitations I pointed out in the functional technique in part 1. In Python in particular, you cannot avoid the binding of names representing function expressions. In FP, the name is often understood as an abbreviation of a longer expression, but the 1 statement implies that "the same value is always found for the 1 expression." If the name of the tag is rebound, the 1 hint is not valid. For example, let's define some quick expressions to be used in functional programming, such as:
Listing 1. The following rebind of the Python FP section will cause a failure


>>> car = 
    
    lambda
    
     lst: lst[0]
>>> cdr = 
    
    lambda
    
     lst: lst[1:]
>>> sum2 = 
    
    lambda
    
     lst: car(lst)+car(cdr(lst))
>>> sum2(range(10))
1
>>> car = 
    
    lambda
    
     lst: lst[2]
>>> sum2(range(10))
5

Unfortunately, the exact same expression, sum2(range(10)), finds two different values at two points in the program, even though the expression itself does not use any mutable variables in its arguments.

Fortunately, the functional module provides a class called Bindings (proposed to Keller) to prevent such a rebelling (Python does not, at least occasionally, stop a programmer who wants to unbind). Using Bindings, however, requires some extra syntax, so accidents are less likely to happen. In the example of the functional module, Keller names the Bindings instance let (I assume after the let keyword in the ML family of languages). For example, we would do this:
Listing 2. The Python FP section with a secure rebind


>>> 
    
    from
    
     functional 
    
    import
    
     *
>>> let = Bindings()
>>> let.car = 
    
    lambda
    
     lst: lst[0]
>>> let.car = 
    
    lambda
    
     lst: lst[2]
Traceback (innermost last):
 File "<stdin>", line 1, 
    
    in
    
     ?
 File "d:\tools\functional.py", line 976, 
    
    in
    
     __setattr__
  
    
    raise
    
     BindingError, "Binding '%s' cannot be modified." % name
functional.BindingError: Binding 'car' cannot be modified.
>>> car(range(10))
0

Obviously, real programs have to do some setup to catch "binding errors," and they are thrown to avoid class 1 problems.

Starting with Bindings 1, functional provides the namespace function to fetch a namespace (actually a dictionary) from an Bindings instance. This is very easy to do if you want to evaluate an expression in the (immutable) namespace defined in Bindings. The eval() function of Python allows operations in namespaces. Let's figure it out with an example:
Listing 3. The Python FP section using immutable namespaces


>>> let = Bindings()   
    
    # "Real world" function names
>>> let.r10 = range(10)
>>> let.car = 
    
    lambda
    
     lst: lst[0]
>>> let.cdr = 
    
    lambda
    
     lst: lst[1:]
>>> eval('car(r10)+car(cdr(r10))', namespace(let))
>>> inv = Bindings()   
    
    # "Inverted list" function names
>>> inv.r10 = let.r10
>>> inv.car = 
    
    lambda
    
     lst: lst[-1]
>>> inv.cdr = 
    
    lambda
    
     lst: lst[:-1]
>>> eval('car(r10)+car(cdr(r10))', namespace(inv))
17

closure

An interesting concept in FP is closures. In fact, closures are interesting to many developers, even in functionless languages such as Perl and Ruby. Also, Python 2.1 is currently looking to add lexical scoping, which will provide most of the functionality of closures.

What is a closure? Steve Majewski recently provided a good description of this 1 concept in the Python newsgroup:

      objects are data that comes with the procedure... Closures are procedures that attach data.

Closures are like Jekyll for FP for Hyde for OOP (roles may also be swapped). Closures are similar to the object example and are a way to encapsulate a large amount of data and functionality in one place.

Let's go back to where we were before to see what objects and closures solve, and to see how the problem could be solved without them. The result returned by a function is often determined by the context used in its computation. The most common -- and perhaps the most obvious -- way to specify context is to pass certain parameters to a function, telling it what values to handle. But sometimes there is a fundamental difference between the "background" and "foreground" arguments -- the difference between what the function is doing at this particular moment and what the function is "configuring" for multiple potential calls.

When focusing on the foreground, there are many ways to deal with the background. One is a simple "bullet bite" method that passes every parameter the function needs on every call. This method usually passes a number of values (or structures with multiple members) in the call chain whenever a value is likely to be needed. Here's a small example:
Listing 4. Shows the Python part of the cargo variable


>>> 
    
    defa
    
    (n):
...   add7 = b(n)
...   
    
    return
    
     add7
...
>>> 
    
    defb
    
    (n):
...   i = 7
...   j = c(i,n)
...   
    
    return
    
     j
...
>>> 
    
    defc
    
    (i,n):
...   
    
    return
    
     i+n
...
>>> a(10)   
    
    # Pass cargo value for use downstream
17

In b() of the cargo example, n has no role other than to pass to c(). Another method USES global variables:
Listing 5. Shows the Python part of the global variable


>>> N = 10
>>> 
    
    defaddN
    
    (i):
...   
    
    global
    
     N
...   
    
    return
    
     i+N
...
>>> addN(7)  
    
    # Add global N to argument
17
>>> N = 20
>>> addN(6)  
    
    # Add global N to argument
26

 The global variable  N  Call in any wish  addN()  ", but there is no need to explicitly pass the global background "context". On the other 1 A more  Python  The dedicated technology is going to be 1 Three variables are "frozen" in at the time of definition 1 Functions with default parameters: 
 listing  6.  That shows the frozen variable  Python  Part of the 

>>> N = 10
>>> 
    
    defaddN
    
    (i, n=N):
...   
    
    return
    
     i+n
...
>>> addN(5)  
    
    # Add 10
15
>>> N = 20
>>> addN(6)  
    
    # Add 10 (current N doesn't matter)
16

A frozen variable is essentially a closure. Some data is "attached" to the addN() function. For full closures, when addN() is defined, all the data will be available at call time. However, in this example (or many more robust examples), using the default parameters is simply enough. Variables that addN() has never used do not affect its calculations.

Next, let's look at an OOP method that is closer to the real problem. The timing of the year reminded me of those "meet-and-greet" style tax procedures that collect all kinds of data -- not necessarily in a particular order -- and end up using all the data to calculate. Let's create a simple version:
Listing 7. Python style tax calculation class/example


class
    
     TaxCalc:
  
    
    deftaxdue
    
    (self):
    
    
    return
    
     (self.income-self.deduct)*self.rate
taxclass = TaxCalc()
taxclass.income = 50000
taxclass.rate = 0.30
taxclass.deduct = 10000
    
    print
    
     "Pythonic OOP taxes due =", taxclass.taxdue()

In the TaxCalc class (or example), you can collect some data -- in any order -- and once you have all the elements you need, you can call the 1 object's methods to complete the computation of the 1 mass of data. All 1 cuts are in the instance, and different examples carry different data. The possibility to create multiple examples and the data that distinguishes them cannot exist in a "global variable" or "frozen variable" method. The cargo method handles this, but for the extended example, we see that it may be necessary to start passing various values. Now that we've covered this, it's also interesting to note how the OPP style of messaging is handled (Smalltalk or Self are similar, as are some of the OOP xBase variables I use) :
Listing 8. Tax calculation for Smalltalk style (Python)


class
    
     TaxCalc:
  
    
    deftaxdue
    
    (self):
    
    
    return
    
     (self.income-self.deduct)*self.rate
  
    
    defsetIncome
    
    (self,income):
    self.income = income
    
    
    return
    
     self
  
    
    defsetDeduct
    
    (self,deduct):
    self.deduct = deduct
    
    
    return
    
     self
  
    
    defsetRate
    
    (self,rate):
    self.rate = rate
    
    
    return
    
     self
    
    print
    
     "Smalltalk-style taxes due =", \
   TaxCalc().setIncome(50000).setRate(0.30).setDeduct(10000).taxdue()

Returning self with each "setter" enables us to view "existing" things as the result of each method application. This has many interesting similarities to the FP closure method.

With the Xoltar toolkit, we can create complete closures with the desired merging data and function properties, while also allowing multi-segment closures (nee objects) to contain different packages:
Listing 9. Tax calculation in the Python function style


from
    
     functional 
    
    import
    
     *
taxdue    = 
    
    lambda
    
    : (income-deduct)*rate
incomeClosure = 
    
    lambda
    
     income,taxdue: closure(taxdue)
deductClosure = 
    
    lambda
    
     deduct,taxdue: closure(taxdue)
rateClosure  = 
    
    lambda
    
     rate,taxdue: closure(taxdue)
taxFP = taxdue
taxFP = incomeClosure(50000,taxFP)
taxFP = rateClosure(0.30,taxFP)
taxFP = deductClosure(10000,taxFP)
    
    print
    
     "Functional taxes due =",taxFP()
    
    print
    
     "Lisp-style taxes due =", \
   incomeClosure(50000,
     rateClosure(0.30,
       deductClosure(10000, taxdue)))()

Each of the closures we defined takes any values defined within the scope of the function, and then binds those values to the global scope of the function object. However, the global scope of the function does not have to look the same as the global scope of the actual module, nor is it the same as the "global" scope of the different closures. Closures simply "carry data".

In the example, we used some special functions to put specific bindings in the closure range (income, deduct, rate). It's also easy to modify the design to put any bindings in scope. We can also use slightly different functional styles in the example, just for fun. The first successfully binds the added value into the scope of the closure; Make taxFP mutable, and these "join the closure" lines can appear in any order. However, if you want to use an immutable name such as tax_with_Income, you must arrange the binding lines in order of 1, and then pass the previous binding to the next one. However, once the necessary 1 cut is bound to the closure, we call the "seeded" function.

The second style looks more like Lisp (more like parentheses for me). If aesthetics aside, two interesting things happened in the second style. The first is that name binding is completely avoided. The second style is a single 1 expression without a statement (see part 1 for why this is problematic).

Another interesting example of the use of "Lisp style" closures is how similar they are to the "Smalltalk style" messaging methods mentioned above. Both accumulate values and call the taxdue() function/method (both will report an error in these original versions if there is no correct data). The "Smalltalk style" passes objects between each step, while the "Lisp style" passes one successive step. But at a deeper level, this is mostly true of functions and object-oriented programming.


Related articles: