1

新潮小众的 Python 最新语法合集

 3 years ago
source link: https://zhuanlan.zhihu.com/p/363157743
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

新潮小众的 Python 最新语法合集

已认证的官方帐号

2020 年 python2 停止维护,公司代码规范也鼓励使用 python3.6+版本,而随着 Python 版本的不断更新,许多旧的语法在可读性与效率上都已经有更好的替代了。当然,大部分的重要特性,例如装饰器、生成器、async 等,相信大家都已经了然于心,这里就对一些用的稍微少一些、日常看到的代码中不太常见的能用得上的语法做一个简单的笔记,供大家参考。经验有限,见解甚浅,还望各位大佬们多多指导、补充。

日常的自用 Python 脚本没有太大的工程压力,能紧跟更新步伐、尝试新的特性。但是语法糖用的好就是效率提升,用的不好就是可读性灾难,有些语法的出现也伴随着种种的争议,用更新的语法不代表能写出更好的代码。

v2-2daffc81c5c0103f72410b76487f5830_720w.jpg

翻看语言的更新日志确实蛮有意思

通过语法的更新变化还有变化带来的争议,也能窥透语言的设计哲学、汇聚浓缩在一个特定点上的社区开发经验。选择合适自己的、保持对代码精简可读的追求才是最重要。

那么就从老到新,理一理那些有意思的小 feature 吧。可能有漏掉有趣的点、也可能有解释不到位的地方,欢迎各位大佬更正补充。

Python 3.0-3.6

PEP 3132 可迭代对象解包拓展

Python3.0 引入,加强了原本的星号运算符(*),让星号运算符能够智能地展开可迭代对象。

>>> a, *b, c = range(5)
>>> a
0
>>> c
4
>>> b
[1, 2, 3]

隐式赋值也同样适用

>>> for a, *b in [(1, 2, 3), (4, 5, 6, 7)]:
>>>     print(b)
[2, 3]
[5, 6, 7]

注意双星号(**)不能用相同语法展开字典

人畜无害,用处也不大的一个 feature

PEP 465 矩阵乘法运算符

Python3.5 引入,顾名思义,使用@符号。直接支持 numpy、pandas 等使用。

>>> a = numpy.array([1, 2, 3])
>>> b = numpy.array([10, 20, 30])
>>> a @ b
140

>>> c = numpy.array([[10, 15], [20, 25], [30, 35]])
>>> d = numpy.array([[4, 5, 6], [7, 8, 9]])
>>> c @ d
array([[145, 170, 195],
       [255, 300, 345],
       [365, 430, 495]])

矩阵乘法运算符的魔术方法为__matmul__()__rmatmul__()__imatmul__()三个

本身用处不大,但是提供了一个额外的操作符使用空间,可以用来重载来进行类似距离计算之类的用途。

>>> from math import sqrt

>>> class Point:
>>>     def __init__(self, x, y):
>>>         self.x = x
>>>         self.y = y
>>>
>>>     def __matmul__(self, value):
>>>         x_sub = self.x - value.x
>>>         y_sub = self.y - value.y
>>>         return sqrt(x_sub**2 + y_sub**2)
>>>
>>> a = Point(1, 3)
>>> b = Point(4, 7)
>>> print(a @ b)
5

争议主要存在于:作为矩阵乘法来说@操作符没有直观联系、影响可读性,不如直接使用 matmul

PEP 3107/484/526 函数注解/类型提示/变量注解

Python3.0 引入函数注解、3.5 引入 typing,让 python 也能享受静态类型的福利。可以说是 py3 中个人最喜欢的 feature,使用简单、效果强大,直接让开发效率以及代码可维护性直线增长。

# 参数后加:即可标注类型,函数结构定义后接->即可标注返回类型
def get_hello(name: str) -> str:
    return f"Hello, {name}!"

如上进行标记之后 IDE 便能自动读取参数、返回类型,直接出联想爽快如 java。

而 PEP 484 Typing 则是极大的扩充了类型定义语法,支持别名、泛型、Callable、Union 等等。非常推荐直接阅读 PEP。

https://www.python.org/dev/peps/pep-0484/

下面就是一个泛型的例子

from typing import TypeVar, Iterable, Tuple

T = TypeVar('T', int, float, complex)
Vector = Iterable[Tuple[T, T]]

def inproduct(v: Vector[T]) -> T:
    return sum(x*y for x, y in v)

def dilate(v: Vector[T], scale: T) -> Vector[T]:
    return ((x * scale, y * scale) for x, y in v)

vec = []  # type: Vector[float]

随后在 3.6 引入了众望所归的变量注解(PEP 526),使用也很简单,直接在变量后添加冒号和类型即可,搭配函数注解一起食用体验极佳

pi: float = 3.142

# 也同样支持Union等
from typing import Union

a: Union[float,None] =1.0

3.7 中又引入了延迟标记求值(PEP 563),让 typing 支持了前向引用、并减轻了标注对程序启动时间的影响,如虎添翼。

# 3.7前合法
class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

# 3.7前不合法、3.7后合法
class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

更多的 python 类型检查示例代码:

https://github.com/realpython/materials/tree/master/python-type-checking

静态类型检查对 Python 所带来的副作用主要还是启动时间上的影响,当然大部分场景所带来的便利是远大于这一副作用的。

PEP 498 f-string

Python3.6 引入,应该是用的最多的 feature 之一了,但是看到很多代码里面还是 str.format,就不得不再提一下。

>>> a = 10
>>> #只需要简单的在任意字符串字面量前加个f,就可以用花括号直接引用变量
>>> print(f"a = {a}")
a = 10

>>> # 格式化也很方便,使用:即可
>>> pi = 3.14159
>>> print(f"pi = {pi: .2f}")
pi = 3.14

也可以在表达式后接!s 或者!r 来选择用 str()还是 repr()方法转换为字符串。

基本就是 str.format 的语法糖。在 3.8 版本以后,又增加了直接套表达式的功能,输出信息非常方便。

>>> theta = 30
>>> print(f'{theta=}  {cos(radians(theta))=:.3f}')
theta=30  cos(radians(theta))=0.866

PEP 515 数值字面值下划线

Python3.6 引入。输入太长的数字字面值怎么办?

>>> a = 123_456_789
>>> b = 123456789
>>> a == b
True

比较鸡肋…

Python 3.7

PEP 557 数据类 Data Classes

提供了一个方便的 dataclass 类装饰器,直接上代码举例:

from dataclasses import dataclass

@dataclass
class InventoryItem:
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

对这个例子,这个类会自动生成以下魔术方法

def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0) -> None:
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand
def __repr__(self):
    return f'InventoryItem(name={self.name!r}, unit_price={self.unit_price!r}, quantity_on_hand={self.quantity_on_hand!r})'
def __eq__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) == (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __ne__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) != (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __lt__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) < (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __le__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) <= (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __gt__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) > (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __ge__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) >= (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented

这一条 PEP 也是比较有争议的,主要原因是 Python 其实已经内置了不少的类似模型:collection.namedtupletyping.NamedTupleattrs

但是这条 PEP 的提出还是为了保证方便地创建资料类的同时,保证静态类型检查,而已有的方案都不方便直接使用检查器。

Python 3.8

PEP 572 海象牙运算符

"逼走"了 Guido van Rossum,最有争议的 PEP 之一。首先引入了海象牙运算符:=,代表行内赋值。

# Before
while True:
    command = input("> ");
    if command == "quit":
        break
    print("You entered:", command)

# After
while (command := input("> ")) != "quit":
    print("You entered:", command)

assignment expressions 在进行分支判断时非常好用,写的时候能够舒服很多。本身使用也集中在 if/while 这种场景,虽然让语法变复杂了,但是总体还是可控的,舒适程度大于风险。

海象运算符本身问题不大,但是争议主要存在于 PEP 572 的第二点,对于生成器语义的变化。

在 PEP 572 后,生成器的in后的运算顺序产生了变化,原本是作为生成器输入,结果现在变成了生成器闭包的一部分。

temp_list = ["abc","bcd"]
result_list = (x for x in range(len(temp_list)))
print(list(result_list))

# 等价于
# Before
temp_list = ["abc", "bcd"]


def func_data(data: int):
    for x in range(data):
        yield x


result_list = func_data(len(temp_list))
print(list(result_list))

# After
temp_list = ["abc", "bcd"]


def func_data():
    for x in range(len(temp_list)):
        yield x


result_list = func_data()
print(list(result_list))

这样的修改目的是配合海象牙运算符增加代码可读性,但无疑是带破坏性的修改,且让运行顺序变得迷惑,让一些老代码出现难以发现的 bug。

python 社区在激烈辩论后,这一部分的修改被成功撤销,只保留了海象牙运算符。

关于这个 PEP,知乎上有难得一见的有价值讨论,这部分范例代码也引用自此:

https://www.zhihu.com/question/274823057/answer/376917512

PEP 570 仅限位置形参

在函数形参处新增一个/语法,划分非关键字与关键字形参。例如

def f(a, b, /, c, d, *, e, f):
    print(a, b, c, d, e, f)

# 以下调用均合法
f(10, 20, 30, d=40, e=50, f=60)

# 以下调用均不合法
f(10, b=20, c=30, d=40, e=50, f=60)   # b cannot be a keyword argument
f(10, 20, 30, 40, 50, f=60)           # e must be a keyword argument

/语法的添加让调用函数时可以在可读性与简洁之间自由选择,可以选择强制不接受关键字参数、不需要形参名称时也可以省略。同时也让接受任意参数函数的实现变得方便了许多,例如:

class Counter(dict):
    def __init__(self, iterable=None, /, **kwds):
        # Note "iterable" is a possible keyword argument

这条本来也有其他方案,例如装饰器实现、def fn(.arg1, .arg2, arg3):def fn(a, (b, c), d):等,这里就不一一展开了,推荐阅读 PEP 原文。

Python 3.9

PEP 584 字典合并运算符

在此之前,要想合并两个字典的画风是这样的

a={'a':1,'b':2}
b={'c':3}

a.update(b)

# 或者是
c = {**a, **b}

但自从有了|之后,可以变成这样

a |= b
c = a | b

当然这个操作符也伴随着一些争议,大概是这样:

反方:合并不符合交换律 正方:python 字典合并本身就不符合交换律,特别是 python3.6 之后统一到有序字典后,相比合并应该更类似于拼接

反方:类似管道写法进行多次合并效率低,反复创建和销毁临时映射 正方:这种问题在序列级联时同样会出现。如果真出现了合并大量字典的使用场景,应当直接显式循环合并

反方:|操作符容易和位运算混淆。运算符行为强依赖于变量种类,这在 python 是非常不利于可读性的 正方:确实有这个问题,但是|已经很混乱了(位运算、集合操作、__or__()魔术方法重载),所以还是先规范变量命名吧

即将到来的 Python 3.10

PEP 617 / bpo-12782 括号内的上下文管理

这一条是针对with语法(PEP 343)的小变动,让一个with可以管理多个上下文。使用也很简单

with (CtxManager() as example):
    ...

with (
    CtxManager1(),
    CtxManager2()
):
    ...

with (CtxManager1() as example,
      CtxManager2()):
    ...

with (CtxManager1(),
      CtxManager2() as example):
    ...

with (
    CtxManager1() as example1,
    CtxManager2() as example2
):
    ...

比较实用,避免了 with 下面接 with 产生不必要缩进的尴尬。值得注意的是,这一条语法变动是新的非 LL(1)文法 CPython PEG 解析器所带来的副产物。所以 PEP 617 的标题是New PEG parser for CPython

PEP 634 结构化模式匹配 match-case

直接上结构:

match subject:
    case <pattern_1>:
        <action_1>
    case <pattern_2>:
        <action_2>
    case <pattern_3>:
        <action_3>
    case _:
        <action_wildcard>

是不是感觉熟悉又臭名昭著的 switch-case 终于来了?当然还是有区别的:

这个写法基本还是 if-elif-else 的语法糖,运行完 case 就自动 break 出来。再加上一些看着不错的模式匹配特性。

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 401 | 403 | 404:
            return "Not allowed"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the Internet"

这样的写法看着就比 if-elif-else 看着清爽了许多。针对元组、类、列表也有不错的支持:

# point is an (x, y) tuple
match point:
    case (0, 0):
        print("Origin")
    case (0, y):
        print(f"Y={y}")
    case (x, 0):
        print(f"X={x}")
    case (x, y):
        print(f"X={x}, Y={y}")
    case _:
        raise ValueError("Not a point")

结语

语言的发展是由技术的进步、工程的需求凝结出的结晶,从中透露出的是满满的代码设计哲学。充分了解语法,可以让开发变得顺畅舒适;理解了语法背后的原因与争议,则可以开拓计算机科学领域的视野。与时俱进,深入了解各种新兴技术,才是真正的极客~

推荐阅读

更多干货尽腾讯技术欢迎关注官方公众号:腾讯技术工程,交流群已建立,交流讨论可加QQ 群:160315980(备注腾讯技术) 。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK