python编程经验、方法、随笔

设计模式、设计原则、编程方法论

设计模式是一种编程方法论,它提供了一套被反复使用、多数人知晓的、经过分类编排的、代码设计经验的总结。设计模式是软件工程的基石,是用来解决特定问题的可重用、可扩展的设计原则。

工厂模式

创建对象时,将对象的创建过程封装在一个工厂类中,通过工厂类来创建对象,而不是直接创建对象。将对象的创建过程和使用过程分离,可以提高代码的可扩展性。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ShapeFactory:
@staticmethod
def create_shape(shape_type):
if shape_type == 'circle':
return Circle()
elif shape_type =='rectangle':
return Rectangle()
else:
return None

class Circle:
def draw(self):
print('Drawing a circle')

class Rectangle:
def draw(self):
print('Drawing a rectangle')

shape1 = ShapeFactory.create_shape('circle')
shape1.draw() # Drawing a circle

shape2 = ShapeFactory.create_shape('rectangle')
shape2.draw() # Drawing a rectangle

建造者模式

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from dataclasses import dataclass

@dataclass
class Car:
name :None
size :None
model:None
color:None
seats:None
def __str__(self):
return f"Name: {self.name}, Size: {self.size}, Model: {self.model}, Color: {self.color}, Seats: {self.seats}"

class CarBuilder:
def __init__(self, name,size):
self._config={
'name': name ,
'size': size ,
}
# 其他属性设置方法
def set_model(self, model):
self._config['model'] = model
return self

def set_color(self, color):
self._config['color'] = color
return self

def set_seats(self, seats):
self._config['seats'] = seats
return self

def build(self):
return Car(**self._config)

builder = CarBuilder(name='BMW', size='S')
car1 = builder.set_model('BMW').set_color('red').set_seats(4).build()
car1.name = 'X5'

car2 = builder.set_model('Audi').set_color('blue').set_seats(2).build()
car2.name = 'A4'
print(car1) # Name: X5, Size: S, Model: Audi, Color: blue, Seats: 2
print(car2) # Name: A4, Size: S, Model: Audi, Color: blue, Seats: 2

单例模式

保证一个类只有一个实例,节省内存,避免多次实例化,可以使用类变量、静态方法或装饰器来实现。比如使用__new__方法来控制实例化过程,保证生成一个唯一的实例。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton:
_instance = None

def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance
def __init__(self,*args, **kwargs):
pass

s1 = Singleton()
s2 = Singleton()
print(id(s1), id(s2)) # 140601222422480 140601222422480

类、函数自动化生成、代码简化

装饰器

装饰器可以用来修改函数的行为,可以用来实现面向切面编程,可以用来实现日志、性能监控、缓存、事务处理等功能。定义装饰器时,可以接收函数作为参数,并返回修改后的函数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import functools
def my_decorator(func):
@functools.wraps(func)#保留原函数的名称和文档
def wrapper(*args, **kwargs):
print('Before calling the function')
result = func(*args, **kwargs)
print('After calling the function')
return result
return wrapper

@my_decorator
def my_function():
print('Hello, world!')

my_function() # 输出:Before calling the function Hello, world! After calling the function

dataclasses

dataclasses可以用来定义数据类,可以用来自动生成数据类的方法如__init__、repr、__eq__等,可以用来定义默认值、类型注解、元数据等,也可以传入参数来控制自动生成的方法。还可以使用field()函数来自定义控制数据类字段的行为。注意语法:

  • dataclass 字段必须标注类型,不然不参与 dataclass 行为
  • x=0、x: ClassVar[int]类似的字段会视为类变量,不会参与 dataclass 行为
  • x:int、_x:int、__x:int类似的字段都会改写为实例变量,参与 dataclass 行为,分别为self.x、self._x、_类名__x
    例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from dataclasses import dataclass, field

@dataclass(frozen=True)
class Point:
x: int = field(default=0, init=True)
y: int = field(default=0, init=True, repr=False)
z: int = field(init=False) # z 是计算得到的字段,不在初始化时传入

def __post_init__(self):
if self.x < 0 or self.y < 0:
raise ValueError('x and y must be non-negative')
object.__setattr__(self, 'z', self.x + self.y) # 动态设置 z 的值

p = Point(1, 2)
print(p) # Point(x=1, z=3)
print(p.z) # 3
print(p.x) # 1
print(p.y) # 2

with上下文管理器

with语句可以用来简化异常处理,可以用来自动关闭文件、网络连接、数据库连接等,可以用来实现资源的自动释放。
1.如果是类对象,则__enter__()方法返回对象,exit()方法接收异常类型、异常对象、traceback对象,用于处理异常。例如:

1
2
3
4
5
6
7
8
9
10
11
12
class File:
def __init__(self, filename, mode):
self.file = open(filename, mode)

def __enter__(self):
return self.file

def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()

with File('test.txt', 'w') as f:
f.write('Hello, world!')

2.如果是函数对象,使用@contextmanager装饰器,yield语句返回对象,用于处理异常。例如:

1
2
3
4
5
6
7
8
9
10
11
12
from contextlib import contextmanager

@contextmanager
def file_manager(filename, mode):
try:
f = open(filename, mode)
yield f
finally:
f.close()

with file_manager('test.txt', 'w') as f:
f.write('Hello, world!')

namedtuple

namedtuple可以用来定义一个不可变的tuple,可以用属性来访问tuple的元素,可以用._asdict()方法转换为dict。例如:

1
2
3
4
5
6
7
from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
print(p.x, p.y) # 1 2
print(p[0], p[1]) # 1 2
print(p._asdict()) # {'x': 1, 'y': 2}

文件、配置读取与写入

dotenv读取敏感信息作为环境变量

dotenv可以用来读取环境变量,可以用来存储敏感信息,可以用来实现配置管理。dotenv文件可以放在项目根目录,也可以放在用户目录。一般dotenv文件要放在.gitignore中,以免泄露敏感信息。dotenv文件中,每一行包含一个环境变量,格式为"key=value",可以用os.getenv()方法读取环境变量。默认读取的是.env文件,也可以指定文件名为my.env,例如:

1
2
3
4
5
6
7
import os
from dotenv import load_dotenv

load_dotenv('my.env')

# 读取环境变量
print(os.getenv('SECRET_KEY'))

toml

toml可以用来读取和写入配置文件,可以用来存储配置信息,可以用来存储数据。语法类似json,但比json更简洁,如果需要存储多层嵌套的字典,可以使用多行来表示。例如:

1
2
3
4
5
6
7
8
9
10
[tool.poetry]
name = "my-project"
version = "0.1.0"
description = "A short description of my project"
authors = ["Alice <<EMAIL>>", "Bob <<EMAIL>>"]

[tool.poetry.dependencies]
python = "^3.8"
pendulum = "^2.1.2"

使用toml.load()可以读取配置文件,使用toml.dump()可以写入配置文件,返回一个字典。

1
2
3
4
import toml

config = toml.load('config.toml')
print(config['tool']['poetry']['name']) # my-project

csv与parquet

csv可以用来读取和写入csv文件,可以用来存储表格数据,可以用来读取和写入数据库。parquet可以用来读取和写入parquet文件,可以用来存储高维数据。两种相比,csv更适合存储结构化数据,parquet更适合存储高维数据。存储大型dataframe时paruqet在存取速度和文件大小上更有优势,且不会丢失数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pandas as pd
import pyarrow.parquet as pq
import random
df = pd.DataFrame({'dates': pd.date_range('2021-01-01', periods=1000000, freq='T'),
'categories': random.choices(['A', 'B', 'C']),
,k=1000000})
df['categories'] = df['categories'].astype('category')]
print(df.info())
# 存入
df.to_csv('data.csv', index=False)
df.to_parquet('data.parquet', index=False)

# 读取
csv_df = pd.read_csv('data.csv')
parquet_df = pd.read_parquet('data.parquet')
print(csv_df.info())
print(parquet_df.info())

模块、包导入和管理

init.py

在python中,init.py文件是用来标识一个目录为一个模块的,它可以为空,也可以包含模块的初始化代码。可以在其中定义包版本、作者等私有属性,也可以导入包里的其他模块,定义模块的公共接口。__all__变量可以用来指定模块的公共接口,在导入模块时,只导入__all__变量中指定的接口。

相对导入

假设目录结构如下:

1
2
3
4
5
6
7
8
9
10
my_package
├── __init__.py
├── A
├── __init__.py
└── a.py
└── c.py
└── B
├── __init__.py
└── b.py
main.py

对于一个python包里的模块c,相对导入必须使用from,如from . import a 和from …B import b.
相对导入使用的逻辑是从当前moudlue的package寻找,如果包里的模块作为第一个运行脚本,则它__package__会设置为None、name__会设置为__main,无法找到其他模块从而运行会报错。所以模块c使用了相对导入,就不能作为文件直接运行c.py,必须通过包外新建文件main.py导入模块再运行。

虚拟环境

python自带venv模块可以创建虚拟环境,可以安装第三方库,隔离不同项目的依赖关系。也可以使用conda创建虚拟环境。虚拟环境工具还有pipenv、poetry等。

1
2
3
python -m venv env 创建虚拟环境
source env/bin/activate 进入虚拟环境
deactivate 退出虚拟环境

实用语法

抽象基类

from abc import ABC, abstractmethod 可以定义抽象基类,并使用 @abstractmethod 装饰器来定义抽象方法,强制子类实现抽象方法。

Enum

from enum import Enum,IntEnum,strEnum,auto 可以限制变量的类型,每一个属性都是类的实例化即只有一个实例,auto()可以用来自动赋值,如果是正数从1开始。

可变对象和不可变对象

python中,字符串、数字、元组都是不可变对象,列表、字典是可变对象。不可变对象在赋值时,会创建新的对象,而可变对象在赋值时,会修改原对象。

函数参数的默认值不要使用可变对象

python中,函数参数的默认值是只在函数定义时执行一次,因此如果默认值是一个可变对象,则所有函数调用都会共享同一个对象,导致不可预期的结果。例如:

1
2
3
4
5
6
7
def append_to_list(lst=[]):
lst.append(1)
return lst

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

规范做法是使用None作为默认值,并在函数内部判断参数是否为None,如果为None,则初始化一个新的可变对象。例如:

1
2
3
4
5
6
7
8
9
10
from typing import Optional
def append_to_list(lst: Optional[list] = None) -> list:
if lst is None:
lst = []
lst.append(1)
return lst

print(append_to_list()) # [1]
print(append_to_list()) # [1]
print(append_to_list()) # [1]

切片对象

切片对象可以用来切片列表、字符串、元组、字典等,可以用来实现复杂的切片操作。切片对象可以指定起始位置、结束位置、步长,还可以指定切片的类型,例如:

1
2
3
4
5
lst = "hello, world!"
hello_slice = slice(0, 5, 1)
hello = lst[hello_slice] #等效于hello = lst[:5]
world_slice = slice(6, None, 1)
world = lst[world_slice] #等效于world = lst[6:]

match case python 3.10

match case可以用来匹配多种情况,可以用来简化条件判断,可以用来处理多种情况的分支,还可以用来处理异常。例如判断元素类型:

1
2
3
4
5
6
7
8
x = [w for w in range(10) if w % 2 == 0]
match x:
case int():
print('x is an integer')
case str():
print('x is a string')
case _:
print('x is something else')

*args和**kwargs

*args和**kwargs可以用来接收任意数量的位置参数和关键字参数,可以用来实现可变参数和关键字参数。
*可以用来打包和解包多个参数或者元组、列表,例如:

1
2
3
4
5
6
7
8
9
10
11
def my_func(*args):
print(args)

my_func(1, 2, 3) # (1, 2, 3)

l1 = [1, 2, 3]
l2 = [4, 5, 6]
print([*l1, *l2]) # [1, 2, 3, 4, 5, 6]

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

**可以用来打包和解包关键字参数和字典,例如:

1
2
3
4
5
6
7
8
9
10
11
12
def my_func(**kwargs):
print(kwargs)

my_func(name='Alice', age=25) # {'name': 'Alice', 'age': 25}

d1 = {'name': 'Alice', 'age': 25}

name, age = {'name': 'Alice', 'age': 25}.values()
print(name, age) # Alice 25

{**d1, 'city': 'Beijing'} # {'name': 'Alice', 'age': 25, 'city': 'Beijing'}

lamba\map\zip

lamba可以用来创建匿名函数,可以用来简化代码,可以用来实现高阶函数。例如:

1
2
add = lambda x, y: x + y
print(add(1, 2)) # 3

map接收两个参数,一个是函数,一个是可迭代对象,对可迭代对象中的每个元素应用函数,返回一个迭代器。

1
2
lst1 = [1, 2, 3]
print(list(map(lambda x: x * 2, lst1))) # [2, 4, 6]

zip可以打包多个可迭代对象,返回一个迭代器,迭代器的元素是每个可迭代对象对应位置的元素组成的元组。

1
2
3
4
lst1 = [1, 2, 3]
lst2 = [4, 5, 6]
result = list(zip(lst1, lst2))
print(result) # [(1, 4), (2, 5), (3, 6)]

类变量、类方法、静态方法

类变量是类的一个属性,放在实例方法外面,可以被所有实例共享,可以被类或实例访问。类变量用类名来表示,例如:

1
2
class MyClass:
count = 0

类方法是类的方法,第一个参数是cls,可以被类本身即cls调用,方法内可以访问类方法和修改类的属性,类方法用@classmethod装饰器来定义.
实例方法是实例的方法,第一个参数是self,可以被实例本身即self调用,方法内可以访问实例属性和类属性,但要通过cls_name.method_name()来调用类方法
静态方法不需要传入self或cls参数,可以被类或实例调用,方法内部不能访问实例属性和类属性。

魔法方法

python中有很多魔法方法,可以用来控制对象的行为,可以用来实现定制类,可以用来实现元类。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass:
def __init__(self, name):
self.name = name

def __str__(self):
return f'MyClass object (name: {self.name})'

def __len__(self):
return len(self.name)

def __getitem__(self, key):
return self.name[key]

def __setitem__(self, key, value):
self.name[key] = value
def __delitem__(self, key):
del self.name[key]
# 将类的实例当作函数调用
def __call__(self, *args, **kwargs):
print(f'Calling {self.name} with {args} and {kwargs}')

迭代器、生成器、表达式、推导式

迭代器和生成器

迭代器和生成器是python中两个重要的概念,可以用来遍历集合、生成值,可以用来节省内存,可以用来实现惰性计算。比如读取文件时,不一次性读取所有内容,而是按需读取,可以节省内存。

迭代器是实现了__iter__()和__next__()方法的对象,可以用for循环来遍历,或者用next()方法来获取下一个值。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyIterator:
def __init__(self, filename):
self.file= open(filename, 'r')

def __iter__(self):
return self

def __next__(self):
line = self.file.readline()
while line:
if line.strip()[0]=="target":
return line
line = self.file.readline()
self.file.close()
raise StopIteration
line_iter = MyIterator('test.txt')
for target_line in line_iter:
print(target_line)

生成器是使用yield关键字的函数,可以理解为简化版的迭代器,可以用for循环来遍历,或者用next()方法来获取下一个值。例如:

1
2
3
4
5
6
7
8
9
10
def my_generator(filename):
with open(filename, 'r') as f:
line = f.readline()
while line:
if line.strip()[0]=="target":
yield line
line = f.readline()
line_gen = my_generator('test.txt')
for target_line in line_gen:
print(target_line)

列表推导式\生成器表达式\元组推导式

两者的区别主要在于一个用[]表示,一个用()表示,换言之,列表和元组推导式相当于在生成器表达式的基础上加上了list()和tuple()进行元素搜集和转换,故生成器表达式在效率和内存方面远优于列表推导式。
列表推导式可以用来创建列表,可以用来简化循环,可以用来过滤、排序、映射等。列表推导式存在多重循环时,前面的for是外循环,后面的for是内循环。例如:

1
lst = [x for x in range(10) if x % 2 == 0]

生成器表达式可以用来创建生成器,可以用来节省内存,可以用来实现惰性计算。例如:

1
gen = (x for x in range(10) if x % 2 == 0)

元组推导式可以用来创建元组,和生成器表达式类似,但返回的不是生成器,而是元组。例如:

1
tup = tuple(x for x in range(10) if x % 2 == 0)

实用标准库

单元测试:unittest和mock

通过unittest和assert可以进行单元测试,可以测试函数的输入输出是否符合预期,可以测试函数的执行时间,可以测试函数的异常情况。加上虚拟对象mock可以模拟真实对象,限定函数的输入输出,排除外部接口和环境的影响

异步编程:asyncio

asyncio可以用来编写异步代码,可以用来实现并发、并行,可以用来处理网络、IO密集型任务。协程函数不会直接执行,而是返回一个协程对象,需要使用asyncio.run()或loop.run_until_complete()建立事件循环,才会真正执行协程。使用协程的步骤为:

  1. 使用async和await关键字等定义协程函数
  2. 使用asyncio.create_task()将协程包装成任务
  3. 使用asyncio.run()启动事件循环:检查协程、让出控制权、处理事件、等待协程
  • await 关键字用来暂停协程,直到await后的任务完成,也可以将await后的协程函数自动包装成任务
  • asyncio.gather()可以将多个任务并发执行,全部任务完成后返回结果列表
  • asyncio.as_completed()可以将多个任务并发执行,并返回一个生成器,可以迭代获取每个任务的结果,不必等待所有任务完成
    例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import asyncio

async def hello():
await asyncio.sleep(1)
print('Hello, world!')
return 'done'

async def main():
start_time = asyncio.get_running_loop().time()
tasks = [asyncio.create_task(hello()) for _ in range(3)]
results = await asyncio.gather(*tasks)
end_time = asyncio.get_running_loop().time()
print(f'Results: {results}')
print(f'Time taken: {end_time - start_time:.2f} seconds')

asyncio.run(main())
# output:
# Hello, world!
# Hello, world!
# Hello, world!
# Results: ['done', 'done', 'done']
# Time taken: 1.01 seconds

基于协程实现的第三方库有aiohttp、aiomysql等,不同场景可以使用不同的协程库。

实用第三方库

进度条:tqdm

tqdm可以用来显示进度条,可以用来显示循环的进度,可以用来显示下载文件的进度,也可以手动设置进度条。

1
2
3
4
5
6
7
8
9
from tqdm import tqdm

for i in tqdm(range(10)):
pass
pbar = tqdm(total=100, desc='Downloading', unit='B', unit_scale=True, unit_divisor=1024)
pbar.update(10)
sleep(0.1)
pbar.update(90)
pbar.close()