Python 进阶之道

Python 和 JavaScript 在笔者看来是很相似的语言,本文归纳了 Python 的各种 tricks。

函数

匿名函数

函数的简化写法,配合 map、filter、reduce 等高阶函数实现函数式编程

# def foo(parameters):
#     return expression
foo = lambda parameters: expression

map - 映射

numbers = [1, 2, 3, 4, 5]
list(map(lambda e: e ** 2, numbers))
# [1, 4, 9, 16, 25]

filter - 过滤

values = [None, 0, '', True, 'alphardex', 666]
list(filter(lambda e:e, values))
# [True, "alphardex", 666]

reduce - 归并

from functools import reduce
numbers = [1, 2, 3, 4, 5]
reduce(lambda acc, cur: acc + cur, numbers)
# 15

星号和双星号

数据容器的合并

l1 = ['kaguya', 'miyuki']
l2 = ['chika', 'ishigami']
[*l1, *l2]
# ['kaguya', 'miyuki', 'chika', 'ishigami']
d1 = {'name': 'rimuru'}
d2 = {'kind': 'slime'}
{**d1, **d2}
# {'name': 'rimuru', 'kind': 'slime'}

函数参数的打包与解包

# 打包
def foo(*args):
    print(args)
foo(1, 2)
# (1, 2)

def bar(**kwargs):
    print(kwargs)
bar(name='hayasaka', job='maid')
# {'name': 'hayasaka', 'job': 'maid'}

# 解包
t = (10, 3)
quotient, remainder = divmod(*t)
quotient
# 商:3
remainder
# 余:1

数据容器

列表

迭代

li = ['umaru', 'ebina', 'tachibana']
for e in li:
    print(li)
# umaru ebina tachibana

同时迭代元素与其索引

用 enumerate 即可

li = ['umaru', 'ebina', 'tachibana']
print([f'{i+1}. {elem}' for i, elem in enumerate(li)])
# ['1. umaru', '2. ebina', '3. tachibana']

同时迭代 2 个以上的可迭代对象

用 zip 即可

subjects = ('nino', 'miku', 'itsuki')
predicates = ('saikou', 'ore no yome', 'is sky')
print([f'{s} {p}' for s, p in zip(subjects, predicates)])
# ['nino saikou', 'miku ore no yome', 'itsuki is sky']

测试是否整体/部分满足条件

all 测试所有元素是否都满足于某条件,any 则是测试部分元素是否满足于某条件

all(e<20 for e in [1, 2, 3, 4, 5])
# True
any(e%2==0 for e in [1, 3, 4, 5])
# True

解包

最典型的例子就是 2 数交换

a, b = b, a
# 等价于 a, b = (b, a)

用星号运算符解包可以获取剩余的元素

first, *rest = [1, 2, 3, 4]
first
# 1
rest
# [2, 3, 4]

用下划线可以忽略某个变量

filename, _ = 'eroge.exe'.split('.')
filename
# 'eroge'

字典

迭代

d = {'name': 'sekiro', 'hobby': 'blacksmithing', 'tendency': 'death'}
[key for key in d.keys()]
# ['name', 'hobby', 'tendency']
[value for value in d.values()]
# ['sekiro', 'blacksmithing', 'death']
[f'{key}: {value}' for key, value in d.items()]
# ['name: sekiro', 'hobby: blacksmithing', 'tendency: death']

排序

data = [{'rank': 2, 'author': 'alphardex'}, {'rank': 1, 'author': 'alphardesu'}]
data_by_rank_desc = sorted(data, key=lambda d: d['rank'], reverse=True)
# [{'rank': 2, 'author': 'alphardex'}, {'rank': 1, 'author': 'alphardesu'}]

缺失键处理

get 返回键值,如果键不在字典中,将会返回一个默认值

d = {'name': 'okabe rintaro', 'motto': 'elpsycongroo'}
d.get('job', 'mad scientist')
# mad scientist

setdefault 返回键值,如果键不在字典中,将会添加它并设置一个默认值

d = {'name': 'okabe rintaro', 'motto': 'elpsycongroo'}
# if 'job' not in d:
#     d['job'] = 'mad scientist'
d.setdefault('job', 'mad scientist')
# mad scientist
d
# {'name': 'okabe rintaro', 'motto': 'elpsycongroo', 'job': 'mad scientist'}

语言专属特性

推导式

推导式是一种快速构建可迭代对象的方法,因此凡是可迭代的对象都支持推导式

列表推导式

获取 0-10 内的所有偶数

even = [i for i in range(10) if not i % 2]
even
# [0, 2, 4, 6, 8]

字典推导式

将装满元组的列表转换为字典

SEIREI = [(0, 'takamiya mio'), (1, 'tobiichi origami'), (2, 'honjou nia'), (3, 'tokisaki kurumi'), (4, 'yoshino'), (5, 'itsuka kotori'), (6, 'hoshimiya mukuro'), (7, 'natsumi'), (8, 'yamai'), (9, 'izayoi miku'), (10, 'yatogami tohka')]
seirei_code = {seirei: code for code, seirei in SEIREI}
seirei_code
# {'takamiya mio': 0, 'tobiichi origami': 1, 'honjou nia': 2, 'tokisaki kurumi': 3, 'yoshino': 4, 'itsuka kotori': 5, 'hoshimiya mukuro': 6, 'natsumi': 7, 'yamai': 8, 'izayoi miku': 9, 'yatogami tohka': 10}
{code: seirei.upper() for seirei, code in seirei_code.items() if code > 6}
# {7: 'NATSUMI', 8: 'YAMAI', 9: 'IZAYOI MIKU', 10: 'YATOGAMI TOUKA'}

集合推导式

求所有数字的平方并去除重复元素

{x ** 2 for x in [1, 2, 2, 3, 3]}
# {1, 4, 9}

生成器表达式

求 0-10 内的所有偶数的和

even_sum_under_10 = sum(i for i in range(11) if not i % 2)
even_sum_under_10
# 30

装饰器

装饰器是一个可调用的对象,顾名思义它能够装饰在某个可调用的对象上,给它增加额外的功能

常用于缓存、权限校验、日志记录、性能测试、事务处理等场景

以下实现了一个简单的日志装饰器,能打印出函数的执行时间、函数名、函数参数和执行结果

import time
from functools import wraps

def clock(func):
    @wraps(func) # 防止被装饰函数的属性被wrapper覆盖
    def wrapper(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs) # 由于闭包,wrapper函数包含了自由变量func
        elapsed = time.perf_counter() - t0
        name = func.__name__
        args = ', '.join(repr(arg) for arg in args)
        kwargs = ', '.join(f'{k}={w}' for k, w in sorted(kwargs.items()))
        all_args_str = ', '.join(astr for astr in [args_str, kwargs_str] if astr)
        print(f'[{elapsed:.8f}s] {name}({all_args_str}) -> {result}')
        return result
    return wrapper # 返回内部函数,取代被装饰的函数

@clock
def factorial(n: int) -> int:
    return 1 if n < 2 else n * factorial(n-1)

factorial(5)
# [0.00000044s] factorial(1) -> 1
# [0.00011111s] factorial(2) -> 2
# [0.00022622s] factorial(3) -> 6
# [0.00030844s] factorial(4) -> 24
# [0.00042222s] factorial(5) -> 120
# 120

如果想让装饰器能接受参数,那就要再嵌套一层

import time
from functools import wraps

DEFAULT_FMT = '[{elapsed:.8f}s] {name}({all_args_str}) -> {result}'

def clock(fmt=DEFAULT_FMT):
    def decorate(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            t0 = time.perf_counter()
            result = func(*args, **kwargs)
            elapsed = time.perf_counter() - t0
            name = func.__name__
            args_str = ', '.join(repr(arg) for arg in args)
            kwargs_str = ', '.join(f'{k}={w}' for k, w in sorted(kwargs.items()))
            all_args_str = ', '.join(astr for astr in [args_str, kwargs_str] if astr)
            print(fmt.format(**locals()))
            return result
        return wrapper
    return decorate

@clock()
def factorial_default_fmt(n: int) -> int:
    return 1 if n < 2 else n * factorial_default_fmt(n-1)

@clock('{name}: {elapsed}s')
def factorial_customed_fmt(n: int) -> int:
    return 1 if n < 2 else n * factorial_customed_fmt(n-1)

factorial_default_fmt(3)
# [0.00000044s] factorial_default_fmt(1) -> 1
# [0.00009600s] factorial_default_fmt(2) -> 2
# [0.00018133s] factorial_default_fmt(3) -> 6
# 6
factorial_customed_fmt(3)
# factorial_customed_fmt: 4.444450496521313e-07s
# factorial_customed_fmt: 9.733346314533264e-05s
# factorial_customed_fmt: 0.0001831113553407704s
# 6

在 django 中,可以通过装饰器对函数视图进行功能增强(比如@login_required 进行登录的权限校验,@cache_page 进行视图的缓存等)

上下文管理器

用于资源的获取与释放,以代替 try-except 语句

常用于文件 IO,锁的获取与释放,数据库的连接与断开等

# try:
#     f = open(input_path)
#     data = f.read()
# finally:
#     f.close()
with open(input_path) as f:
    data = f.read()

其实在 pathlib 里已经给我们封装好了文件 IO 方法

# with open('file') as i:
#     data = i.read()
from pathlib import Path
data = Path('file').read_text()

至于上下文管理器的实现,可以用@contextmanager

from contextlib import contextmanager

@contextmanager
def open_write(filename):
    try:
        f = open(filename, 'w')
        yield f
    finally:
        f.close()

with open_write('onegai.txt') as f:
    f.write('Dagakotowaru!')

多重继承

在 django 中经常要处理类的多重继承的问题,这时就要用到 super 函数

如果单单认为 super 仅仅是“调用父类的方法”,那就错了

在继承单个类的情况下,可以认为 super 是调用父类的方法(ES6 里面亦是如此)

但多重继承就不一样了,因为方法名可能会有冲突,所以 super 就不能单指父类了

在 Python 中,super 指的是 MRO 中的下一个类,用来解决多重继承时父类的查找问题

MRO 是啥?Method Resolution Order(方法解析顺序)

看完下面的例子,就会理解了

class A:
    def __init__(self):
        print('A')

class B(A):
    def __init__(self):
        print('enter B')
        super().__init__()
        print('leave B')

class C(A):
    def __init__(self):
        print('enter C')
        super().__init__()
        print('leave C')

class D(B, C):
    pass

d = D()
# enter B
# enter C
# A
# leave C
# leave B
print(d.__class__.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

首先,因为 D 继承了 B 类,所以调用 B 类的__init__,打印了enter B

打印enter B后的 super 寻找 MRO 中的 B 的下一个类,也就是 C 类,并调用其__init__,打印enter C

打印enter C后的 super 寻找 MRO 中的 C 的下一个类,也就是 A 类,并调用其__init__,打印A

打印A后回到 C 的__init__,打印leave C

打印leave C后回到 B 的__init__,打印leave B

特殊方法

在 django 中,定义 model 的时候,希望 admin 能显示 model 的某个字段而不是 XXX Object,那么就要定义好__str__

每当你使用一些内置函数时,都是在调用一些特殊方法,例如 len()调用了__len__(), str()调用__str__()等

以下实现一个 2d 数学向量类,里面有多个特殊方法

from math import hypot

class Vector2d:

    # 限制允许绑定的属性
    __slots__ = ('__x', '__y')

    # 实例创建
    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)

    # 前双下划线是私有属性,property装饰是只读属性
    @property
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y

    # 可迭代对象
    def __iter__(self):
        yield from (self.x, self.y)

    # 字符串表示形式
    def __repr__(self) -> str:
        return f'{type(self).__name__}({self.x}, {self.y})'

    # 数值转换 - 绝对值
    def __abs__(self) -> float:
        return hypot(self.x, self.y)

    # 数值转换 - 布尔值
    def __bool__(self) -> bool:
        return bool(abs(self))

    # 算术运算符 - 加
    def __add__(self, other):
        x = self.x + other.x
        y = self.y + other.y
        return Vector2d(x, y)

    # 算术运算符 - 乘
    def __mul__(self, scalar: float):
        return Vector2d(self.x * scalar, self.y * scalar)

    # 比较运算符 - 相等
    def __eq__(self, other):
        return tuple(self) == tuple(other)

    # 可散列
    def __hash__(self):
        return hash(self.x) ^ hash(self.y)

v = Vector2d(3, 4)

# __slots__限制了允许绑定的属性,只能是x或y
v.z = 1
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# AttributeError: 'Vector2d' object has no attribute 'z'

# 由于x属性只读,因此无法再次赋值
v.x = 1
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# AttributeError: can't set attribute

# iter(v) => v.__iter__()
x, y = v
# x为3, y为4

# repr(v) => v.__repr__()
v
# Vector2d(3, 4)

# abs(v) => v.__abs__()
abs(v)
# 5.0

# bool(v) => v.__bool__()
bool(v)
# True

# v1 + v2  => v1.__add__(v2)
v1 = Vector2d(1, 2)
v2 = Vector2d(3, 4)
v1 + v2
# Vector2d(4, 6)

# v * 3  => v.__mul__(3)
v * 3
# Vector2d(9, 12)

# v1 == v2 => v1.__eq__(v2)
v1 = Vector2d(1, 2)
v2 = Vector2d(1, 2)
v1 == v2
# True

# hash(v) => v.__hash__()
hash(v)
# 7
v1 = Vector2d(1, 2)
v2 = Vector2d(3, 4)
set([v1, v2])
# {Vector2d(1.0, 2.0), Vector2d(3.0, 4.0)}

如果把 Vector 改造为多维向量呢?关键就是要实现序列协议(__len__和__getitem__)

协议:本质上是鸭子类型语言使用的非正式接口

不仅如此,还要实现多分量的获取以及散列化

from array import array
import reprlib
import math
import numbers
import string
from functools import reduce
from operator import xor
from itertools import zip_longest
import numbers
from fractions import Fraction as F

class Vector:
    typecode = 'd'
    shortcut_names = 'xyzt'

    def __init__(self, components):
        self._components = array(self.typecode, components)

    def __iter__(self):
        return iter(self._components)

    def __repr__(self):
        components = reprlib.repr(self._components)
        components = components[components.find('['):-1]
        return f'{type(self).__name__}({components})'

    def __str__(self):
        return str(tuple(self))

    def __eq__(self, other):
        return tuple(self) == tuple(other)

    def __bool__(self):
        return bool(abs(self))

    # 序列协议 - 获取长度
    def __len__(self):
        return len(self._components)

    # 序列协议 - 索引取值
    def __getitem__(self, index):
        cls = type(self)  # Vector
        if isinstance(index, slice):  # 索引是slice对象,则返回Vector实例
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral):  # 索引是整数类型,则返回_components中对应的数字
            return self._components[index]
        else:
            raise TypeError(f'{cls.__name__} indices must be integers.')

    # 属性访问,获取分量的值
    def __getattr__(self, name):
        cls = type(self)
        if len(name) == 1:
            pos = cls.shortcut_names.find(name)
            if 0 <= pos < len(self._components):
                return self._components[pos]
        raise AttributeError(f'{cls.__name__} has no attribute {name}')

    # 属性设置,给分量设值时会抛出异常,使向量是不可变的
    def __setattr__(self, name, value):
        cls = type(self)
        if len(name) == 1:
            if name in string.ascii_lowercase:
                raise AttributeError(f"can't set attribute 'a' to 'z' in {cls.__name__}")
        super().__setattr__(name, value)

    # 比较所有分量,都相等才算两向量相等
    def __eq__(self, other):
        return len(self) == len(other) and all(a == b for a, b in zip(self, other))

    # 散列化
    def __hash__(self):
        hashes = map(hash, self._components)
        return reduce(xor, hashes, 0)

    # 绝对值
    def __abs__(self):
        return math.sqrt(sum(x ** 2 for x in self))

    # 取正
    def __pos__(self):
        return Vector(self)

    # 取负
    def __neg__(self):
        return Vector(-x for x in self)

    # 加 (减法__sub__的实现与之类似,略)
    def __add__(self, other):
        try:
            return Vector(a + b for a, b in zip_longest(self, other, fillvalue=0.0))
        except TypeError:
            return NotImplemented

    # 反向加(a+b中,如果a没有__add__或返回NotImplemented,则检查b是否有__radd__,有则调用之)
    def __radd__(self, other):
        return self + other

    # 乘 (除法__truediv__的实现与之类似,略)
    def __mul__(self, scalar):
        return Vector(n * scalar for n in self) if isinstance(scalar, numbers.Real) else NotImplemented

    # 反向乘
    def __rmul__(self, scalar):
        return self * scalar

    # 中缀运算符@ - 点积
    def __matmul__(self, other):
        try:
            return sum(a * b for a, b in zip(self, other))
        except TypeError:
            return NotImplemented

    # 反向中缀运算符@
    def __rmatmul__(self, other):
        return self @ other

v = Vector(range(7))
v
# Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])

v[1:3]
# Vector([1.0, 2.0])

v[-1]
# 6.0

v[1,3]
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 39, in __getitem__
# TypeError: Vector indices must be integers.

v.x, v.y, v.z
# (0.0, 1.0, 2.0)

v.x = 1
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 62, in __setattr__
# AttributeError: can't set attribute 'a' to 'z' in Vector

v1 = Vector((3, 4, 5))
v2 = Vector((6, 7))

v1 == v2
# False

set([v1, v2])
# {Vector([6.0, 7.0]), Vector([3.0, 4.0, 5.0])}

abs(v)
# 9.539392014169456

+v
# Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])

-v
# Vector([-0.0, -1.0, -2.0, -3.0, -4.0, ...])

v1 + v2
# Vector([9.0, 11.0, 5.0])

v * 3
# Vector([0.0, 3.0, 6.0, 9.0, 12.0, ...])

v * F(1, 2)
# Vector([0.0, 0.5, 1.0, 1.5, 2.0, ...])

v1 @ v2
# 46.0

想了解所有的特殊方法可查阅官方文档,以下列举些常用的:

字符串表示形式:__str__, __repr__
数值转换:__abs__, __bool__, __int__, __float__, __hash__
集合模拟:__len__, __getitem__, __setitem__, __delitem__, __contains__
迭代枚举:__iter__, __reversed__, __next__
可调用模拟:__call__
实例创建与销毁:__init__, __del__
属性访问:__getattr__, __setattr__
运算符相关:__add__, __radd__, __mul__, __rmul__, __matmul__, __rmatmul__, ...

类方法和静态方法

@classmethod 是类方法,它定义操作类的方法,也就是说会将类绑定给方法,而不是实例

@staticmethod 是静态方法,啥都不绑定,一般用来给类绑定各种工具方法(不涉及对实例和类的操作)

在 django 中,我们经常要在视图函数中对模型类进行各种查询

然而,很多查询都是重复的代码,根据 DRY 原则,它们都是可以被封装的

那么,如果我们要给模型类封装一些查询操作,就要用到@classmethod

以下是 Post 类,里面定义了 latest_posts 方法用来获取最新的几个 Post

这样在视图函数中,就能直接调用该方法进行查询,节省了不少代码

class Post(models.Model):
    STATUS_NORMAL = 1
    STATUS_DELETE = 0
    STATUS_DRAFT = 2
    STATUS_ITEMS = (
        (STATUS_NORMAL, '正常'),
        (STATUS_DELETE, '删除'),
        (STATUS_DRAFT, '草稿'),
    )
    ...
    status = models.PositiveIntegerField(_("状态"), choices=STATUS_ITEMS, default=STATUS_NORMAL)
    created_time = models.DateTimeField(_("创建时间"), auto_now_add=True)
    ...

    @classmethod
    def latest_posts(cls, limit=None):
        queryset = cls.objects.filter(status=cls.STATUS_NORMAL).order_by('-created_time')
        if limit:
            queryset = queryset[:limit]
        return queryset

描述符

实现了__set__或__get__协议的类就是描述符

set 和 get 代表存和取,因此描述符是一种对多个类属性运用相同存取逻辑的一种方式

例如 django 的 ORM 中的字段类型是描述符,用来把数据库记录中的字段数据与 Python 对象的属性对应起来

以下实现一个简单的描述符类,用来在读写属性时验证属性的正确性

class Validator:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise ValueError('Value must be an integer')
        if value > 200:
            raise ValueError('Value must be under 200')

class Person:
    age = Validator('age')

    def __init__(self, age):
        self.age = age

person = Person(age=100)
person.age = 'young'
# Traceback (most recent call last):
# ValueError: Value must be an integer
person.age = 201
# Traceback (most recent call last):
# ValueError: Value must be under 200

元类

进入元类这个概念之前,我们先回顾一下 type()这个函数,不,其实它是个类

通过 type(),我们可以获取一个对象所属的类,但通过 help 函数,发现 type()居然也可以用来创建类!

type(name, bases, dict) -> a new type

name 是新类的名称,bases 是继承的子类,dict 则是新类的属性名与其对应值的字典

class A:
    a = 1
    def foo(self):
        return self.a * 2

# 以上类的创建等价于
A = type('A', (object, ), {'a': 1, 'foo': lambda self: self.a * 2})

那么什么是元类呢?

平时我们用类来创建对象,但一切类都继承了对象,说白了类也是对象,而元类就是用来创建类对象的类

说白了,元类就是制造类的工厂

'alphardex'.__class__
# <class 'str'>
'alphardex'.__class__.__class__
# <class 'type'>

通过以上的例子我们知道 type 就是用来创造一切类的元类,它是 Python 内置的元类

既然有内置的元类,也意味着你也可以自定义元类

以下实现一个元类,用来把类的所有非私有属性自动转换为大写(不已_开头的属性都是非私有的)

思路很简单:把属性和对应的值字典(attrs)里的非私有属性键改为大写(upper)就行了

class UpperAttrMeta(type):
    def __new__(cls, name, bases, attrs):
        """
        __init__方法用来初始化对象并传入参数
        而__new__方法专门用来创建对象(显然这里我们要创建一个类对象并定制它)
        """
        upper_attrs = {k.upper() if not k.startswith('_') else k: v for k, v in attrs.items()}
        return super().__new__(cls, name, bases, upper_attrs)

class Foo(metaclass=UpperAttrMeta):
    name = 'alphardex'
    __love = 'unknown'

f = Foo()
f.NAME
# 'alphardex'
f._Foo__love
# 'unknown'

元类的最经典的用途就是 ORM 的实现,以 django 的 ORM 为例

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

p = Person(name='alphardex', age='24')
p.age
# 24

如果你访问一个模型实例的属性(例如这里的 age),你并不会得到什么 IntegerField(),而是得到了 24 这个数字,这就是元类的作用

元类平时很少用到,如果要动态修改类的属性,可以用猴子补丁(直接修改类方法)或者类装饰器

当然,这并不代表元类没什么用,想用到它的时候自然会用到的

Last updated