写给Python小伙伴的一些建议

写在前面

不知不觉作为一名 Python 开发工程师 也有好几年的时间了,对于 Python 总想写点总结,却又不知从何开始。随着公司项目不断转向 Java,我隐约的觉得 Python 的生涯也将迎来终结,所以近期抽个空下这篇关于 Python 的一些总结。

正文开始

先看一个 Python3 Cheat Sheet,忘了哪里收藏的图了- 侵删。

image.png

关于版本的选择

截至 2021 年 7 月 7 日 最新的 Python 版本为 Python 3.9.6,建议小伙伴直接上手最新版的 Python 就好了。如果你有接手之前版本的 Python 项目,其实问题也不大,只是在小部分语法上会有变化。至于入门、学习 Python 的小伙伴推荐浏览官方网站。

当然如果觉得不符合国人习惯,那么我推荐 廖雪峰 Python教程

需要注意的是 2020 年 1 月 1 日,官方宣布停止 Python2 的更新。

关于 Pythonic

用于赞扬符合 Python 风格的代码,即充分利用 Python 语言的特性,写出简洁明了、可读性强,通常运行速度也快的代码。还指 API 符合 Python 高手的编程方式。

Pythonic 是 Python 的开发者描述那种符合特定风格的代码。这种 Pythonic 风格,既不是非常严密的规范,也不是由编译器强加给开发者的规则。而是大家在使用Python语言协同工作的过程中逐渐形成的习惯Python开发者不喜欢复杂的事物,他们崇尚直观、简洁而又易读的代码。

关于 Ipython

IPython是一种基于Python的交互式解释器。相较于本地的Python Shell,IPython提供了更为强大的编辑和交互功能。

建议全体小伙伴在学习测试 Python 代码时候使用 Ipython。可以快速验证结果以及实现想法,Jupyter 就是基于 Ipython 开发的一个 Web端交互式开发工具。

如果你是 Web 开发 Ipython 可以满足日常需求;如果你是数据科学家,直接使用 Jupyter,大多数数据科学家 将 Jupyter 作为首选的开发工具使用。

关于 列表/字典/集合 推导式

熟练使用各种推导式,可以解决多数 map/reduce/filter 解决的问题。

# 过滤 0-9 中能被3整除的数字
[ i for i in range(10) if i % 3 == 0] # [0, 3, 6, 9]

my_list = ['apple', 'banana', 'grapes', 'pear']

# 快速生成下标+元素的字典
{ k:v for k,v in enumerate(my_list)} # {0: 'apple', 1: 'banana', 2: 'grapes', 3: 'pear'}

# 快速获取列表中字符串长度的值
{len(s) for s in ['a','is','with','if','file','exception']} # {1, 2, 4, 9}

# 生成器推导式与列表推导式使用方式一致,不过对于生成器推导式返回值为 生成器对象
(i for i in range(10))  # <generator object <genexpr> at 0x103d808e0>

复制代码

数据量较大的序列中不建议使用列表推导式,推荐使用生成器推导式;利用生成器推导式懒加载特性处理大数据。

关于生成器,本质上也是迭代器,不过它比较特殊。列表是一次性将所有数字存到对象中,生成器是什么时候需要,才什么时候生成。

a = [i for i in range(10)] # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
b = (i for i in range(10)) # <generator object <genexpr> at 0x102a22888>

# 在 for 循环语句下使用一致,在获取元素时 生成器不支持下标直接获取,需要调用 next 函数依次获取

# 生成器获取数据
next(b)
复制代码

PS:for 循环语句内部同时列表、生成器等迭代器的访问,所以在 for 循环表现一致。

关于循环中获取元素索引

强烈推荐 使用 enumerate ,Python 内置 enumerate 可以把各种迭代器包装成生成器,生成器每次调用返回元素下标 + 元素对象。

my_list = ['apple', 'banana', 'grapes', 'pear']

# 不推荐
for i in range(len(my_list)):
   print(i, my_list[i])

# 推荐
for counter, value in enumerate(my_list):
    print(counter, value)
复制代码

关于字典的合并

期望是存在两个字典,合并并生成新的字典

  • ** 解构参数的方式
  • 字典 update 方法
a_dict = {"a":1, "b":1}
b_dict = {"b":2, "c":2}

# 参数解构方式
c_dict = {**a_dict, **b_dict}

# 字典 update 方法
c_dict = {}
c_dict.update(a_dict)
c_dict.update(b_dict)

# 其他方式
import itertools
c_dict = dict(itertools.chain(a_dict.items(), b_dict.items()))

# 上诉合并后的字典,重复的 key 后者会覆盖前者
# 下面的方式是优先使用前者
from collections import ChainMap
c_dict = ChainMap(a_dict, b_dict
复制代码

注意:一个 ChainMap 接受多个字典并将它们在逻辑上变为一个字典。 然而,这些字典并不是真的合并在一起了, ChainMap 类只是在内部创建了一个容纳这些字典的列表 并重新定义了一些常见的字典操作来遍历这个列表。

关于闭包/装饰器

一个有趣的闭包函数 实现函数加法函数调用方式为 add(1)(2) 并且记录函数调用次数(面试被问到过),需要注意的是:

  • 内部函数想使用外部函数的局部变量 内部函数内需要用nonlocal 声明。
def add(a):
    count = 1

    def fun(b):
        nonlocal count
        print(f"func called count {count}")
        count += 1
        return a + b
    return fun

add(1)(2) # 3
复制代码

如下实现一个支持参数的记录日志功能的装饰器,需要注意的是:

  • 内部函数 需要被@wraps(func)修饰,目的是保留原函数的信息。
from functools import wraps, partial
import logging

def logged(func=None, *, level=logging.DEBUG, name=None, message=None):
    if func is None:
        return partial(logged, level=level, name=name, message=message)

    logname = name if name else func.__module__
    log = logging.getLogger(logname)
    logmsg = message if message else func.__name__

    @wraps(func)
    def wrapper(*args, **kwargs):
        log.log(level, logmsg)
        return func(*args, **kwargs)

    return wrapper

# Example use
@logged
def add(x, y):
    return x + y

@logged(level=logging.CRITICAL, name='example')
def spam():
    print('Spam!')
复制代码

关于大文件的读取

比较经典的面试题就会考察 Python 的文件读取,比如下面的题目:

如何在一个内存小于 8 GB 的电脑上, 读取 文件大小为 8 GB 的文件 ?

手动实现文件读取的迭代器,并指定每次文件迭代的大小

def read_in_chunks(file_object, chunk_size=1024):
    """Lazy function (generator) to read a file piece by piece.
    Default chunk size: 1k."""
    while True:
        data = file_object.read(chunk_size)
        if not data:
            break
        yield data


with open('really_big_file.dat', "rb") as f:
    for piece in read_in_chunks(f):
        # <do something with piece>
        process(piece)
复制代码

使用 with 语句块,会自动处理文件的 打开、关闭、异常;for line in f 为一个迭代器,会自动的采用缓冲IO和内存管理,所以你不必担心大文件。


with open("file_name", "rb") as f:
    for line in f:
        # <do something with line>
        process(line) 
复制代码

总结:两种方式都是使用迭代器,懒加载的方式去分块处理文件 ps:二进制的方式打开文件,读取速度更快。

关于一些标准库

下面列举一些个人认为有意思的库,更多的内容可以自行查看源码。

  • collections 提供额外的数据类型:

    1. namedtuple 生成可以使用名字来访问元素内容的tuple子类
    2. deque 双端队列,可以加速从另一侧追加和推出对象
    3. counter 计数器,主要用来计数
    4. orderedDict 有序字典
    5. defaultdict 带有默认值的字典
from collections import * 
# orderedDict 以理解为有序的dict,底层源码是通过双向链表来实现
o = OrderedDict()
o["x"] = 1
print(o) # OrderedDict([('x', 1)])

# 理解为给tuple的元素命名
rectangle = namedtuple("rectangle", ["a", "b"])
r1 = rectangle(3,4)
print(r1) # rectangle(a=3, b=4)
print(r1.a) # 3

# ... 

# Counter 这个很有用,可以给列表元素计数;类似功能有词频统计
wc = Counter("hello world")
print(wc) # Counter({'h': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1})
print(wc["h"]) # 1
复制代码
  • heapq python 实现的堆队列
# 有这么个面试题,求一数组的前K大个元素。用 heapq 两行代码就能实现
import heapq
nums = [14, 20, 5, 28, 1, 21, 6, 12, 27, 19]
# 前K个元素
heapq.nlargest(3, nums) # [28, 27, 21]
heapq.nsmallest(3, nums) #  [1, 5, 6]
复制代码

关于一些书籍的推荐

由于有些书没有公开的电子版,这里就不放链接了。

  • Python3-Cookbook
  • 流畅的 Python
  • Python 3 标准库
  • 编写高质量Python代码的59个有效方法

再多说一句,多看官网文档。

写在最后

让我们来一首 Python 的诗结束这篇文章。

import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

# 对应的中文翻译
'''
Python之禅,作者Tim Peters

优美胜于丑陋
明了胜于晦涩
简单胜于复杂
复杂胜于杂乱
扁平胜于嵌套
间隔胜于紧凑
可读性很重要
特例不足以特殊到违背这些原则
不要忽视错误,除非程序需要这样做
面对模棱两可,拒绝猜测
解决问题最直接的方法应该有一种,最好只有一种
可能这种方法一开始不够直接,因为你不是范罗苏姆
做也许好过不做,但不想就做还不如不做
如果方案难以描述明白,那么一定是个糟糕的方案
如果容易描述,那么可能是个好方案
命名空间是一种绝妙的理念,多加利用
'''
复制代码