Python面向对象编程进阶
面向对象
面向对象(Object-Oriented,简称OO)是一种编程范式,它将数据和操作数据的方法封装在一起,形成一个对象。面向对象的编程思想强调对象的概念,将现实世界中的事物抽象成对象,通过对象之间的交互来实现程序的功能。
面向对象3大特性
- 封装:将数据和操作数据的方法封装在一起,形成一个对象,隐藏对象的内部实现细节,只暴露必要的接口。
- 继承:可以从已有的类中派生出新的类,新的类可以继承已有类的属性和方法,并可以添加新的属性和方法。
- 多态:同一个方法可以在不同的对象上产生不同的行为,这种特性称为多态。多态可以通过继承、接口、重载等方式实现。
面向对象的编程思想可以提高程序的可维护性、可扩展性和可重用性,使程序更加模块化和灵活。在Python中,面向对象的编程思想得到了广泛的应用,Python中的所有数据类型都是对象,Python中的函数也可以看作是对象的一种。通过面向对象的编程思想,可以更好地组织和管理Python程序的代码。
一切皆对象
Python 中的所有数据类型都是对象,包括数字、字符串、列表、元组、字典、函数等。在 Python 中,对象是一种数据结构,它包含了数据和方法。对象的数据部分可以是任何类型的数据,而对象的方法部分是一组可以操作对象的函数。
>>> dir(1)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', ...]
>>> (1).__class__
<class 'int'>
>>> (1).__add__(2)
3
Python中的常见对象
函数对象
在Python中,函数也是一种对象,类型为<class 'function'>,属于types.FunctionType类的实例。
属性 | 返回类型 | 说明 |
---|---|---|
__name__ |
str | 函数名 |
__qualname__ |
str | 函数限定名,例如,函数在一个类中时,其限定名为 类名.函数名 |
__doc__ |
str or None | 函数注释(docstring) |
__annotations__ |
dict or None | 函数函数参数及返回值类型注释 |
__module__ |
str | 函数所属模块名 |
__class__ |
类对象 | 函数所属类(对象) |
__code__ |
code对象 | 函数代码(对象) |
__defaults__ |
tuple or None | 函数参数默认值 |
__kwdefaults__ |
tuple or None | 函数限定关键字参数默认值 |
__closure__ |
tuple of cell or None | 闭包函数中引用的cell变量 |
类对象
在Python中,类也是一种对象,所属类为<class 'type'>,属于type类的实例。
属性 | 返回类型 | 说明 |
---|---|---|
__name__ |
str | 类名 |
__doc__ |
str or None | 类注释(docstring) |
__module__ |
str | 类所属模块名 |
__class__ |
类对象 | |
__dict__ |
dict | 属性字典 |
模块及包对象
在Python中,模块及包是一种同一种对象,类型为<class 'module'>,属于types.ModuleType的实例。
属性 | 返回类型 | 说明 |
---|---|---|
__name__ |
str | 模块名,作为主模块时,模块名为固定的字符串__main__ |
__doc__ |
str or None | 模块注释(docstring) |
__file__ |
str | 模块所在脚本路径 |
__package__ |
str or None | 模块所包名,作为主模块时__package__为None |
__buildins__ |
dict | Python内置关键字及函数 |
Code对象
属性 | 返回类型 | 说明 |
---|---|---|
co_filename | str | 代码所在文件路径 |
co_firstlineno | int | 代码第一行行号 |
co_argcount | int | 常规参数数量 |
co_posonlyargcount | int | 限定位置参数数量 |
co_kwonlyargcount | int | 限定关键字参数数量 |
co_nlocals | int | 局部变量数量 |
co_varnames | tuple | 局部变量名列表 |
co_freevars | tuple | 自由变量名列表 |
co_cellvars | tuple | cell变量(多作用域变量)名列表 |
co_names | tuple | 引用其他函数名列表 |
co_consts | tuple | 所使用常量列表,包含函数的docstring、内部函数等 |
co_code | bytes | 编译后的二进制操作码(opcodes) |
co_lnotab | bytes | 地址及代码行号映射编码后的二进制 |
co_flags | int | |
co_stacksize | int |
对象检查-inspect模块基本使用
inspect 模块提供了一些有用的函数帮助获取对象的信息,例如模块、类、方法、函数、Trackback、Frame对象以及代码对象。例如它可以帮助你检查类的内容,获取某个方法的源代码,取得并格式化某个函数的参数列表,或者获取你需要显示的回溯的详细信息。
该模块提供了4种主要的功能:类型检查、获取源代码、检查类与函数、检查解释器的调用堆栈。
- 类型检查:ismodule() / isclass() / ismethod() / isfunction() / ...
- 获取源代码:getsource() / getsourcefile()/ getmodule() / getmembers()/...
- 检查函数和类:getargspec() / getargvalues() / getcallargs() / ...
- 检查解释器:currentframe() / getframeinfo() / stack() / trace() / ...
参考:https://docs.python.org/zh-cn/3.9/library/inspect.html#types-and-members
鸭子类型
鸭子类型(Duck Typing)是一种动态类型的编程方式,它的核心思想是“如果它走起路来像鸭子,叫起来也像鸭子,那么它就是鸭子”。换句话说,鸭子类型是一种基于对象行为而非对象类型的编程方式。
在鸭子类型中,对象的类型并不重要,重要的是对象是否具有特定的方法或属性。如果一个对象具有特定的方法或属性,那么它就可以被视为另一种类型的对象。例如,如果一个对象具有 len() 方法,那么它就可以被视为一个序列类型的对象,无论它的实际类型是什么。
from collections import OrderedDict
d = OrderedDict() # 创建一个有序字典
d['a'] = 1 # 向有序字典中添加元素
d.update({'b':2, 'c':3})
for key, value in d.items(): # 按照插入顺序迭代有序字典中的元素
print(key, value)
额外的容器类型-collections
在Python内置collections中提供了一些额外的容器类型,这些类型可以像基本类型(元祖、列表、字典等)一样使用。
- namedtuple(具名元祖):具名元祖,支持通过名称访问其中元素
- deque(双端列表):双端队列,支持快速从头部或尾部插入元素
- ChainMap (链接字典):将多个映射集合到一个视图里面
- Counter(计数器):提供了可哈希对象的计数功能
- OrderedDict(有序字典):保存了他们被添加的顺序
- defaultdict(默认值字典):为字典查询提供一个默认值
- UserDict(用户字典):用于用户继承并自定义字典类型
- UserList(用户列表):用于用户继承并自定义列表类型
魔术方法
Python 中的魔术方法(Magic Methods)是一种特殊的方法,它们以双下划线开头和结尾,例如__init__
、__str__
、__add__
等。
魔术方法通常用于定义类的行为和操作,例如初始化对象、打印对象、比较对象等。
对象生命周期
对象生命周期是指对象的创建、初始化、调用及销毁过程。
魔术方法 | 说明 | 示例 |
---|---|---|
__new__(cls, *args, **kwargs) |
控制对象创建(构造函数) | a = A(...) |
__init__(self, ...) |
控制对象初始化(如属性绑定) | |
__del__(self) |
控制对象删除(析构函数) | del a |
__call__(self, ...) |
实现对象类似函数的调用行为 | a(...) |
注意:new(cls, *args, **kwargs)方法是一个类方法,cls代表当前类,bases是当前类的所有父类,attrs是当前类所有传入的属性。创建对象时,先执行__new__进行创建后才执行__init__方法。
示例-对象作为函数使用
通过实现魔术方法__call__可以将对象作为函数进行调用,示例如下。
class Calc:
def __init__(self, method):
self.method = method
def __call__(self, a, b): # 实现对象调用功能
if self.method == 'add':
return a + b
# ...
add = Calc('add')
print(add(2, 3)) # 5
对象表示
对象表示指对象区别与其他对象的表现形式,如对象的文本表示、真假值、哈希值等。
魔术方法 | 说明 | 示例 |
---|---|---|
__str__(self) |
实现对象的字符串形式 | str(a) 或 print(a) |
__repr__(self) |
实现对象的真实表示 | repr(a) |
__format__(self, formatstr) |
实现对象的字符串格式化操作 | {0:formatstr}.format(a) |
__hash__(self) |
实现对象的哈希值 | hash(a) |
__bool__(self) |
实现对象的真假值判断 | bool(a) 或 if a: ... |
__dir__(self) |
实现对象的属性列表 | dir(a) |
__sizeof__(self) |
实现对象的大小计算 | sys.getsizeof(a) |
控制属性访问
Python类中设置了关于属性获取、设置、删除时统一的控制方法,以及获取不存在属性时的操作方法。
魔术方法 | 说明 | 示例 |
---|---|---|
__getattribute__(self,name) |
实现获取对象属性 | a.name |
__hasattr__(self, name) |
实现判断对象是否有属性 | hasattr(a, name) |
__getattr__(self, name) |
实现获取对象不存在的属性 | a.name2或getattr(a,name2) |
__setattr__(self, name, value) |
实现设置对象属性 | setattr(a, name, value) |
__delattr__(self, name) |
实现删除对象属性 | delattr(a, name) |
示例-访问属性时定位元素
利用魔术方法__getattr__
可以将对象原本没有的属性,按对象属性直接使用,示例如下。
import time
from selenium.webdriver import Chrome
class Page:
elements = {'keyword_ipt': ('id', 'kw'), 'submit_btn': ('id', 'su')}
def __init__(self, driver, url=None):
self.driver = driver
self.url = url
def open(self, url=None):
url = url or self.url
self.driver.get(url)
def __getattr__(self, name):
if name in self.elements:
return self.driver.find_element(*self.elements[name])
raise AttributeError('未配置元素定位方式: {name}')
driver = Chrome()
baidu = Page(driver, 'https://www.baidu.com/')
baidu.open()
baidu.keyword_ipt.send_keys('篮球') # 访问元素keyword_ipt属性时促发定位操作
baidu.submit_btn.click() # 访问元素submit_btn属性时促发定位操作
time.sleep(5)
driver.quit()
对象作为描述符
Python描述符(Descriptor)是一种特殊的Python对象,它可以控制属性的访问和赋值操作。描述符可以用于实现属性的访问控制、类型检查、属性计算等功能。
Python描述符必须实现__get__、set__和__delete__三个方法中的至少一个,这些方法分别用于控制属性的获取、设置和删除操作。当一个对象被用作描述符时,它可以被其他对象所引用,这些对象可以通过调用描述符的__get、__set__和__delete__方法来访问和修改属性。
魔术方法 | 说明 | 示例 |
---|---|---|
__get__(self, instance, owner) |
实现作为指定对象属性时的获取值行为 | class B: a = A(); print(B().a) |
__set__(self, instance, value) |
实现作为指定对象属性时的赋值行为 | class B: a = A(); B().a = value |
__delete__(self, instance) |
实现作为指定对象属性时的删除行为 | class B: a = A(); del B().a |
示例-对象作为描述符
常见的描述符使用场景是对象作为类属性时,控制该属性的访问、设置及删除操作。
class A: # 描述符类
def __get__(self, instance, owner):
print(“获取值”)
return instance._a
def __set__(self, instance, value):
print(“设置值”)
if not isinstance(value, int):
raise ValueError(“必须是整数”)
instance._a = value
def __delete__(self, instance):
print(“删除值”)
del instance._a
class B:
a = A() # 对象作为描述符使用
def __init__(self, value=1):
self._a = value
b = B(10)
print(b.a)
B().a = 20
del b.a
对象比较
在某些情况下,对象需要支持大小比较功能(如数字类型、字符串类型等),Python中可以通过实现特定的魔术方法来实现对象的比较。
魔术方法 | 说明 | 示例 |
---|---|---|
__eq__(self, other) |
实现相等比较 | a == other |
__ne__(self, other) |
实现不相等比较 | a != other |
__lt__(self, other) |
实现小于比较 | a < other |
__gt__(self, other) |
实现大于比较 | a > other |
__le__(self, other) |
实现小于等于比较 | a <= other |
__ge__(self, other) |
实现大于等于比较 | a >= other |
示例-对象比较
在某些情况下,对象需要支持大小比较功能(如数字类型、字符串类型等),Python中可以通过实现特定的魔术方法来实现对象的比较。
class Person:
def __init__(self, name, gender, age):
self.name = name
self.gender = gender
self.age = age
def __eq__(self, other):
assert isinstance(other, Person), '只支持和同样Person对象比较'
return self.age == other.age # 年龄相等视为相等
def __gt__(self, other):
assert isinstance(other, Person), '只支持和同样Person对象比较'
return self.age > other.age # 按年龄比较
def __ge__(self, other):
assert isinstance(other, Person), '只支持和同样Person对象比较'
return self.age >= other.age # 按年龄比较
zhang_san = Person('张三', '男', 15)
li_si = Person('李四', '女', 16)
print(zhang_san > li_si) # False
print(zhang_san <= li_si) # True
对象拷贝
在Python中,对象的赋值和拷贝操作涉及到深拷贝和浅拷贝的概念。
- 浅拷贝(Shallow Copy)是指创建一个新的对象,该对象与原始对象共享内存中的数据。浅拷贝只复制对象的引用,而不复制对象本身。当原始对象发生变化时,浅拷贝对象也会发生变化。
- 深拷贝(Deep Copy)是指创建一个新的对象,该对象与原始对象不共享内存中的数据。深拷贝会递归地复制对象及其子对象,直到所有对象都被复制。当原始对象发生变化时,深拷贝对象不会发生变化。
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a) # 浅拷贝 b = a 赋值也是浅拷贝
c = copy.deepcopy(a) # 深拷贝
a[2][0] = 5 # 修改a
print(a) # [1, 2, [5, 4]]
print(b) # 受影响 [1, 2, [5, 4]]
print(c) # 不受影响 [1, 2, [3, 4]]
魔术方法 | 说明 | 示例 |
---|---|---|
__copy__(self) |
实现对象的浅拷贝 | copy.copy(a) |
__deepcopy__(self, memodict={}) |
实现相等的深拷贝 | copy.deepcopy(a) |
算术运算
魔术方法 | 说明 | 示例 |
---|---|---|
__add__(self, other) |
实现加法 | a + other |
__sub__(self, other) |
实现减法 | a - other |
__mul__(self, other) |
实现乘法 | a * other |
__truediv__(self, other) |
实现除法 | a / other |
__floordiv__(self, other) |
实现整数除法 | a // other |
__mod__(self, other) |
实现取模(取余数) | a % other |
__divmod__(self, other) |
实现整数除及取模 | divmod(a, other) |
__pow__(self,other) |
实现乘方 | a ** other |
__lshift__(self, other) |
实现按位左移 | a << other |
__rshift__(self, other) |
实现按位右移 | a >> other |
__and__(self, other) |
实现按位与 | a & other |
__or__(self, other) |
实现按位或 | a |
__xor__(self, other) |
实现按位异或 | a ^ other |
数字类型的对象的各种运算操作本质上也是通过各种魔术方法实现的。
我们也可以通过实现对应的魔术方法来使我们的对象支持各种运算。
位运算主要是二进制数据等一些运算操作。
示例-利用除法完成特殊操作
由于算术运算不同的魔术方法对应不同的运算符和,如+、-、*、/、%等,我们可以通过实现对应的魔术方法来实现特定的操作。以下是一个使用除法符合完成的路径连接操作。
import os.path
class Path:
def __init__(self, path: str):
self._path = path
def __str__(self):
return self._path
def __truediv__(self, other): # 利用除法符号完成路径连接
self._path = os.path.join(self._path, other)
return self # 返回当前对象以支持连续操作
root = Path('/')
path = root / 'home' / 'hzc' / 'data'
print(path) # 输出结果: /home/hzc/data
反向运算
魔术方法 | 说明 | 示例 |
---|---|---|
__radd__(self, other) |
实现反向加法 | other + a |
__rsub__(self, other) |
实现反向减法 | other - a |
__rmul__(self, other) |
实现反向乘法 | other * a |
__rdiv__(self, other) |
实现反向除法 | other / a |
__rfloordiv__(self, other) |
实现反向整数除法 | other // a |
__rmod__(self, other) |
实现反向取模(取余数) | other % a |
__rdivmod__(self, other ) |
实现反向整数除及取模 | divmod(other, a) |
__rpow__(self,other) |
实现反向乘方 | other ** a |
__rlshift__(self, other) |
实现按位左移 | other << a |
__rrshift__(self, other) |
实现按位右移 | other >> a |
__rand__(self, other) |
实现按位与 | other & a |
__ror__(self, other) |
实现按位或 | other | a |
__rxor__(self, other) |
实现按位异或 | other ^ a |
反向运算是对象在运算符右边时的一种运算,当运算符左边对象不支持该运算时,会尝试调用右边对象对应的反运算。
例如,other + a,如果other对象没有实现__add__方法,则尝试使用a对象的__radd__方法进行运算。
自运算
自运算是将运算结果重新赋值给当前变量的一种操作,如自增、自减等。
魔术方法 | 说明 | 示例 |
---|---|---|
__iadd__(self, other) |
实现反向加法 | a += other |
__isub__(self, other) |
实现反向减法 | a -= other |
__imul__(self, other) |
实现反向乘法 | a *= other |
__idiv__(self, other) |
实现反向除法 | a /= other |
__ifloordiv__(self, other) |
实现反向整数除法 | a //= other |
__imod__(self, other) |
实现反向取模(取余数) | a %= other |
__ipow__(self,other) |
实现反向乘方 | a **= other |
__ilshift__(self, other) |
实现按位左移 | a <<= other |
__irshift__(self, other) |
实现按位右移 | a >>= other |
__iand__(self, other) |
实现按位与 | a &= other |
__ior__(self, other) |
实现按位或 | a |= other |
__ixor__(self, other) |
实现按位异或 | a ^= other |
一元运算
一元运算是单个数的运算,如正数、负数、取反等。
魔术方法 | 说明 | 示例 |
---|---|---|
__pos__(self) |
实现正数行为 | +a |
__neg__(self) |
实现负数行为 | -a |
__abs__(self) |
实现绝对值行为 | abs(a) |
__invert__(self) |
实现取反行为 | ~a |
__round__(self, n) |
实现保留n位小数行为 | round(a, 2) |
__floor__(self) |
实现向下取整行为 | math.floor(a) |
__ceil__(self) |
实现向上取整行为 | math.ceil(a) |
__trunc__(self) |
实现截断为积分行为 | math.trunc(a) |
示例-利用取反符实现列表倒序
我们也可以利用对应的魔术方法,来实现特定的操作,下面是一个自定义列表,支持取反操作的示例
from collections import UserList
class MyList(UserList):
def __invert__(self): # 用取反符实现倒序
return self.data[::-1]
a = MyList([1,2,3,4,5])
print(~a) # [5, 4, 3, 2, 1]
类型转换
Python同样为对象提供了在面临转为不同类型时的魔术方法。也包含之前的__str__、__bool__等。
魔术方法 | 说明 | 示例 |
---|---|---|
__int__(self) |
实现转为int类型 | int(a) |
__float__(self ) |
实现转为float类型 | float(a) |
__complex__(self) |
实现转为复数类型 | complex(a) |
__oct__(self) |
实现转为8进制 | oct(a) |
__hex__(self) |
实现转为16进制 | hex(a) |
__floor__(self) |
实现向下取整行为 | math.floor(a) |
__ceil__(self) |
实现向上取整行为 | math.ceil(a) |
__index__(self) |
实现作为索引时的值 | array[a] |
容器类型
对于列表、字典等容器类型,Python提供了长度计算、获取元素、设置元素、删除元素等魔术方法。
魔术方法 | 说明 | 示例 |
---|---|---|
__len__(self) |
返回容器的长度 | len(a) |
__getitem__(self, key) |
根据索引或key返回容器中元素 | a[key] |
__setitem__(self, key, value) |
根据索引或key对容器中元素进行赋值 | a[key] = value |
__delitem__(self, key) |
删除容器中指定的索引或key | del a[key] |
__missing__(self,key) |
获取元素时找不到时的返回值 | a[key] |
__contains__(self, item) |
判断元素是否在容器中 | if item in a: ... |
__iter__(self) |
实现迭代器遍历 | for i in a: ... |
__next__(self) |
实现迭代器获取下一项 | next(a) |
__reversed__(self) |
实现容器反向排序 | reversed(a) |
上下文管理器
上下文管理器是Python中一种特殊的语法形式,常用于自动释放资源,异常处理等。
魔术方法 | 说明 | 示例 |
---|---|---|
__enter__(self) |
实现进入上下文管理器行为 | with A() as a: ... |
__exit__(self, exception_type, exception_value, traceback) |
实现退出上下文管理器行为 | with A() as a: pass # do something |
对象序列化
魔术方法 | 说明 | 示例 |
---|---|---|
__getstate__(self) |
实现进入上下文管理器行为 | pickle.dump(a) |
__setstate__(self, state) |
实现退出上下文管理器行为 | pickle.load(a, state) |
__getinitargs__(self) |
||
__getnewargs__(self) |
||
__reduce_ex__(self) |
对象之间的关系
在面向对象编程中,对象之间的关系可以通过组合、聚合和继承来描述。
- 组合(Composition):是指一个对象包含另一个对象,被包含的对象是组合对象的一部分,它们的生命周期是一致的。组合关系通常用于描述整体与部分之间的关系,例如汽车和发动机的关系,房子和房间的关系等。
- 聚合(Aggregation):是指一个对象包含另一个对象,被包含的对象不是组合对象的一部分,它们的生命周期可以不一致。聚合关系通常用于描述整体与部分之间的关系,但是部分可以独立存在,例如学校和学生的关系,公司和员工的关系等。
- 继承(Inheritance):是指一个类从另一个类继承属性和方法,被继承的类称为父类或基类,继承的类称为子类或派生类。继承关系通常用于描述类之间的层次关系,例如动物和猫的关系,人和学生的关系等。
组合
组合关系是一个对象是另外一个对象的组成部分,不单独存在。
class Engine: # 引擎是汽车的一部分-不离开车单独存在
def start(self):
print("启动引擎")
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
self.engine.start()
car = Car()
car.start() # 启动引擎
聚合
聚合关系是一个对象可以包含或关联多个其他对象,所包含的对象也可以单独存在。
class Student:
def __init__(self, name):
self.name = name
class Course:
def __init__(self, name):
self.name = name
self.students = [] # 课程可以多个参与的学生
def add_student(self, student):
self.students.append(student)
class School:
def __init__(self):
self.courses = [] # 学校可以包含多门开设的课程
def add_course(self, course):
self.courses.append(course)
def add_student(self, student, course_name):
for course in self.courses:
if course.name == course_name:
course.add_student(student)
school = School()
math_course = Course("Math")
school.add_course(math_course)
alice = Student("Alice")
school.add_student(alice, "Math")
继承
继承关系一般是类型的细分或概念的具体化,子类可以包含父类的所有方法。
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Cat(Animal):
def speak(self):
print("Meow")
class Dog(Animal):
def speak(self):
print("Woof")
cat = Cat("Kitty")
dog = Dog("Buddy")
cat.speak() # Meow
dog.speak() # Woof
继承与聚合的区别
继承适用于以下场景:
- 子类与父类之间具有相似的属性和方法,但是子类需要扩展或修改父类的行为。
- 子类需要继承父类的接口和实现,以实现代码的复用和扩展。
- 子类需要实现父类的抽象方法,以实现多态性和接口规范。
例如,在实现一个图形库时,可以定义一个Shape类作为所有图形的基类,然后定义Rectangle类和Circle类作为Shape类的子类,以实现不同形状的图形。Rectangle类和Circle类可以继承Shape类的属性和方法,同时扩展或修改Shape类的行为,以实现不同形状的图形。
聚合适用于以下场景:
- 一个对象包含另一个对象,但是被包含的对象不是组合对象的一部分,它们的生命周期可以不一致。
- 一个对象需要包含多个对象,以实现复杂的功能和行为。
- 一个对象需要动态地添加或删除其他对象,以实现灵活的组合和扩展。
例如,在实现一个电商网站时,可以定义一个Order类作为订单的基类,然后定义Product类作为商品的类,以实现商品和订单之间的聚合关系。Order类包含多个Product对象,每个Product对象表示一个商品,它们之间是聚合关系。Order类可以动态地添加或删除Product对象,以实现灵活的组合和扩展。
继承与聚合的区别示例
继承和聚合的选择的关键点在于,是不是同一种类型范畴,或要不要继续作为上一个类型的子类型还是一个全新的类型,两种在使用其他对象方法时上稍有不同,示例如下:
import requests
class Http:
def __init__(self):
self.session = requests.session() # 聚合requests库的Session对象
def post(self, url,**kwargs):
return self.session.get(url, **kwargs)
# ...
class YApi(Http): # 继承Http
def add_project(self):
res = self.post('/api/project/add', ...) # 通过self直接使用父类方法
class YApi2:
def __init__(self):
self.http_client = Http() # 聚合Http操作
def add_project(self):
res = self.http_client.post('/api/project/add', ...) # 通过聚合的属性对象进行操作
MixIn类
MixIn类是一种特殊的类,它通常用于实现类的复用和组合。MixIn类不是独立的类,而是一组方法和属性的集合,它可以被其他类继承或混合使用,以实现类的功能扩展和复用。
MixIn类通常具有以下特点:
- MixIn类通常不包含状态信息,它只包含方法和属性的集合。
- MixIn类通常不直接实例化,它只用于被其他类继承或混合使用。
- MixIn类通常命名以Mixin结尾,例如LoggingMixin、AssertionMixIn等。
class LoggingMixin:
def log(self, message):
print(f"[{self.__class__.__name__}] {message}")
class AssertionMixin:
def assert_true(self, value, message=None):
message = message or f'{value} is not True'
assert value is True, message
class MyTestCase(LoggingMixin, AssertionMixin):
def run_test(self):
self.log('期望 1+1 > 2')
self.assert_true(1 + 1 > 2)
MyTestCase().run_test()
抽象类
抽象类是一种特殊的类,它不能被实例化,只能被用作其他类的基类。抽象类通常用于定义一组接口或规范,而不是具体的实现。子类必须实现抽象类中定义的所有方法,否则会引发 TypeError 异常。
可以使用 abc 模块来定义抽象类。abc 模块提供了 ABC 类和 abstractmethod 装饰器,用于定义抽象类和抽象方法。
from abc import ABC, abstractmethod
class HostBase(ABC): # 定义一个抽象类
@abstractmethod
def execute(self, cmd): # 子类必须实现的抽象方法
pass
面向接口编程
Python3 中的面向接口编程是一种编程方式,它强调程序设计应该基于接口而不是实现。接口是一组方法或属性的集合,它定义了一个对象的行为和功能,而不关心对象的具体实现。
在 Python3 中,可以使用抽象类或接口类来定义接口。
面向接口编程优点
- 使代码更加灵活和可扩展
- 提高代码的可读性和可维护性
class LocalHost(HostBase): # 子类
def execute(self, cmd): # 子类实现父类接口
# ...
class RemoteHost(HostBase): # 子类
def execute(self, cmd): # 子类实现父类接口
# ...
面向接口编程-示例
这里我们实现一个服务器执行命令并返回结果的方法,支持本地服务器和远程服务器。
首先我们用抽象方法来定义子类必须实现的接口。
对于本地命令的执行,我们可以用子进程subprocess模块实现,代码如下:
import subprocess
from abc import ABC, abstractmethod
class HostBase(ABC): # 定义一个抽象类
@abstractmethod
def execute(self, cmd: str) -> str: # 子类必须实现的抽象方法
pass
class LocalHost(HostBase): # 子类1
def __init__(self, **kwargs):
self.hostname = 'localhost'
def execute(self, cmd: str) -> str: # 子类实现父类接口
process = subprocess.run(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, shell=True)
if process.stderr:
raise RuntimeError(f'[{self.hostname}] 执行命令 {cmd} 报错: {process.stderr.decode()}')
return process.stdout.decode().strip()
对于远程命令的执行,我们可以使用三方库paramiko实现(需要pip安装),使用方式如下:
首先要先创建一个SSHClient对象,然后连接远程服务器(需要设置默认的密钥缺失策略)。
使用SSHClient对象的exec_command执行命令后便可拿到标准输入、标准输出及标准错误输出相关信息。
# ... 接上面代码
import paramiko
class RemoteHost(HostBase): # 子类2
def __init__(self, hostname, port=22, username='root', password=''):
self.hostname = hostname
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh.connect(hostname=hostname, port=port, username=username, password=password)
def execute(self, cmd): # 子类实现父类接口
stdin, stdout, stderr = self.ssh.exec_command(cmd)
if stderr:
raise RuntimeError(f'[{self.hostname}] 执行命令 {cmd} 报错: {stderr.read().decode()}')
return stdout.read().decode().strip()
元类
元类(metaclass)是一种特殊的类,它用于定义其他类的行为。元类可以控制类的创建过程,从而使得类的行为更加灵活和可定制化。
元类的优点是它可以控制类的创建过程,从而使得类的行为更加灵活和可定制化。元类可以用于实现单例模式、ORM 框架、插件系统等高级功能。此外,元类还可以用于强制执行编程规范,例如在多人协作开发中,可以使用元类来规范类的定义方式。
元类是什么
在面向对象(OOP)编程中,我们可以用不同的类来描述不同的实体及操作,可以通过父类来设计一些“默认”操作,也可以用MixIn类来组合扩展一些额外操作,也可以用抽象类及抽象方法来描述要实现的接口,面向接口编程。
大部分情况下我们并不需要用到元类。
元类是一种type(type的子类),是一种自定义类型,可以定制类的调用、对象创建、初始化、销毁等各种操作。
使用type示例-动态创建类
在 Python 中,可以使用 type() 函数来动态创建类。type() 函数接受三个参数:类名、父类元组和类属性字典。例如:
使用 type() 函数动态创建类
MyClass = type('MyClass', (), {'x': 1, 'y': 2})
obj = MyClass() # 创建一个 MyClass 对象
print(obj.x) # 输出:1
print(obj.y) # 输出:2
元类的使用场景
多数情况下元类用来对普通类来加以限制和规范。使用元类(自定义类型)可以在类的创建(new)、初始化()比如限制类必须包含特定属性和实现特定方法、限制类只能有一个实例对象。
典型使用场景如下:
- 不允许类实例化
- 单例模式:每个类只允许创建一个对象
- 根据属性缓存对象:当某一属性相同时返回同一对象
- 属性限制:类中必须包含某些属性或实现某些方法
- ORM框架:用类及类属性来描述数据库表并转为数据库操作
不允许类实例化
在某些情况下,假设我需要限制一些类不允许创建对象(只允许使用类名操作),可以使用元类加以限制,代码如下。
class NoInstances(type): # 定义元类-继承type
def __call__(self, *args, **kwargs): # 控制类调用(实例化)过程
"""类调用"""
raise TypeError("不允许实例化")
class User(metaclass=NoInstances): # 声明使用元类(该类型)
pass
user = User() # ()即调用类的__call__操作,这里会抛出异常,因此无法直接实例化创建对象
单例模式
在某些情况下仅允许类创建一个实例对象,也可以使用元类进行限制,代码如下:
class Singleton(type): # 单例类型-定制的元类
def __init__(self, *args, **kwargs):
self.__instance = None # 添加一个私有属性,用于保存唯一的实例对象
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs): # 控制类调用
if self.__instance is None:
self.__instance = super().__call__(*args, **kwargs) # 不存在则创建
return self.__instance
else:
return self.__instance # 否则返回已创建的
class User(metaclass=Singleton):
def __init__(self):
print('创建用户')
user1 = User() # 创建对象
user2 = User() # 创建对象
print(user1 is user2) # 返回True,两者是同一对象
根据属性缓存对象
这个是单例模式的扩展,针对特定属性,完全相同的属性组合创建同一对象。
import weakref
class Cached(type):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__cache = weakref.WeakValueDictionary() # 添加一个缓存字典
def __call__(self, *args):
if args in self.__cache: # 通过 参数组合查询缓存字典中有没有对应的对象
return self.__cache[args]
else:
obj = super().__call__(*args) # 创建对象
self.__cache[args] = obj # 根据参数组合(元祖类型)到缓存字典
return obj
class User(metaclass=Cached):
def __init__(self, name):
print('创建用户({!r})'.format(name))
self.name = name
a = User('张三')
b = User('李四')
c = User('张三')
print(a is b) # False 名字不同,不是同一对象
print(a is c) # True 名字相同,是同一对象
限制类必须包含特定属性
在某些情况下我们需要限制特定类必须包含某些属性,假设我们的测试用例类,需要包含优先级、超时时间、归属人、用例状态等属性及run_test方法,可以使用元类实现
class TestCaseType(type):
def __new__(cls, name, bases, attrs):
print('name', name)
print('bases', bases)
print('attrs', attrs)
if {'priority', 'timeout', 'owner', 'status', 'run_test'} - set(attrs.keys()):
raise TypeError('测试用例类必须包含priority、status、owner、timeout属性并实现run_test方法')
return super().__new__(cls, name, bases, attrs)
class TestA(metaclass=TestCaseType):
# priority = 'P1'
timeout = 10
owner = 'kevin'
status = 'ready'
def run_test(self):
pass
a = TestA() # 这里注释了用例类的priority属性,实例化会报错
注册和自动继承父类
当我们需要继承多个父类来组合不同的操作外,除了使用MixIn类外,也可以使用元类配合装饰器实现父类注册和自动继承,代码如下
class AutoBase(type):
registered_bases = []
def __new__(cls, name, bases, attrs):
bases = tuple(list(bases) + cls.registered_bases)
return super().__new__(cls, name, bases, attrs)
@classmethod
def register_base(cls, register_base):
cls.registered_bases.append(register_base)
@AutoBase.register_base
class FatherClassA:
pass
@AutoBase.register_base
class FatherClassB:
pass
class MainClass(metaclass=AutoBase):
pass
print(MainClass.__bases__) # (<class '__main__.FatherClassA'>, <class '__main__.FatherClassB'>)
其他特殊类
除迭代器、上下文管理器、抽象类、元类外,Python中还有一些特殊的类,如枚举类、数据类(Python>=3.7)等。
枚举类
Python 中的枚举类是一种特殊的类,它用于定义一组常量或枚举值。枚举类可以使代码更加清晰和易于理解,因为它可以将一组相关的常量或枚举值组织在一起,并为它们提供有意义的名称。
在 Python 中,可以使用 enum 模块来定义枚举类。enum 模块提供了 Enum 类和 auto 函数,用于定义枚举类和枚举值。例如:
from enum import Enum, auto
class Color(Enum): # 定义一个枚举类
RED = auto()
GREEN = auto()
BLUE = auto()
print(Color.RED.name) # 输出:RED
print(Color.RED.value) # 输出:1 # 遍历枚举类中的所有枚举值
for color in Color:
print(color.name, color.value)
数据类
Python3.7 引入了一种新的语法,称为数据类(data classes),它可以更方便地定义只包含数据的类。数据类是一种特殊的类,它自动为属性生成 __init__()
、__repr__()
、__eq__()
等方法,从而使代码更加简洁和易于理解。
在 Python3.7 中,可以使用 dataclass 装饰器来定义数据类。dataclass 装饰器会自动为类生成 __init__()
、__repr__()
、__eq__()
等方法,从而使代码更加简洁和易于理解。例如:
from dataclasses import dataclass
@dataclass
class Point: # 定义一个数据类
x: float
y: float
p = Point(1.0, 2.0) # 创建一个 Point 对象
print(p.x) # 输出:1.0
print(p.y) # 输出:2.0
练习
魔术方法
- 实现一个key大小写不敏感字典
- 实现字典列表的.取值
对象关系
- 实现项目、需求、任务、缺陷模型
- 实现用例、步骤、测试计划(测试集)、测试报告模型