Py学习  »  Python

后悔没早知道这些Python特性

图灵教育 • 4 年前 • 392 次点击  

写 Python 也好几年时间了。讲道理,在工作中大家肯定遇到过这样的场景:

这个故事告诉我们什么?先造轮子再去 GitHub?还是提高下 GitHub 搜索技巧?

都不是!

实际上,在日常的工作中,我们很多需求,无论是常见的、还是不常见的,Python 都为我们提供了一些独特的解决方案,既不需要自己造轮子,也不需要引入新的依赖(引入新的依赖势必会增加项目的复杂度)。

但是 Python 有太多功能和特性被我们忽略了,导致我们在遇到问题的时候,没法第一时间作出良好的决策。

所以,干脆来一起扫清这些被我们忽略的 Python 死角。

装饰器的妙用

我们经常会想完成一些注册&调用的功能,比如我们有四个函数:

  1. def add(a: int, b: int) -> float:

  2. return a + b



  3. def sub(a: int, b: int) -> float:

  4. return a - b



  5. def mul(a: int, b: int) -> float:

  6. return a * b



  7. def div(a: int, b: int) -> float:

  8. return a / b

现在我们想将这四个函数和 +、-、*、/ 四个操作符绑定,那么我们该怎么做?

可能我们第一反应是这样:

  1. operator_map = {}



  2. def add(a: int, b: int) -> float:

  3. return a + b



  4. def sub(a: int, b: int) -> float:

  5. return a - b



  6. def mul(a: int, b: int) -> float:

  7. return a * b



  8. def div(a: int, b: int) -> float:

  9. return a / b



  10. operator_map["+"] = add


  11. operator_map["-"] = sub


  12. operator_map["*"] = mul


  13. operator_map["/"] = div

但这样写起来,有一个很大的问题就是太不美观了。因为直接对于 dict 的操作从实际上来讲可维护性是很差的,那么我们这个地方应该怎么做?

在改进这段代码之前,我们首先要明确 Python 中一个很重要的概念,即:函数/方法是:First Class Member 。用不精确的话来讲,就是函数/方法可以作为参数被传递、被使用。

举个例子:

  1. import typing



  2. def execute(func: typing.Callable, *args, **kwargs) -> typing.Any:

  3. return func(*args, **kwargs)



  4. def print_func(data: int) -> None:

  5. print(data)



  6. execute(print, 2)

大家可以看到我们将 print_func 这个函数作为参数传递给 execute 函数并被调用。

那么我们来改造下之前的代码:

  1. import typing


  2. operator_map = {}



  3. def add(a: int, b: int) -> float:

  4. return a + b



  5. def sub(a: int, b: int) -> float:

  6. return a - b



  7. def mul(a: int, b: int) -> float:

  8. return a * b



  9. def div(a: int, b: int) -> float:

  10. return a / b



  11. def register_operator(operator: str, func: typing.Callable) -> None:

  12. operator_map[operator] = func



  13. register_operator("+", add)

  14. register_operator("-", sub)

  15. register_operator("*", mul)

  16. register_operator("/", div)

好了,大家看看,目前整体代码的可读性以及可维护性是不是改了很多?

但是我们现在的问题在于,每次都需要在单独调用一次 register_operator 函数,这样也太烦了吧!要不要再改进一下?要得。我们可以用装饰器来改进一下。

首先,看一个最简单的装饰器例子:

  1. import functools

  2. import typing

  3. import time



  4. def execute(func: typing.Callable) -> typing.Callable:

  5. @functools.wraps(func)

  6. def wraps(*args, **kwargs) -> typing. Any:

  7. start_time = time.time()

  8. result = func(*args, **kwargs)

  9. print("{}".format(time.time() - start_time))

  10. return result


  11. return wraps



  12. @execute

  13. def add(a: int, b: int) -> float:

  14. return a + b

我们能看到这段函数的意义是计算函数的执行时间。那么这个原理是什么?

实际上装饰器是一个语法糖,具体可以参见 PEP318 Decorators for Functions and Methods。

简而言之,实际上是 Python 替我们做了一个替换过程。以上面的例子为例,这个替换过程就是 add=execute(add) 。

好了,我们就用这个知识点来改进下之前的代码:

  1. import typing


  2. operator_map = {}



  3. def register_operator(operator: str) -> typing.Callable:

  4. def wraps(func: typing.Callable) -> typing.Callable:

  5. operator_map[operator] = func

  6. return func


  7. return wraps



  8. @register_operator("+")

  9. def add(a: int, b: int) -> float:

  10. return a + b



  11. @register_operator("-")

  12. def sub(a: int, b: int) -> float:

  13. return a - b



  14. @register_operator("*")

  15. def mul(a: int, b: int) -> float:

  16. return a * b



  17. @register_operator("/")

  18. def div(a: int, b: int) -> float:

  19. return a / b

这样我们这段代码的注册过程是不是就显得更优雅了?
嗯,是的!实际上 Python 中有很多特性会帮助我们的代码更简洁,更优美。
接下来这个例子很可能帮我们减轻工作量。

聊聊 OrderedDict

dict 是我们经常使用的一种数据解构。但是在 Python 3.6 之前 dict 都是无序的,即我插入的顺序,和数据在 dict 中存放的顺序并无关联(笔者注:Python 3.6 dict 有序只是新版实现的顺带产物,Python 3.7 正式作为 feature 被固定下来)。
但是很多时候,比如在验签等场景,我们需要保证 dict 数据存放顺序,和我们插入顺序是一致的。那么我们该怎么办?
老板有需求下来了,我们肯定不能告诉老板这个需求没法做。那我们就自己实现一个 ordereddict 吧。于是,想了想,写了如下的代码:
  1. import typing



  2. class OrderedDict:

  3. def __init__(self, *args, **kwargs):

  4. self._data = {}

  5. self._ordered_key = []


  6. def __getitem__(self, key: typing.Any) -> typing.Any:

  7. return self._data[key]


  8. def __setitem__(self, key: typing.Any, value: typing.Any) -> None:

  9. if key not in self._data:

  10. return

  11. self._data[key] = value

  12. self._ordered_key.append(key)


  13. def __delitem__(self, key: typing.Any):

  14. del self._data[key]

  15. self._ordered_key.remove(key)

通过额外维护一个 list 来维护 key 插入的顺序。这段代码,看似完成了我们的需求,但是实则存在很大问题。大家可以猜猜问题在哪?

3,2,1!

揭晓答案,这段代码利用 list 来保证 key 的有序性,在删除的时候, list 的删除操作,是一个时间复杂度 O(n) 的操作。换句话说,我们的删除操作随着内部数据的增多,所需的删除时间也变得越长。这对于某些性能敏感的场景是无法接受的。

那要怎么办呢?事实上,Python 在很早之前就已经内置了有序字典,即很多人可能都用过的 collections.OrderedDict 。

OrderedDict 中, Python 维护了一个双向链表解构,来保证插入的有序性,如下图所示:

在最左侧维护一个卫兵节点,卫兵节点的 next 指针恒指向于数据中最后插入的节点。那么插入新的数据时,我们将新的数据插入到卫兵节点之后,从而达成维护插入顺序的目的。

在删除的时候,通过额外维护的一个字典找到待删除的 key 所对应的节点。这个操作是 O(1) 的复杂度,然后大家都知道,双向链表删除一个节点的时间复杂度也是 O(1) 。通过这样保证我们在即便有大量数据的情况下,也能保证相应的性能。
好了,我们按照这个思路来做一个最简单的实现:
  1. import typing



  2. class Node:

  3. def __init__(self, key: typing.Any, value: typing.Any) -> None:

  4. self.key = key

  5. self.value = value

  6. self.prev = None

  7. self.next = None



  8. class OrderedDict:

  9. def __init__(self, *args, **kwargs):

  10. self._data = {}

  11. self._head = Node(None, None)

  12. self._last = self._head


  13. def __getitem__(self, key: typing.Any) -> typing.Any:

  14. if key in self._data:

  15. return self._data[key].value

  16. raise ValueError


  17. def __setitem__(self, key: typing.Any, value: typing.Any) -> None:

  18. if key not in self._data:

  19. return

  20. value_node = Node(key, value)

  21. next_node = self._head.next

  22. if not next_node:

  23. self._head.next = value_node

  24. value_node.prev = self._head

  25. self._last = value_node

  26. else:

  27. value_node.next = next_node

  28. next_node.prev = value_node

  29. value_node.prev = self._head

  30. self._head.next = value_node

  31. self._data[key] = value_node


  32. def __delitem__(self, key: typing.Any):

  33. if key not in self._data:

  34. return

  35. value_node = self._data[key]

  36. if value_node == self._last:

  37. self._last = value_node.prev

  38. self._last.next = None

  39. else:

  40. prev_node = value_node.prev

  41. next_node = value_node.next

  42. prev_node.next = next_node

  43. next_node.prev = prev_node

  44. del self._data[key]

  45. del value_node

(此段代码,如有错乱,烦请将浏览字体调小几号)

这只是一个 OrderedDict 的简化版,如果想完成一个完整的 OrderedDict 还有很多很多的 corner case 要去处理。不过现在,我们可以使用内置的数据结构去完成我们需求。怎么样,是不是有了一种幸福的感觉?

随意聊聊

通过今天的两个例子,我们发现 Python 提供了相当多的功能去帮助我们完成日常的工作与学习任务。同时通过去深入地了解 Python 内部的一些功能实现,以便我们能更好地去学习一些知识。比如,上文提到的 OrderedDict 的实现,会让我们学到双头链表的一种非常典型的应用,与此同时,双头链表也会用于诸如 LRU 这样非常常用的数据解构的实现。所以,多去深入了解 Python 的方方面面,有助于我们整体能力的提升。

那么既然电脑上已经有了 Python,为何不将这些 tricks 用起来,让 Python 更 6 呢?安利这位来自德国的大咖,资深养蛇玩家 Dan Bader。

他的博客 dbader.org  每月有超过 20 万的浏览量,Real Python 视频累计浏览量超过百万。订阅他的邮件,每天都会准时收到他更新 Python 技巧,一日不差,是一个极其自律的人。


畅销书 Python Tricks 作者,影响全球 1 000 000 以上程序员的 PythonistaCafe 社区创始人,Real Python 培训机构总编,拥有近 20 年软件开发经验。巴德尔毕业于欧洲历史悠久的慕尼黑工业大学,该校以优异的科教质量闻名,截至 2018 年已经培养出 17 位诺贝尔奖得主。
他把自己对 Python 的理解成书,试图通过一些常用的小例子帮助更多开发者拥抱 Python。

精进Python不二之选

《深入理解Python特性》
Dan Bader 著
孙波翔 译

(扫一扫,新旧封面随机发货)
上市两个月获 Amazon 百余条五星评价,详解用好 Python 需要了解的最重要特性,与《流畅的Python》互为补充,Python 进阶必备。帮助 Python 开发人员挖掘这门语言及相关程序库的优秀特性,避免重复劳动,同时写出简洁、流畅、易读、易维护的代码。
彩蛋
看过书后,我发送了一封邮件给作者。提问他为什么会想把这些特性写成一本书?
他是这么说的:

When I started thinking about the book I'd been a lead developer at this software company in Vancouver, Canada for a while and we were doing lots of Python web development.

最开始有写书计划的时候,我在温哥华的一个软件公司担任开发团队的头儿,我们那会儿正在做大量有关 Python web 的开发工作。

I'd been a passionate user of Python for several years at that point and I always tried going out of my way to teach others at the company how to write better, cleaner, and more Pythonic code.

那时,我是一个狂热的 Python 用户,而且我不遗余力地想教会公司其他人如何编写更简洁易用,更 Pythonic 的代码。

I felt like there were already lots of good resources out there that taught people how to get started with Python, but there was a lack of material that showed the little "tricks" and idioms that are a sign of someone graduating from beginner level to intermediate and beyond.

我意识到已经有很多资源可以教会人们如何入门 Python,但是还是缺少一些能够帮助了解其特性和习惯用法的内容,这样的内容可以帮助一些初学者进阶到更高的水平。

I also wanted to make learning this stuff fun and engaging. I got so much joy from using Python in my day to day work, I wanted to get a chance to share that love and passion for Python as a programming language.

我还想把学习这些东西变得更有趣。在工作中使用 Python 使我得到了很多乐趣,我也想有机会分享对 Python 这门编程语言的热爱。

Writing Python Tricks was my outlet for that. My ideal reader is someone who really wants to make Python their own and learn how to write clean and Pythonic code. I want them to discover best practices and little-known tricks to round out their knowledge, and to fill up their "Python toolbox" one step at a time.

写这本书就是我对那份热爱的实现。我想写给那些想用好 Python 或想写出 Pythonic 代码的人。我希望他们发现这些 Python 的最佳实践和鲜为人知的技巧以完整他们的知识体系,并且一步步地填满他们的“Python工具箱”。

每一个大佬都有一个济世的梦想,他们善于分享,拥抱开源,并致力于建立良好的生态,不愧是养蛇专业户。

好了!不能再说了,我要去码代码了。回见!

文末畅聊

小伙伴都修炼了哪几门编程语言,平时工作或者学习中使用哪个比较多呢?可以说说你们相爱相杀的故事,或许它还有更多可爱之处等着你去发觉呢?精选留言选出 5 位获得赠书。活动截至 2019.10.15。

题图来源:Freepik.com

漫画制作:图晓轩

推荐阅读:
9本新书上市
垃圾面试官让我回去等通知!
漫画:HTTP之大明邮差

☟☟☟ 更多养蛇方法
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/46892
 
392 次点击