2018年5月25日 星期五

[ FP In Python ] Ch2. Callables

Preface 
The emphasis in functional programming is, somewhat tautologously, on calling functions. Python actually gives us several different ways to create functions, or at least something very function-like (i.e., that can be called). They are: 
• Regular functions created with def and given a name at definition time
• Anonymous functions created with lambda
• Instances of classes that define a __call__ method
• Closures returned by function factories
• Static methods of instances, either via the @staticmethod decorator or via the class __dict__
• Generator functions

This list is probably not exhaustive, but it gives a sense of the numerous slightly different ways one can create something callable. Of course, a plain method of a class instance is also a callable, but one generally uses those where the emphasis is on accessing and modifying mutable state. Python is a multiple paradigm language, but it has an emphasis on object-oriented styles. When one defines a class, it is generally to generate instances meant as containers for data that change as one calls methods of the class. This style is in some ways opposite to a functional programming approach, which emphasizes immutability and pure functions

Any method that accesses the state of an instance (in any degreeto determine what result to return is not a pure function. Of course, all the other types of callables we discuss also allow reliance on state in various ways. The author of this report has long pondered whether he could use some dark magic within Python explicitly to declare a function as pure—say by decorating it with a hypothetical @purefunction decorator that would raise an exception if the function can have side effects—but consensus seems to be that it would be impossible to guard against every edge case in Python’s internal machinery. 

The advantage of a pure function and side-effect-free code is that it is generally easier to debug and test. Callables that freely intersperse statefulness with their returned results cannot be examined independently of their running context to see how they behave, at least not entirely so. For example, a unit test (using doctest or unittest, or some third-party testing framework such as py.test or nose) might succeed in one context but fail when identical calls are made within a running, stateful program. Of course, at the very least, any program that does anything must have some kind of output (whether to console, a file, a database, over the network, or whateverin it to do anything useful, so side effects cannot be entirely eliminated, only isolated to a degree when thinking in functional programming terms

Named Functions and Lambdas 
The most obvious ways to create callables in Python are, in definite order of obviousness, named functions and lambdas. The only in-principle difference between them is simply whether they have a .__qualname__ attribute, since both can very well be bound to one or more names. In most cases, lambda expressions are used within Python only for callbacks and other uses where a simple action is inlined into a function call. But as we have shown in this report, flow control in general can be incorporated into single-expression lambdas if we really want. Let’s define a simple example to illustrate: 
  1. >>> def hello1(name):  
  2. ...     print("Hello {}".format(name))  
  3. ...  
  4. >>> hello2 = lambda name: print("Hello {}".format(name))  
  5. >>> hello1('David')  
  6. Hello David  
  7. >>> hello2('Davoid')  
  8. Hello Davoid  
  9. >>> hello1.__qualname__  
  10. 'hello1'  
  11. >>> hello2.__qualname__  
  12. ''  
  13. >>> hello3 = hello2  
  14. >>> hello3.__qualname__  
  15. ''  
  16. >>> hello3.__qualname__ = 'hello3'  
  17. >>> hello3.__qualname__  
  18. 'hello3'  
One of the reasons that functions are useful is that they isolate state lexically, and avoid contamination of enclosing namespaces. This is a limited form of nonmutability in that (by default) nothing you do within a function will bind state variables outside the function. Of course, this guarantee is very limited in that both the global and nonlocal statements explicitly allow state to “leak out” of a function. Moreover, many data types are themselves mutable, so if they are passed into a function that function might change their contents. Furthermore, doing I/O can also change the “state of the world” and hence alter results of functions (e.g., by changing the contents of a file or a database that is itself read elsewhere). 

Notwithstanding all the caveats and limits mentioned above, a programmer who wants to focus on a functional programming style can intentionally decide to write many functions as pure functions to allow mathematical and formal reasoning about them. In most cases, one only leaks state intentionally, and creating a certain subset of all your functionality as pure functions allows for cleaner code. They might perhaps be broken up by “pure” modules, or annotated in the function names or docstrings. 

Closures and Callable Instances 
There is a saying in computer science that a class is “data with operations attached” while a closure is “operations with data attached.” In some sense they accomplish much the same thing of putting logic and data in the same object. But there is definitely a philosophical difference in the approaches, with classes emphasizing mutable or rebindable state, and closures emphasizing immutability and pure functions. Neither side of this divide is absolute—at least in Python—but different attitudes motivate the use of each. 

Let us construct a toy example that shows this, something just past a “hello world” of the different styles: 
  1. # A class that creates callable adder instances  
  2. class Adder(object):  
  3.   def __init__(self, n):  
  4.     self.n = n  
  5.   def __call__(self, m):  
  6.     return self.n + m  
  7.   
  8. add5_i = Adder(5) # "instance" or "imperative"  
We have constructed something callable that adds five to an argument passed in. Seems simple and mathematical enough. Let us also try it as a closure: 
  1. def make_adder(n):  
  2.   def adder(m):  
  3.     return m + n  
  4.   return adder  
  5.   
  6. add5_f = make_adder(5) # "functional"  
So far these seem to amount to pretty much the same thing, but the mutable state in the instance provides a attractive nuisance
  1. >>> add5_i(10)  
  2. 15  
  3. >>> add5_f(10) # only argument affects result  
  4. 15  
  5. >>> add5_i.n = 10 # state is readily changeable  
  6. >>> add5_i(10) # result is dependent on prior flow  
  7. 20  
The behavior of an “adder” created by either Adder() or make_adder() is, of course, not determined until runtime in general. But once the object exists, the closure behaves in a pure functional way, while the class instance remains state dependent. One might simply settle for “don’t change that state”—and indeed that is possible (if no one else with poorer understanding imports and uses your code)—but one is accustomed to changing the state of instances, and a style that prevents abuse programmatically encourages better habits

There is a little “gotcha” about how Python binds variables in closures. It does so by name rather than value, and that can cause confusion, but also has an easy solution. For example, what if we want to manufacture several related closures encapsulating different data: 
  1. # almost surely not the behavior we intended!  
  2. >>> adders = []  
  3. >>> for n in range(5):  
  4.     adders.append(lambda m: m+n)  
  5. >>> [adder(10for adder in adders]  
  6. [1414141414]  
  7. >>> n = 10  
  8. >>> [adder(10for adder in adders]  
  9. [2020202020]  
Fortunately, a small change brings behavior that probably better meets our goal: 
  1. >>> adders = []  
  2. >>> for n in range(5):  
  3. .... adders.append(lambda m, n=n: m+n)  
  4. ....  
  5. >>> [adder(10for adder in adders]  
  6. [1011121314]  
  7. >>> n = 10  
  8. >>> [adder(10for adder in adders]  
  9. [1011121314]  
  10. >>> add4 = adders[4]  
  11. >>> add4(10100) # Can override the bound value  
  12. 110  
Notice that using the keyword argument scope-binding trick allows you to change the closed-over value; but this poses much less of a danger for confusion than in the class instance. The overriding value for the named variable must be passed explictly in the call itself, not rebound somewhere remote in the program flow. Yes, the name add4 is no longer accurately descriptive for “add any two numbers,” but at least the change in result is syntactically local. 

Methods of Classes 
All methods of classes are callables. For the most part, however, calling a method of an instance goes against the grain of functional programming styles. Usually we use methods because we want to reference mutable data that is bundled in the attributes of the instance, and hence each call to a method may produce a different result that varies independently of the arguments passed to it

Accessors and Operators 
Even accessors, whether created with the @property decorator or otherwise, are technically callables, albeit accessors are callables with a limited use (from a functional programming perspective) in that they take no arguments as getters, and return no value as setters: 
  1. class Car(object):  
  2.   def __init__(self):  
  3.     self._speed = 100  
  4.   
  5.   @property  
  6.   def speed(self):  
  7.     print("Speed is", self._speed)  
  8.     return self._speed  
  9.   
  10.   @speed.setter  
  11.   def speed(self, value):  
  12.     print("Setting to", value)  
  13.     self._speed = value  
  14.   
  15. # >> car = Car()  
  16. # >>> car.speed = 80 # Odd syntax to pass one argument  
  17. # Setting to 80  
  18. # >>> x = car.speed  
  19. # Speed is 80  
In an accessor, we co-opt the Python syntax of assignment to pass an argument instead. That in itself is fairly easy for much Python syntax though, for example: 
  1. class TalkativeInt(int):  
  2.   def __lshift__(self, other):  
  3.     print("Shift", self, "by", other)  
  4.     return int.__lshift__(self, other)  
  5.   
  6. >>> t = TalkativeInt(8)  
  7. >>> t << 3  
  8. Shift 8 by 3  
  9. 64  
Every operator in Python is basically a method call “under the hood.” (Standard operators as functions) But while occasionally producing a more readable “domain specific language” (DSL), defining special callable meanings for operators adds no improvement to the underlying capabilities of function calls. 

Static Methods of Instances 
One use of classes and their methods that is more closely aligned with a functional style of programming is to use them simply as namespaces to hold a variety of related functions: 
  1. import math  
  2.   
  3. class RightTriangle(object):  
  4.   "Class used solely as namespace for related functions"  
  5.   @staticmethod  
  6.   def hypotenuse(a, b):  
  7.     return math.sqrt(a**2 + b**2)  
  8.   
  9.   @staticmethod  
  10.   def sin(a, b):  
  11.     return a / RightTriangle.hypotenuse(a, b)  
  12.   
  13.   @staticmethod  
  14.   def cos(a, b):  
  15.     return b / RightTriangle.hypotenuse(a, b)  
Keeping this functionality in a class avoids polluting the global (or module, etc.) namespace, and lets us name either the class or an instance of it when we make calls to pure functions. For example: 
>>> RightTriangle.hypotenuse(3,4)
5.0
>>> rt = RightTriangle()
>>> rt.sin(3,4)
0.6
>>> rt.cos(3,4)
0.8

By far the most straightforward way to define static methods is with the decorator named in the obvious way. If your namespace is entirely a bag for pure functions, there is no reason not to call via the class rather than the instance. But if you wish to mix some pure functions with some other stateful methods that rely on instance mutable state, you should use the @staticmethod decorator. 

Generator Functions 
A special sort of function in Python is one that contains a yield statement, which turns it into a generator. What is returned from calling such a function is not a regular value, but rather an iterator that produces a sequence of values as you call the next() function on it or loop over it. This is discussed in more detail in the chapter entitled “Lazy Evaluation.” 

While like any Python object, there are many ways to introduce statefulness into a generator, in principle a generator can be “pure” in the sense of a pure function. It is merely a pure function that produces a (potentially infinite) sequence of values rather than a single value, but still based only on the arguments passed into it. Notice, however, that generator functions typically have a great deal of internal state; it is at the boundaries of call signature and return value that they act like a side-effect-free “black box.” A simple example: 
  1. >>> def get_primes():  
  2. ...   "Simple lazy Sieve of Eratosthenes"  
  3. ...   candidate = 2  
  4. ...   found = []  
  5. ...   while True:  
  6. ...      if all(candidate % prime != 0 for prime in found):  
  7. ...         yield candidate  
  8. ...         found.append(candidate)  
  9. ...      candidate +=  
  10. ...  
  11. >>> primes = get_primes()  
  12. >>> next(primes), next(primes), next(primes)  
  13. (235)  
  14. >>> for _, prime in zip(range(10), primes):  
  15. ...   print(prime, end=" ")  
  16. ....  
  17. 7 11 13 17 19 23 29 31 37 41  
Every time you create a new object with get_primes() the iterator is the same infinite lazy sequence—another example might pass in some initializing values that affected the result—but the object itself is stateful as it is consumed incrementally. 

Multiple Dispatch 
A very interesting approach to programming multiple paths of execution is a technique called “multiple dispatch” or sometimes “multimethods.” The idea here is to declare multiple signatures for a single function and call the actual computation that matches the types or properties of the calling arguments. This technique often allows one to avoid or reduce the use of explicitly conditional branching, and instead substitute the use of more intuitive pattern descriptions of arguments. 

A long time ago, this author wrote a module called multimethods that was quite flexible in its options for resolving “dispatch linearization” but is also so old as only to work with Python 2.x, and was even written before Python had decorators for more elegant expression of the concept. Matthew Rocklin’s more recent multipledis patch is a modern approach for recent Python versions, albeit it lacks some of the theoretical arcana I explored in my ancient module. Ideally, in this author’s opinion, a future Python version would include a standardized syntax or API for multiple dispatch (but more likely the task will always be the domain of third-party libraries). 

To explain how multiple dispatch can make more readable and less bug-prone code, let us implement the game of rock/paper/scissors in three styles. Let us create the classes to play the game for all the versions: 
  1. class Thing(object): pass  
  2. class Rock(Thing): pass  
  3. class Paper(Thing): pass  
  4. class Scissors(Thing): pass  
Many Branches 
First a purely imperative version. This is going to have a lot of repetitive, nested, conditional blocks that are easy to get wrong: 


Delegating to the Object 
As a second try we might try to eliminate some of the fragile repitition with Python’s “duck typing”—that is, maybe we can have different things share a common method that is called as needed: 
  1. class DuckRock(Rock):  
  2.     def beats(self, other):  
  3.         if isinstance(other, Rock):  
  4.             return None # No winner  
  5.         elif isinstance(other, Paper):  
  6.             return other  
  7.         elif isinstance(other, Scissors):  
  8.             return self  
  9.         else:  
  10.             raise TypeError("Unknown second thing")  
  11.   
  12. class DuckPaper(Paper):  
  13.     def beats(self, other):  
  14.         if isinstance(other, Rock):  
  15.             return self  
  16.         elif isinstance(other, Paper):  
  17.             return None # No winner  
  18.         elif isinstance(other, Scissors):  
  19.             return other  
  20.         else:  
  21.             raise TypeError("Unknown second thing")  
  22.   
  23. class DuckScissors(Scissors):  
  24.     def beats(self, other):  
  25.         if isinstance(other, Rock):  
  26.             return other  
  27.         elif isinstance(other, Paper):  
  28.             return self  
  29.         elif isinstance(other, Scissors):  
  30.             return None # No winner  
  31.         else:  
  32.             raise TypeError("Unknown second thing")  
  33.   
  34. def beats2(x, y):  
  35.     if hasattr(x, 'beats'):  
  36.         return x.beats(y)  
  37.     else:  
  38.         raise TypeError("Unknown first thing")  
Then you can test it this way: 
>>> rock, paper, scissors = DuckRock(), DuckPaper(), DuckScissors()
>>> beats2(rock, paper)

>>> beats2(3, rock)
Traceback (most recent call last):
File "", line 1, in
File "/tmp/game.py", line 43, in beats2
raise TypeError("Unknown first thing")
TypeError: Unknown first thing

We haven’t actually reduced the amount of code, but this version somewhat reduces the complexity within each individual callable, and reduces the level of nested conditionals by one. Most of the logic is pushed into separate classes rather than deep branching. In object-oriented programming we can “delgate dispatch to the object” (but only to the one controlling object). 

Pattern Matching 
As a final try, we can express all the logic more directly using multiple dispatch. This should be more readable, albeit there are still a number of cases to define: 
  1. from multipledispatch import dispatch  
  2.   
  3. @dispatch(Rock, Rock)  
  4. def beats3(x, y): return None  
  5.   
  6. @dispatch(Rock, Paper)  
  7. def beats3(x, y): return y  
  8.   
  9. @dispatch(Rock, Scissors)  
  10. def beats3(x, y): return x  
  11.   
  12. @dispatch(Paper, Rock)  
  13. def beats3(x, y): return x  
  14.   
  15. @dispatch(Paper, Paper)  
  16. def beats3(x, y): return None  
  17.   
  18. @dispatch(Paper, Scissors)  
  19. def beats3(x, y): return x  
  20.   
  21. @dispatch(Scissors, Rock)  
  22. def beats3(x, y): return y  
  23.   
  24. @dispatch(Scissors, Paper)  
  25. def beats3(x, y): return x  
  26.   
  27. @dispatch(Scissors, Scissors)  
  28. def beats3(x, y): return None  
  29.   
  30. @dispatch(object, object)  
  31. def beats3(x, y):  
  32.     if not isinstance(x, (Rock, Paper, Scissors)):  
  33.         raise TypeError("Unknown first thing")  
  34.     else:  
  35.         raise TypeError("Unknown second thing")  
  36.   
  37. # >>> beats3(rock, paper)  
  38. # <__main__ .duckpaper="" at="" class="number" nbsp="" span="" style="background-color: inherit; border: none; color: #c00000; margin: 0px; padding: 0px;">0x103b894a8>  

  • # >>> beats3(rock, 3)  
  • # TypeError: Unknown second thing  
  • Predicate-Based Dispatch 
    A really exotic approach to expressing conditionals as dispatch decisions is to include predicates directly within the function signatures (or perhaps within decorators on them, as with multipledispatch). I do not know of any well-maintained Python library that does this, but let us simply stipulate a hypothetical library briefly to illustrate the concept. This imaginary library might be aptly named predicative_dispatch
    1. from predicative_dispatch import predicate  
    2.   
    3. @predicate(lambda x: x < 0, lambda y: True)  
    4. def sign(x, y):  
    5.   print("x is negative; y is", y)  
    6.   
    7. @predicate(lambda x: x == 0, lambda y: True)  
    8. def sign(x, y):  
    9.   print("x is zero; y is", y)  
    10.   
    11. @predicate(lambda x: x > 0, lambda y: True)  
    12. def sign(x, y):  
    13.   print("x is positive; y is", y)  
    While this small example is obviously not a full specification, the reader can see how we might move much or all of the conditional branching into the function call signatures themselves, and this might result in smaller, more easily understood and debugged functions.

    沒有留言:

    張貼留言

    [Linux 常見問題] How to set a file size limit for a directory?

    Source From   Here   Question   I   have a directory on my system which is used for a specific reason by applications and users , but I   d...