Python 高级技巧学习

本文介绍一些 Python 高级技巧。

拆包

Python 中,我们可以使用拆包的方式来将一个序列或者字典中的元素赋值给多个变量。下面是最简单的用法:

a, b = (1, 2)
print(a, b) # 1 2

我们还可以使用 * 来拆包,* 会将多个元素拆包为一个列表:

a, *b = (1, 2, 3)
print(a, b) # 1 [2, 3]

* 还可以用在中间位置,这样可以将中间的元素拆包为一个列表:

a, *b, c = (1, 2, 3, 4)
print(a, b, c) # 1 [2, 3] 4

* 还可以用在左边,这样可以将右边的元素拆包为一个列表:

*a, b = (1, 2, 3)
print(a, b) # [1, 2] 3

* 还可以直接用在列表变量中,这样可以将列表中的所有元素拆包为多个变量:

a = [1, 2, 3]
print(a) # [1, 2, 3]
print(*a) # 1 2 3

* 还可以用在字典中,这样可以将字典中的所有键拆包为一个列表:

a, *b = {'a': 1, 'b': 2, 'c': 3}
print(a, b) # 'a' ['b', 'c']

* 可以用在函数的参数中,这样可以将参数拆包为多个参数:

def f1(a, b, *c):
    return a, b, c
print(f1(1, 2, 3, 4, 5)) # (1, 2, (3, 4, 5))

** 可以用在函数的参数中,这样可以将参数记录成字典:

def g1(a, b, **c):
    return a, b, c
print(g1(1, 2, x=3, y=4, z=5)) # (1, 2, {'x': 3, 'y': 4, 'z': 5})

fstring

fstringPython 3.6 新增的字符串格式化方法,可以使用 {}f 前缀来格式化字符串。

!r, !s!a

fstring 中的 !r!s 分别表示调用对象的 __repr____str__ 方法。

!a 则会调用 ascii,注意这里没有以 __ 开头结尾,这是一个 Python 提供的方法,不是一个特殊方法。

在默认情况下 f'{number} 会调用 number.__str__ 方法,而 f'{number!r} 会调用 number.__repr__ 方法。

:f, :e, :E, :g:%

fstring 中的 :f, :e, :E, :g:% 分别表示浮点数、科学计数法、科学计数法大写、通用格式和百分比。

number = 123.456
print(f'{number:f}') # '123.456000'
print(f'{number:e}') # '1.234560e+02'
print(f'{number:E}') # '1.234560E+02'
print(f'{number:g}') # '123.456'
print(f'{number:%}') # '12345.600000%'

:>, :<:^

fstring 中的 :>, :<:^ 分别表示右对齐、左对齐和居中对齐。

number = 123
print(f'{number:>8}') # '     123'
print(f'{number:<8}') # '123     '
print(f'{number:^8}') # '  123   '

我们还可以指定填充字符,例如 f'{number:0>8} 表示右对齐,不足 8 位的部分用 0 填充。

:b, :d, :o, :x:X

fstring 中的 :b, :d, :o, :x:X 分别表示二进制、十进制、八进制、十六进制小写和十六进制大写。

例如 f'{number:b} 表示将 number 转换为二进制字符串。还可以通过设置宽度和填充字符来格式化字符串,例如 f'{number:08b} 表示将 number 转换为 8 位的二进制字符串,不足 8 位的部分用 0 填充。

千分位分隔符

fstring 中可以使用 , 等来添加千分位分隔符。

number = 1234567890
print(f'{number:,}') # 1,234,567,890
print(f'{number:_}') # 123_456_7890

自定义 __format__ 方法

我们可以通过自定义 __format__ 方法来实现自定义的格式化方法。

class Number:
    def __init__(self, number):
        self.number = number

    def __format__(self, format_spec):
        if format_spec == 'b':
            return f'{self.number:08b}'
        elif format_spec == 'd':
            return f'{self.number:08d}'
        elif format_spec == 'o':
            return f'{self.number:08o}'
        elif format_spec == 'x':
            return f'{self.number:08x}'
        elif format_spec == 'X':
            return f'{self.number:08X}'
        else:
            return str(self.number)

number = Number(10)
print(f'{number:b}') # '00001010'
print(f'{number:d}') # '00000010'
print(f'{number:o}') # '00000012'
print(f'{number:x}') # '0000000a'
print(f'{number:X}') # '0000000A'

上面的代码中,我们更改了 Number 类的 __format__ 方法,使得 Number 类的 :b, :d, :o, :x:X 变成了 8 位的二进制、十进制、八进制、十六进制小写和十六进制大写。

match/case 语句

Python 3.10 新增了 match/case 语句,这是一种新的模式匹配语法,可以替代 if/elif/else 语句。

def match_color(color):
    match color:
        case 'red':
            print('Red')
        case 'green':
            print('Green')
        case 'blue':
            print('Blue')
        case _:
            print('Unknown')

match_color('red') # Red

match 语句会依次匹配 case 语句,如果匹配成功则执行对应的代码块,如果没有匹配成功则执行 _ 代码块。

match 语句还可以使用 | 来匹配多个值:

def match_color(color):
    match color:
        case 'red' | 'green' | 'blue':
            print('Primary')
        case 'cyan' | 'magenta' | 'yellow':
            print('Secondary')
        case _:
            print('Unknown')

match_color('red') # Primary

match 语句还可以使用 if 来添加条件:

def match_color(color, light):
    match color:
        case 'red' if light:
            print('Light Red')
        case 'red':
            print('Dark Red')
        case 'green' if light:
            print('Light Green')
        case 'green':
            print('Dark Green')
        case _:
            print('Unknown')

match_color('red', True) # Light Red

case 语句中,我们可以指定匹配模式:

def match_color(info):
    match info:
        case ['red', _, _, light] if light:
            print('Light Red')
        case ['red', _, _, light] if not light:
            print('Dark Red')
        case ['green', _, _, light] if light:
            print('Light Green')
        # 也可以使用 *_ 来匹配任意项
        case ['green', *_, light] if not light:
            print('Dark Green')
        case _:
            print('Unknown')

match_color(['red', 1, 2, True]) # Light Red
match_color(['red', 1, 2, False]) # Dark Red

还可以使用 as 来重新命名变量:

def match_color(info):
    match info:
        case [('red', light) as color, _] if light:
            print(f'Light {color}')
        case [('red', light) as color, _] if not light:
            print(f'Dark {color}')
        case [('green', light) as color, _] if light:
            print(f'Light {color}')
        case [('green', light) as color, _] if not light:
            print(f'Dark {color}')
        case _:
            print('Unknown')

match_color([('red', True), 1]) # Light ('red', True)

需要注意的是,只有以下的类型支持对序列进行拆分的写法:

  • list
  • memoryview
  • array.array
  • tuple
  • range
  • collections.deque

需要注意的是在序列结构中使用 str() 等是标明匹配项必须是 str 类型,而不是进行字符串转换:

def match_color(info):
    match info:
        case [str(name), bool(light)] if light:
            print('Light ' + name)
        case _:
            print('Unknown')

match_color((1, True)) # Unknown
match_color(('red', True)) # Light red

特殊方法

Python 中的特殊方法也叫魔术方法 (Magic Method),这些方法以 __ 开头和结尾,比如 __init____str____repr__ 等。这些方法是 Python 解释器调用的,我们可以重写这些方法,从而实现自定义的功能。

这里给出常用的特殊方法的定义与调用方式。

类型转换

方法 说明 调用方式
__bool__(self) 布尔化 bool(obj)
__int__(self) 整型化 int(obj)
__float__(self) 浮点化 float(obj)
__complex__(self) 复数化 complex(obj)
__bytes__(self) 字节化 bytes(obj)
__str__(self) 字符串化 str(obj)

一元运算符

方法 说明 调用方式
__neg__(self) 取负 -obj
__pos__(self) 取正 +obj
__abs__(self) 取绝对值 abs(obj)
__round__(self, ndigits) 四舍五入 round(obj, ndigits)

算数运算

方法 说明 调用方式
__add__(self, other) 加法 obj + other
__sub__(self, other) 减法 obj - other
__mul__(self, other) 乘法 obj * other
__truediv__(self, other) 除法 obj / other
__floordiv__(self, other) 整除 obj // other
__mod__(self, other) 取模 obj % other
__pow__(self, other) 幂运算 obj ** other

说明:上述的所有方法增加了 r 前缀,比如 __radd____rsub__ 等,表示反向运算,即 other + obj。 增加了 i 前缀,比如 __iadd____isub__ 等,表示增量运算,即 obj += other

比较

方法 说明 调用方式
__eq__(self, other) 等于 obj == other
__ne__(self, other) 不等于 obj != other
__lt__(self, other) 小于 obj < other
__le__(self, other) 小于等于 obj <= other
__gt__(self, other) 大于 obj > other
__ge__(self, other) 大于等于 obj >= other

迭代

方法 说明 调用方式
__len__(self) 长度 len(obj)
__getitem__(self, pos) 获取元素 obj[pos]
__setitem__(self, pos, value) 设置元素 obj[pos] = value
__iter__(self) 迭代器 for i in obj:
__contains__(self, value) 是否包含 value in obj
__next__(self) 下一个 next(obj)
__reversed__(self) 反向迭代器 reversed(obj)

位运算

方法 说明 调用方式
__and__(self, other) 与运算 obj & other
__or__(self, other) 或运算 obj | other
__xor__(self, other) 异或运算 obj ^ other
__lshift__(self, other) 左移 obj << other
__rshift__(self, other) 右移 obj >> other
__invert__(self) 取反 ~obj

说明:上述的所有方法增加了 r 前缀,比如 __rand____ror__ 等,表示反向运算,即 other & obj。 增加了 i 前缀,比如 __iand____ior__ 等,表示增量运算,即 obj &= other

构造与析构

方法 说明 调用方式
__init__(self, ...) 构造方法 obj = CalssName(...)
__new__(cls, ...) 创建对象 obj = ClassName(...)
__del__(self) 析构方法 del obj

其中的 __new__ 方法是一个类方法,用于创建对象,而 __init__ 方法是一个实例方法,用于初始化对象。 __new__ 方法需要返回一个对象,通常是调用父类的 __new__ 方法,这个返回值会传递给 __init__ 方法, 而 __init__ 方法不需要返回值。

其他

方法 说明 调用方式
__repr__(self) 表示形式 repr(obj)
__format__(self, format_spec) 格式化 format(obj, format_spec)
__hash__(self) 哈希值 hash(obj)
__index__ 索引值 index(obj)
__call__(self, ...) 调用 obj(...)
__delitem__(self, pos) 删除元素 del obj[pos]
__getattr__(self, name) 获取属性 obj.name
__setattr__(self, name, value) 设置属性 obj.name = value
__delattr__(self, name) 删除属性 del obj.name
__enter__(self) 上下文管理器 with obj as ...:
__exit__(self, exc_type, exc_value, traceback) 上下文管理器 with obj as ...:

注意:实现方法后的调用方式可能并不只有一种,比如 __str__ 方法可以通过 str(obj)print(obj) 来调用。 而如果实现了 __bool__,那么 notandor 也会调用这个方法。

注意:Python 中的很多特殊方法即使不进行实现也能使用其支持的调用形式,例如对于一个只实现了 __add__ 方法的类,也可以使用 a += b 这种形式,此时会被解释为 a = a + b,即先调用 __add__ 方法,然后进行赋值。

海象运算符 :=

这个符号被称为海象运算符,仅仅是因为它的形状像海象。

:= 运算符是 Python 3.8 新增的,它可以在表达式中赋值,这样我们就可以在表达式中使用赋值的变量:

if (n := len('hello')) > 5:
    print(f'Length of hello is {n}')

# 如果不使用 := 运算符,那么代码会变得更加冗长
n = len('hello')
if n > 5:
    print(f'Length of hello is {n}')

一个更加常见的例子是在循环中使用 := 运算符:

file = open(filename, "r")
while (line := file.readline()):
    print(line.strip())

# 如果不使用 := 运算符,那么代码会变得更加冗长
file = open(filename, "r")
while True:
    line = file.readline()
    if not line:
        break
    print(line.strip())

我们还可以在列表推导式中使用 := 运算符。这种情况下,我们往往需要将计算后的结果放到列表中:

numbers = [1, 2, 3, 4, 5]
evens = [x for n in numbers if (x := n * 2) >= 10]

# 如果不使用 := 运算符,那么代码会变得更加冗长
evens = [n * 2 for n in numbers if n * 2 >= 10]

上面的代码能够帮我简化一次 n * 2 的计算,对于一些复杂的计算,这种方式会更加简洁且效率更高。

海象运算符变量的作用域

:= 运算符定义的变量的作用域是当前函数,除非目标变量使用 global 或者 nonlocal 修饰:

if (n := len('hello')) > 5:
    print(f'Length of hello is {n}')

print(n) # OK

切片

Python 中的切片是一个非常强大的功能,我们可以使用切片来获取列表、元组、字符串等序列的子序列。

切片对象

除了基础的切片用法之外,我们可以通过 slice 类来创建切片对象,这样我们就可以在多个地方使用同一个切片对象:

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
s1 = slice(1, 5) # start = 1, end = 5, step = 1
s2 = slice(1) # start = 1, end = None, step = 1
print(numbers[s1]) # [2, 3, 4, 5]
print(numbers[s2]) # [2, 3, 4, 5, 6, 7, 8, 9]

切片获取到的对象可以通过赋值来修改原对象:

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
numbers[1:5] = [10, 20, 30, 40]
print(numbers) # [1, 10, 20, 30, 40, 6, 7, 8, 9]

上面的例子说明切片获取到的是引用,对于可变对象而言我们可以修改切片后,原对象也会发生变化:

test = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# 修改切片的第一个元素的第一个值
test[0:2][0][0] = -1 # 等价于 test[0][0] = -1
print(test) # [[-1, 2, 3], [4, 5, 6], [7, 8, 9]]

# test 中保存的是不可变对象,所以 test 本身不会发生变化
test = [1, 2, 3]
test[0:2][0] = -1
print(test) # [1, 2, 3]

memoryview

memoryview 是一个内置类,它可以让我们直接操作内存,而不需要复制数据。memoryview 对象可以通过 memoryview() 函数创建。

numbers = array.array('B', [1, 2, 3, 4, 5, 6])
# 此时 mem 和 number 共享内存,但是此时我们可以随意更改 mem 的形状等
mem = memoryview(numbers)
mem = mem.cast('B', [2, 3]) # 2 行 3 列
print(mem.tolist()) # [[1, 2, 3], [4, 5, 6]]
mem[1, 2] = 100
print(numbers) # [1, 2, 3, 4, 5, 100]

字典

字典推导式

我们可以使用列表推导式快速创建列表,实际上,对于字典的创建也有类似的语法:

{key: value for key, value in iterable}

match/case 中匹配字典

match/case 语句还可以用来匹配字典:

def match_color(info):
    match info:
        case {'color': 'red', 'light': True}:
            print('Light Red')
        case {'color': 'red', 'light': False}:
            print('Dark Red')
        case {'color': 'green', 'light': True}:
            print('Light Green')
        case {'color': 'green', 'light': False}:
            print('Dark Green')
        case _:
            print('Unknown')

match_color({'color': 'red', 'light': True}) # Light Red

上面的匹配式的意思是只要含有这些键值对的都会被匹配,而且顺序不重要,即使是一个 OrderDict 也会被忽略 顺序。

集合

集合推导式

也可以使用集合推导式创建集合:

{value for value in iterable}

判断集合的包含关系

使用比较运算符可以判断集合的包含关系:

运算符 说明
<= 子集
< 真子集
>= 超集
> 真超集
== 相等
!= 不相等

可变对象与不可变对象

Python 中的对象分为可变对象和不可变对象,可变对象是指对象的值可以改变,而不可变对象是指对象的值不可以改变。

Python 中的不可变对象有:intfloatcomplexstrtuplefrozenset 等。

Python 中的可变对象有:listdictset 等。

一个对象如果可以通过 hash 函数计算哈希值且不抛出异常,那么这个对象就是不可变对象。例如我们可以通过 以下的代码来判断一个对象是否是可变对象:

def is_mutable(obj):
    try:
        hash(obj)
        return False
    except TypeError:
        return True

print(is_mutable(1)) # False
print(is_mutable('hello')) # False
print(is_mutable([1, 2, 3])) # True

避免可变对象作为默认值

Python 中如果使用可变对象绑定到默认值,那么在函数调用时会共享这个可变对象,这样会导致默认值的改变:

def f(a=[]):
    a.append(1)
    return a

print(f()) # [1]
print(f()) # [1, 1]

如果我们想要避免这种情况,我们可以使用 None 作为默认值,然后在函数内部判断是否为 None,然后创建一个新的对象:

def f(a=None):
    if a is None:
        a = []
    a.append(1)
    return a

print(f()) # [1]
print(f()) # [1]

类属性和实例属性

Python 中,存在类属性和实例属性的区分,一般而言,类属性可以通过在类的开头定义,而实例对象可以在 __init__ 方法中定义。类属性可以通过类名或者实例对象访问,而实例属性则可以通过实例对象访问。例如:

class A:
    class_attr = 1

    def __init__(self):
        self.instance_attr = 2

a = A()
print(a.class_attr) # 1
print(a.instance_attr) # 2
print(A.class_attr) # 1

如果通过实例对象去修改类属性,那么实际上是创建了一个新的实例属性:

# 创建一个新的实例对象,名字与类属性相同
a.class_attr = 3
print(a.class_attr) # 3
print(A.class_attr) # 1
# 可以通过 del 删除实例属性
del a.class_attr
# 由于实例对象的 class_attr 属性被删除,所以会访问类属性
print(a.class_attr) # 1

静态方法、类方法和实例方法

Python 的类中,有三种方法:静态方法、类方法和实例方法。

首先介绍最常见的实例方法,实例方法的第一个参数是 self,表示实例对象本身:

class A:
    def instance_method(self):
        # 实例方法可以访问实例属性
        return self

实例方法可以通过实例对象调用,也可以通过类对象调用,但是需要传入实例对象:

a = A()
print(a.instance_method()) # <__main__.A object at 0x7f8b3c7b3d30>
print(A.instance_method(a)) # <__main__.A object at 0x7f8b3c7b3d30>

类方法使用 @classmethod 装饰器来定义,类方法的第一个参数是 cls,表示类对象本身:

class A:
    @classmethod
    def class_method(cls):
        # 类方法可以访问类属性
        return cls

类方法可以通过类对象调用,也可以通过实例对象调用,但是不需要传入实例对象:

a = A()
print(a.class_method()) # <class '__main__.A'>
print(A.class_method()) # <class '__main__.A'>

静态方法使用 @staticmethod 装饰器来定义,静态方法没有 selfcls 参数:

class A:
    @staticmethod
    def static_method():
        return 'static method'

静态方法可以通过类对象调用,也可以通过实例对象调用,但是不需要传入实例对象:

a = A()
print(a.static_method()) # static method
print(A.static_method()) # static method

静态方法其实更像是一个普通的函数,它不能访问类属性和实例属性,但是他可以被实例方法或者类方法调用, 所以如果需要处理的逻辑只会被当前的类多次调用,那么可以使用静态方法实现代码的复用。




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • IEEE Xtreme 18.0 题解
  • 马拉松 4 小时挑战记录
  • ssh 端口转发简介
  • 服务器上创建 git 远程仓库
  • Spring Boot Test 自定义测试类顺序