• Python函数式编程


    在 Python 中使用函数式编程的最佳实践!

    简 介

    Python 是一种功能丰富的高级编程语言。它有通用的标准库,支持多种编程语言范式,还有许多内部的透明度。如果你愿意,还可以查看 Python 的底层并修改,甚至能在程序运行的时候直接修改运行时。

    我最近注意到一个有经验的 Python 程序员使用 Python 的新方法。就像许多 Python 新手一样,我在第一次看到 Python 时喜欢它的简单易懂的基本循环、函数和类定义的语法。在掌握了基础语法之后,我开始对高级功能感兴趣,如继承、生成器、元编程等。但是,我不太清楚它们的使用方法,经常会在不恰当的地方使用。有一段时间里我写的代码复杂又难理解。后来我反复修改,特别是需要长期在同一段代码上工作时,最终会将大部分代码慢慢改回使用基本的函数、循环、单例类。

    尽管如此,那些高级功能一定有其存在的理由,它们也一定是非常重要的工具。很明显,“怎样编写优秀的代码”是个非常广泛的话题,甚至没有唯一的正确答案!相反,这篇文章的目标是一个特定的话题:Python 中函数式编程的应用。我将讨论函数式是什么,怎样在 Python 中使用,并根据我的经验介绍最佳使用方法。

    号:923414804
    	群里有志同道合的小伙伴,互帮互助,
    	群里有不错的视频学习教程和PDF!
    

    什么是函数式编程?

    函数式编程(简称 FP)是一种编程范式,其中最基本的元素是不可修改的值,以及不与其他函数共享状态的“纯函数”。纯函数对于给定的输入永远返回同样的输出,而且不会修改任何数据,也不会造成副作用。因此,纯函数经常与数学运算比较。例如,3+4 永远等于 7,不管同时进行了其他任何数学运算,也不管之前进行了多少次加法运算。

    有了纯函数和不可修改的值,程序员就可以创建逻辑结构了。迭代可以用递归代替,因为递归才是让同一个动作多次执行的“函数式”做法。函数使用新的输入调用自己,直到参数满足某个终止条件。此外,还有高阶函数,它的输入是其他函数,返回另一个函数。我稍后会介绍这个概念。

    尽管函数式编程从上世纪五十年代就出现了,而且许多语言也都实现了它,但它并没有完全地描述一门语言。Clojure、Common Lisp、Haskell 和 OCaml 都是以函数式为主的语言,也都融合了其他不同的编程语言概念,如类型系统、严格或懒惰求值等。大多数语言还用某种方法支持副作用,如写入文件、读取文件等,通常这些副作用都被仔细地标记为“不纯净”。

    人们通常都认为函数式很深奥,而且与可实践性相比,它更看重优雅和简洁。大公司很少会在大规模项目上依赖于函数式为主的语言,即使要用也是在较小的范围内,远远不如其他 C++、Java、Python 等语言流行。但是,FP 实际上只是一种框架,一种考虑逻辑流的方式,它本身也有优点和缺点,而且也能与其他编程范式配合使用。

    Python 支持什么?

    尽管Python并不是以函数式为主的语言,但对它来说支持函数式编程也相对比较容易,因为Python中的一切都是对象。这意味着函数定义也可以赋给变量并传递。

    def add(a, b):
        return a + b
    
    plus = add
    
    plus(3, 4)  # returns 7
    

    Lambda

    通过 Lambda 表达式的语法,可以用声明式的方式创建函数。关键字 lambda 来自希腊字母,经常在正式的数学逻辑中用来描述函数和变量的虚拟绑定,即“lambda 演算”,它的历史比函数式编程还要久远。这一概念的另一个术语叫做“匿名函数”,因为 lambda 函数可以直接嵌入到行内使用,不需要事先指定名称。将匿名函数赋值给变量后,它的行为与正常函数完全一样。

    (lambda a, b: a + b)(3, 4)  # returns 7
    
    addition = lambda a, b: a + b
    addition(3, 4)  # returns 7
    

    lambda 函数最常见的用法就是提供给那些接受可调用对象作为参数的函数。“可调用对象”是任何能够通过括号调用的东西,具体来说有类、函数和方法。其中最常见的用法就是在对数据结构进行排序时,通过参数的键指定排序的相对顺序。

    authors = ['Octavia Butler', 'Isaac Asimov', 'Neal Stephenson', 'Margaret Atwood', 'Usula K Le Guin', 'Ray Bradbury']
    sorted(authors, key=len)  # Returns list ordered by length of author name
    sorted(authors, key=lambda name: name.split()[-1])  # Returns list ordered alphabetically by last name.
    

    行内嵌入式 lambda 函数的缺点在于它不会在栈跟踪中显示名称,可能会给调试带来麻烦。

    Functools

    高阶函数是函数式编程的精华,部分由 Python 直接提供,部分通过 functools 函数库提供。你可能在大规模分布式数据分析方面听说过 map 和 reduce,但实际上它们也是最重要的两个高阶函数。map 在给定序列的每个元素上执行函数,然后返回结果的序列;reduce 使用一个函数收集序列中的每个元素,然后返回单个值。

    val = [1, 2, 3, 4, 5, 6]
    
    # Multiply every item by two
    list(map(lambda x: x * 2, val)) # [2, 4, 6, 8, 10, 12]
    # Take the factorial by multiplying the value so far to the next item
    reduce(lambda: x, y: x * y, val, 1) # 1 * 1 * 2 * 3 * 4 * 5 * 6
    

    还有许多高阶函数能用其他方式操作函数,其中最值得一提的就是 partial,它能锁定函数的一部分参数。这种方式也叫做“currying”,这个术语来自函数式编程的先驱者 Haskell Curry:

    def power(base, exp):
         return base ** exp
    cube = partial(power, exp=3)
    cube(5)  # returns 125
    

    关于 Python 中的 FP 概念的具体介绍,以及怎样优先使用函数式进行编程,我推荐 Mary Rose Cook 的这篇文章(https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming)。

    这些函数可以将许多行的循环转变成极其精简的一行代码。但是,一般的程序员也很难理解这些代码,特别是 Python 原本与英语十分类似的语法流。个人经验,我永远都记不住参数的顺序,以及每个函数的功能,尽管我查了这么多次手册。但我强烈建议尝试一下这些函数,以了解一些 FP 的概念,而且有时候我认为它们才是正确的选择,如下一节的例子所示。

    修饰器

    高阶函数也以修饰器的形式融入了日常的 Python 编程中。定义修饰器的方法就反映了这一点,而@符号实际上只是个语法糖,将被修饰的函数传递给修饰器作为参数。下面就定义了一个简单的修饰器,它会将给定的代码重试三次,返回第一个成功的值,或者在三次尝试都失败之后放弃并抛出最后的异常。

    def retry(func):
        def retried_function(*args, **kwargs):
            exc = None
            for _ in range(3):
                try:
                   return func(*args, **kwargs)
                except Exception as exc:
                   print("Exception raised while calling %s with args:%s, kwargs: %s. Retrying" % (func, args, kwargs).
    
            raise exc
         return retried_function
    
    @retry
    def do_something_risky():
        ...
    
    retried_function = retry(do_something_risky)  # No need to use `@`
    

    这个修饰器的输入和输出的类型和值完全一样,但这并不是必须的。修饰器可以添加或减少参数,也可以改变参数的类型。它们也可以通过本身的参数进行配置。我想指出的是,修饰器本身不一定是“纯函数”,它们可以(而且经常会)有副作用,只不过是恰巧使用了高阶函数而已。

    就像许多中级或高级 Python 技巧一样,这个功能非常强大,但也很容易造成混乱。你必须使用 functools.wrap 修饰器进行修饰,否则调用的函数和栈跟踪中看到的函数名字会不一样。我见过一些修饰器会做一些非常复杂或非常重要的事情,如解析 json blob 中的值,或者处理认证。我还见过同一个函数或方法定义上的多层修饰器,必须掌握修饰器的应用次序才能正确理解。我认为通过利用内置的修饰器如“staticmethod”可以帮助理解,或者编写最简单的修饰器来避免大量样板代码,但如果你想让你的代码符合类型检查的话,那么尽量不要去修改输入或输出的类型。

    我的建议

    函数式编程很有趣,而且学习舒适区之外的编程范式能够为你带来灵活性,而且也可以让你从另一个角度考虑问题。但是,我不推荐使用 Python 时以函数式为主,特别是在旧的代码库中不要这么做。除了上面我提到的那些坑之外,还有下面的理由:

    开始使用 Python 不需要理解 FP。这样做很可能会迷惑其他阅读者,或者迷惑未来的自己。

    你无法保证任何你依赖的代码(通过 pip 安装的模块,或其他同事的代码)是函数式的,是纯净的。你也不知道你自己的代码是否像你想象的那么纯净。与函数式为主的语言不同,Python 的语法或编译器不会帮你强制纯净,也不会帮你消灭某些 Bug。将副作用和高阶函数混合在一起回导致巨大的混乱,因为你需要论证两种不同的复杂性,其难度是两者的乘积。

    使用带有类型注释的高阶函数是高级技巧。类型签名通常是又长又笨拙的“Callable”的嵌套。例如,一个简单的返回输入函数的高阶修饰器,其定义是“F = TypeVar[‘F’, bound=Callable[..., Any]]”,然后标注是“def transparent(func: F) -> F: return func”。也许你懒得研究正确的签名的写法,而直接使用“Any”代替了。

    那么,我们应该使用函数式编程的哪部分呢?

    纯函数

    只要可能并且合理,就应该尽量保持函数“纯净”,并仔细考虑应当在何处保持改变了的状态,并仔细地标记好。这样能让单元测试变得更容易,你不需要做太多 set-up 和 tear-down,也不需要太多 mocking,而且测试用例不论执行顺序如何,都会产生预期中的结果。

    下面是个非函数式的例子。

    dictionary = ['fox', 'boss', 'orange', 'toes', 'fairy', 'cup']
    def puralize(words):
       for i in range(len(words)):
           word = words[i]
           if word.endswith('s') or word.endswith('x'):
               word += 'es'
           if word.endswith('y'):
               word = word[:-1] + 'ies'
           else:
               word += 's'
           words[i] = word
    
    def test_pluralize():
        pluralize(dictionary)
        assert dictionary == ['foxes', 'bosses', 'oranges', 'toeses', 'fairies', 'cups']
    

    第一次运行 test_pluraize 时该测试能够通过,但以后每次运行都会失败,因为它会反复添加“s”和“es”。为了让它变成纯函数, 可以这样写:

    dictionary = ['fox', 'boss', 'orange', 'toes', 'fairy', 'cup']
    def puralize(words):
       result = []
       for word in words:
           word = words[i]
           if word.endswith('s') or word.endswith('x'):
               plural = word + 'es')
           if word.endswith('y'):
               plural = word[:-1] + 'ies'
           else:
               plural = +  's'
           result.append(plural)
        return result
    
    def test_pluralize():
        result = pluralize(dictionary)
        assert result == ['foxes', 'bosses', 'oranges', 'toeses', 'fairies', 'cups']
    

    注意这里并没有使用任何 FP 特有的概念,只是创建并返回了一个新的对象,而不是重用并修改已有的旧对象。这样输入的内容也会保持不变。

    虽然这个例子像个玩具,但想象一下,如果你传递并改变了某个复杂的对象,或者通过数据库连接进行了某些操作。当编写很多很多测试用例时就会发现,你必须非常小心地处理测试用例的顺序,或者花大量代价在每个测试用例之后清除并重新创建状态。这些工作应该是在 e2e 集成测试阶段的活儿,不应该在比较小的单元测试阶段进行。

    理解(并避免)可修改性

    为什么这一点很重要?有些时候列表和元组可以互换使用,因此人们经常会在代码中随机使用两者之一。于是当你试图修改一个元组(比如给其中一个元素赋值)时就会出错。或者试图用列表作为字典的键,也会导致 TypeError,因为列表是可修改的。元组和字符串可以作为字典的键使用,因为它们不可修改,可以得到确定的哈希值,而其他数据结构都不行,因为它们的对象标识即使保持不变,值也会改变。

    最重要的是,在传递字典、列表或集合时,它们可能会在其他上下文中被意料之外地改变。这种问题非常难以调试。可修改的默认参数就是个经典的例子:

    def add_bar(items=[]):
        items.append('bar')
        return items
    
    l = add_bar()  # l is ['bar']
    l.append('foo')
    add_bar() # returns ['bar', 'foo', 'bar']
    

    字典、集合和列表很强大、效率很高、非常 Python,而且非常有用。写代码时完全不使用它们是不明智的。但即使如此,我永远会在默认参数的位置使用元组或 None(代替空字典或空列表),并且在缺乏足够的防御代码的情况下,避免将可修改的数据结构在不同的上下文中传递。

    减少类的使用

    类(及其实例)的可修改性是把双刃剑。随着写的 Python 代码越来越多,我开始倾向于仅在绝对必要时才使用类,而且我几乎从不使用可修改的类属性。对于那些高度面向对象的语言(如 Java)的程序员来说这一点可能很难做到,但许多其他语言中在类层面完成的东西,在 Python 可以在模块层面完成。例如,如果需要将函数或常量或命名空间分组,那么可以把它们一起放到另一个 .py 文件中。

    我经常看到一些类的目的是保存几个命名变量的值,这种情况下 namedtuple(其类型是 typing.NamedTuple)就足够,而且还是不可改变的。

    from collections import namedtuple
    VerbTenses = namedtuple('VerbTenses', ['past', 'present', 'future'])
    # versus
    class VerbTenses(object):
        def __init__(self, past, present, future):
            self.past = past,
            self.present = present
            self.future = future
    

    如果确实需要状态的来源,而且多个视图都需要改变该状态,那么类是绝佳的选择。此外,与静态方法相比,我更倾向于单例纯函数,这样它们能在其他上下文中组合使用。

    可修改的类属性非常危险,因为它们属于类定义而不是类实例,因此可能会不小心修改到同一个类的多个实例中的状态!

    class Bus(object):
         passengers = set()
         def add_passenger(self, person):
            self.passengers.add(person)
    
    bus1 = Bus()
    bus2 = Bus()
    bus1.add_passenger('abe')
    bus2.add_passenger('bertha')
    bus1.passengers  # returns ['abe', 'bertha']
    bus2.passengers  # also ['abe', 'bertha']
    

    幂等性

    任何实际的大规模复杂系统都可能会失败,而失败就要重试。矩阵代数中的“幂等性”的概念也存在于 API 设计中,但对于函数式编程来说,传递之前的输出给幂等函数,永远会返回相同的值。因此,重做某件事情会收敛到相同的值。因此,上述 pluralize 函数更理想的写法为:,首先检查输入是否已是复数,再考虑怎样计算出复数形式。

    lambda 和高阶函数使用上的注意点

    我发现,在进行短小的操作(如获取排序的键供 sort 使用)时使用 lambda 非常方便。但如果 lambda 超过一行,那么使用普通的函数定义可能更好。通常传递函数可以避免重复,但我在使用时经常提醒自己,额外的结构是否会让代码清晰度下降。通常,将其分解成更小的辅助函数会更清晰。

    在需要时使用生成器和高阶函数

    有时候你会遇到抽象的生成器和迭代器,它们可能会返回巨大或者无限的序列。一个例子就是 range。在 Python 3 中,range 默认是生成器(相当于Python 2 中的 xrange),避免在迭代大数字时出现内存不足的错误,如range(10 ** 10)。如果要在一个可能很大的生成器的每个元素上执行某个操作,那么使用 map、filter 之类的工具可能是最好的选择。

    与此相似,如果不知道你新写的迭代器可能会返回多少结果,但可能会很大,那就应该定义一个生成器。但是,并不是每个人都愿意去使用生成器,他们可能更希望使用列表解析式(list comprehension),从而导致你一开始想要避免的内存不足错误。生成器是 Python 对于流式编程的实现,它也不一定是函数式的,所以它也有其他 Python 编程方式拥有的安全性缺陷。

    结 论

    通过浏览功能、库和内部代码来理解自己选择的编程语言,毫无疑问能帮你在调试和阅读代码方面提高速度。理解其他语言或编程语言理论方面的思想也很有意思,而且能让你成为更强大、无所不通的程序员。

    但是,成为Python的高级程序员意味着你不仅要知道能做什么,更要理解哪种才是最有效的方式。在Python中应用函数式编程可能很容易。

    为了保持优雅,特别是在共享的代码中保持优雅,我认为最好是使用纯粹的函数式思想,让代码更容易预测,从而更容易维护,并且具有幂等性。

  • 相关阅读:
    hdu 1017 A Mathematical Curiosity 解题报告
    hdu 2069 Coin Change 解题报告
    hut 1574 组合问题 解题报告
    hdu 2111 Saving HDU 解题报
    hut 1054 Jesse's Code 解题报告
    hdu1131 Count the Trees解题报告
    hdu 2159 FATE 解题报告
    hdu 1879 继续畅通工程 解题报告
    oracle的系统和对象权限
    oracle 自定义函数 返回一个表类型
  • 原文地址:https://www.cnblogs.com/paisenpython/p/10371459.html
Copyright © 2020-2023  润新知