设计模式、设计原则、编程方法论
设计模式是一种编程方法论,它提供了一套被反复使用、多数人知晓的、经过分类编排的、代码设计经验的总结。设计模式是软件工程的基石,是用来解决特定问题的可重用、可扩展的设计原则。
工厂模式
创建对象时,将对象的创建过程封装在一个工厂类中,通过工厂类来创建对象,而不是直接创建对象。将对象的创建过程和使用过程分离,可以提高代码的可扩展性。
例如:
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() shape2 = ShapeFactory.create_shape('rectangle' ) shape2.draw()
建造者模式
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。例如:
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) print (car2)
单例模式
保证一个类只有一个实例,节省内存,避免多次实例化,可以使用类变量、静态方法或装饰器来实现。比如使用__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))
类、函数自动化生成、代码简化
装饰器
装饰器可以用来修改函数的行为,可以用来实现面向切面编程,可以用来实现日志、性能监控、缓存、事务处理等功能。定义装饰器时,可以接收函数作为参数,并返回修改后的函数。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import functoolsdef 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()
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 ) 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) p = Point(1 , 2 ) print (p) print (p.z) print (p.x) print (p.y)
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 namedtuplePoint = namedtuple('Point' , ['x' , 'y' ]) p = Point(1 , 2 ) print (p.x, p.y) print (p[0 ], p[1 ]) print (p._asdict())
文件、配置读取与写入
dotenv读取敏感信息作为环境变量
dotenv可以用来读取环境变量,可以用来存储敏感信息,可以用来实现配置管理。dotenv文件可以放在项目根目录,也可以放在用户目录。一般dotenv文件要放在.gitignore中,以免泄露敏感信息。dotenv文件中,每一行包含一个环境变量,格式为"key=value",可以用os.getenv()方法读取环境变量。默认读取的是.env文件,也可以指定文件名为my.env,例如:
1 2 3 4 5 6 7 import osfrom dotenv import load_dotenvload_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 tomlconfig = toml.load('config.toml' ) print (config['tool' ]['poetry' ]['name' ])
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 pdimport pyarrow.parquet as pqimport randomdf = 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())
模块、包导入和管理
在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()) print (append_to_list()) print (append_to_list())
规范做法是使用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()) print (append_to_list()) print (append_to_list())
切片对象
切片对象可以用来切片列表、字符串、元组、字典等,可以用来实现复杂的切片操作。切片对象可以指定起始位置、结束位置、步长,还可以指定切片的类型,例如:
1 2 3 4 5 lst = "hello, world!" hello_slice = slice (0 , 5 , 1 ) hello = lst[hello_slice] world_slice = slice (6 , None , 1 ) world = lst[world_slice]
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 ) l1 = [1 , 2 , 3 ] l2 = [4 , 5 , 6 ] print ([*l1, *l2]) a, b, *rest = [1 , 2 , 3 , 4 , 5 ] print (a, b, rest)
**可以用来打包和解包关键字参数和字典,例如:
1 2 3 4 5 6 7 8 9 10 11 12 def my_func (**kwargs ): print (kwargs) my_func(name='Alice' , age=25 ) d1 = {'name' : 'Alice' , 'age' : 25 } name, age = {'name' : 'Alice' , 'age' : 25 }.values() print (name, age) {**d1, 'city' : 'Beijing' }
lamba\map\zip
lamba可以用来创建匿名函数,可以用来简化代码,可以用来实现高阶函数。例如:
1 2 add = lambda x, y: x + y print (add(1 , 2 ))
map接收两个参数,一个是函数,一个是可迭代对象,对可迭代对象中的每个元素应用函数,返回一个迭代器。
1 2 lst1 = [1 , 2 , 3 ] print (list (map (lambda x: x * 2 , lst1)))
zip可以打包多个可迭代对象,返回一个迭代器,迭代器的元素是每个可迭代对象对应位置的元素组成的元组。
1 2 3 4 lst1 = [1 , 2 , 3 ] lst2 = [4 , 5 , 6 ] result = list (zip (lst1, lst2)) print (result)
类变量、类方法、静态方法
类变量是类的一个属性,放在实例方法外面,可以被所有实例共享,可以被类或实例访问。类变量用类名来表示,例如:
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()建立事件循环,才会真正执行协程。使用协程的步骤为:
使用async和await关键字等定义协程函数
使用asyncio.create_task()将协程包装成任务
使用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 asyncioasync 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:.2 f} seconds' ) asyncio.run(main())
基于协程实现的第三方库有aiohttp、aiomysql等,不同场景可以使用不同的协程库。
实用第三方库
进度条:tqdm
tqdm可以用来显示进度条,可以用来显示循环的进度,可以用来显示下载文件的进度,也可以手动设置进度条。
1 2 3 4 5 6 7 8 9 from tqdm import tqdmfor 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()