学习理解一个软件非常好的方法就是跟随每一个版本演进的新特性,好比一个人被别人看着长大的,知子莫若父。因此每个版本的 Changelogs 或 What's New 是非常值得一读的,见 What's New In Python 3.8。因为接触 Python 比较晚,没能即时的重前往后的看过 Python 各版本的新特性,于是采用倒叙的方式来看 Python 的演进也不错。
在正式进入 Python 3.8 新特性前,先来看 Python 3.8 的安装和以及介绍一个非常棒的 Python 命令行解析器 bpython 作为 python 命令,ipython, idle 的替代品。
Python 3.8 各平台的安装包可在 https://www.python.org/downloads/release/python-380/ 找到。比如 Mac OS X 下可下载 https://www.python.org/ftp/python/3.8.0/python-3.8.0-macosx10.9.pkg,然后执行安装。安装完后,我机器上的 /usr/local/bin/python3
就指向到了 Python 3.8, 要用 Python 3.7 的话命令是 python3.7; pip3 却仍然是老版本的。
bpython 的安装要用相应版本的 pip install bpython, 如 /Library/Frameworks/Python.framework/Versions/3.8/bin/pip3 install bpython
, 或者创建的虚拟环境中安装, python -m venv .venv; source .venv/bin/activate; pip install bpython。安装后启动 bpython 进入一个更智能的 Python 命令解析器。py
下面正式了解 Python 3.8 的主要新特性
一. 赋值表达式
简单说就是赋值也有返回值的,不只是一个单纯的操作。这是一个 Java 和 C 里的默认行为,也就是 a = 100
这个表达式的返回值是也是 100,所以 Java/C 里可以写成
int a = 50;
int b = (a = 100); 这时候 b 也是 100
也是由于 C 中表达式有返回值以及宽泛的 bool 真值的认定,才有 if(a == 1)
建议写成 if(1 == a)
。在 Java 中先前常用的按行读文件的方式
1 2 3 4 |
String line; while((line = buffer.readLine()) != null) { //process line } |
也是因为 Java 的赋值是个表达式。
Python 3.8 之前的版本是不能写成 b = (a=1)
, 更不能写成 print(a=1)
(这不按名传参吗),但是可以用 a = b = 1。
不过介于 Python 等号 =
在传递函数参数时用以指定参数名的用途,所以 Python 创建了 :=
组合符号(像海象眼睛和獠牙)来实现赋值表达式,于是在 Python 3.8 中可以写成如下方式
1 2 3 4 5 6 7 8 |
a = (b := 1) f = open('test.text', 'r') while((line := f.readline()) is not None): print(line.rstrip()) if (n := len(a)) > 10: //近似官方例子 print(f"too long {n}") |
二. 只按位置传递的参数
定义 Python 函数
1 2 |
def incr(x): return x + 1 |
那么既能用 incr(3.8)
也能用 incr(x=3.8)
两种方式调用。假如我们改写成
1 2 |
def incr(x, /): return x + 1 |
那么就只能按位置来传递 x 参数, 用 incr(3.8)
尝试用 incr(x=3.8) 的话会出现如下错误
>>> incr(x=3.8)
Traceback (most recent call last):
File "<input>", line 1, in <module>
incr(x=3.8)
TypeError: incr() got some positional-only arguments passed as keyword arguments: 'x'
定义函数参数中的 /
并不是一个特殊的参数名,它只是用来限定在它之前的参数必须按位置一一匹配参数,不能用参数名来指定。但 /
后的的参数它管不了,像
1 2 3 4 5 6 7 |
def greet(name, /, greeting='hello'): return f"{greeting}, {name}" greet('world') # hello, world greet('world', 'hola') # hola, world greet('world', greeting='hola') # hola, world |
但下面方式就行了
>> greet(name='world')
Traceback (most recent call last):
File "<input>", line 1, in <module>
greet(name='world')
TypeError: greet() got some positional-only arguments passed as keyword arguments: 'name'
有只能按位置传递参数的方式,同时也有只能按名传递参数的函数,这在 Python 3.7 中就有的特性,就是加一个 *
作为特殊参数名,如
1 2 |
def to_fahrenheit(*, celsius): return 32 + celsius * 9/5 |
调用时只能通过
1 |
to_fahrenheit(celsius = 40) |
来调用,因为星号占据着一个特殊位置,下方是各种错误调用方式的尝试及错误
to_fahrenheit(40) # TypeError: to_fahrenheit() takes 0 positional arguments but 1 was given
to_fahrenheit(a=3, celsius=40) # TypeError: to_fahrenheit() got an unexpected keyword argument 'a'
结合一下 /
和 *
在定义函数参数的效果
1 2 |
def headlin(text, /, border="~", *, width=50): return f" {text} ".center(width, border) |
首先明确一下规则,调用该函数时 /
前的参数必须按位置传入,*
后的参数必须按名传入,而它们之间的参数按名按位置传入都行。所以可用下面的调用
headline("hello", "=", width=20) # 正确
headline("hello", border="=", width=20) # 正确
headline(text="hello", border="=", width=20) # TypeError: headline() got some positional-only arguments passed as keyword arguments: 'text'
headline("hello", border="=", 20) # SyntaxError: positional argument follows keyword argument
三. 更精确的类型提示
Python 3.7 就开始支持 Type Hints, 对变量,函数参数或函数返回值进行类型提示,如
1 2 3 |
count: int = 20 def foo(number: float) -> float: pass |
不知为什么 Python 对类型提示的语法针对参数和函数返回值会用不一样的标识符, :
和 ->
注意,这只是类型提示,解释器运行时会把类型提示忽略掉,也就是调是实际类型可以是任意的。像下面的代码是合法的
1 2 3 4 5 6 |
count: int = 'hello' def foo(number: float) -> float: return number foo('hello') |
但这种类型提示可被 IDE 利用或是用第三方工具来进行静态检查,比如 mypy
(用 pip install mypy 安装). 因为既然用了类型提示就该严格遵循代码的意愿。
上面是 Python 3.7 已支持的特性,现在看 Python 3.8 加入的一些 Literal type 类型提示,同样它也不是语法强制,而是像 mypy 来进行静态检查时的约束。下面来看一堆例子
1 2 3 4 5 6 7 |
from typing import Literal # Literal 像是枚举类型的提示 def get_status(port: int) -> Literal['connected', 'disconnected']: return 'connected' # 提示该函数只返回 'connected' 或 'disconnected' def draw_line(direction: Literal['horizontal', 'vertical']) -> None: pass # 函数应传入 'horizontal' 或 'vertical' |
Literal 是限定值的选择,此外还有 Union 类型的提示 -- 可接受类型的枚举
1 2 3 4 |
from typing import Union def foo(number: Union[float, int]) -> Union[str, int]: pass |
还有 Final 或 @final 装饰,相当于 Java 的 final 关键字,变量不能重赋值,类不能被继承,方法不能被覆盖
1 2 3 4 5 6 7 8 9 10 11 12 |
from typing import Final ID: Final = 1 ID += 1 # 用 mypy 静态检查时会报错: Cannot assign to final name "ID" from typing import final @final class Base: pass class Sub(Base): # 如果用 mypy test.py 检查,错误为 error: Cannot inherit from final class "Base" pass |
四. 用 f-strings 进行简单的调试
f-strings 是 Python 3.6 开始引入的语法,用于字符串内的表达式解析,如
f"Diameter {(diam :=2 * r)} give circumference {math.pi * diam:.2f}" #可以运算,中间变量,格式化
在 Python 3.8 中的 f-strings 能力有所增强,f-strings 里 {}
中变量名后再加个等能同时显示变量名与值。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> python = 3.8 >>> f"{python=}" 'python=3.8' >>> name = "Eric" >>> f"{name = }" "name = 'Eric'" >>> f"{name = :>10}" 'name = Eric' >>> f"{name.upper()[::-1] = }" "name.upper()[::-1] = 'CIRE'" |
五. 新模块 importlib.metadata
1 2 3 4 5 6 7 8 9 10 11 |
>>> from import lib import metadata >>> metadata.version("pip") '19.3.1' >>> pip_metadata = metadata.metadata('pip') >>> list(pip_metadata) >>> pip_metadata['Home-page'] 'https://pip.pypa.io/' [p for p in metadata.files('requests')] //列出模块文件 >>> metadata.requires('requests') //列出 requests 模块的所有依赖 |
六. Math 和 Statistics 函数 (NumPy 可能会是更好的选择)
七. 警告信息, 更聪明一些, 以下红色部分是新加的提示信息
>>> version = "3.8">>> version is "3.8"<bpython-input-17>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?version is "3.8"
>>> [(3,5) (6,8)]TypeError: 'tuple' object is not callable>>><input>:2: SyntaxWarning: 'tuple' object is not callable; perhaps you missed a comma?
>>>import sys>>>sys.getsizeof(list(range(20191014)) //内存使用 sizeof
161528168
本文链接 https://yanbin.blog/python-3-8-new-features/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
[…] 体验一下 Python 3.8 带来的主要新特性 […]
[…] 体验一下 Python 3.8 带来的主要新特性 […]