python-面向对象高级特性

动态绑定属性和方法

python是一种动态语言,而动态语言的类的属性和方法可以动态绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 比如有这么一个类
class Student(object):
pass

# 动态绑定属性
s = Student()
s.name = 'Peter'
print(s.name)

# 动态绑定方法
def set_age(self, age): # 先定义一个函数
self.age = age

from types import MethodType
s.set_age = MethodType(set_age, s) # 给实例绑定方法
s.set_age(25)
s.age
>>> 25

这种动态绑定属性和方法,可以在程序运行过程中对实例添加属性和方法,但是只是对该实例生效,同一个类创建的另一个实例是不生效的.

__solt__

如果不想让别人随意动态绑定属性,可以使用__solt__指定可以绑定的属性名,实例只可以绑定指定的属性.

1
2
3
4
5
6
7
8
9
10
class Student(object):
__slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称

>>> s = Student() # 创建新的实例
>>> s.name = 'Michael' # 绑定属性'name'
>>> s.age = 25 # 绑定属性'age'
>>> s.score = 99 # 绑定属性'score'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score' # slot没有允许score的属性绑定,因而报错AttributeError

@property

属性装饰器,用于将类的方法转换为属性,从而实现属性的访问和设置时调用对应的方法.

主要适用场景有:

  • 实现属性的访问控制: 将方法装饰为@property控制属性的访问权限(只读,只写,读写).
  • 简化属性的访问: 将方法转为属性,就可以直接适用属性的访问方式(点号语法)访问方法
  • 属性计算/验证: 在属性访问时执行特定的计算或逻辑,或实现验证

getter和setter: 类的外部公共接口,用来访问和修改属性

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

@property # diameter就装饰成@property,该方法就会成为对应属性的getter方法
def diameter(self):
return self.radius * 2

@diameter.setter # 被属性装饰器装饰后,会自动生成对应的setter装饰器
def diameter(self, value):
if not isinstance(value, int):
raise ValueError('diameter must be an integer!')
if value < 0 or value > 100:
raise ValueError('diameter must between 0 ~ 100')
self._radius = value # _开头的变量名是一个约定俗成的做法,表示这个变量是一个私有变量,应谨慎处理,但开发人员依旧可以直接访问和修改该变量
# 使用_开头是为了提醒开发者,这是一个私有变量,应该通过类的公共接口(getter,setter)来访问和修改

a = Circle(5)
print(a.diameter) # 结果是10 (getter)
a.diameter = -1 # 报错 (setter)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Student:
#birth是一个可读写属性
@property
def birth(self):
return self._birth # 注意实例变量名不能与方法名一样,因为在调用s.birth时,首先会转回方法调用,然后在执行return时候,又视为访问self的属性,再次转为方法调用,造成无限递归,最终导致栈溢出RecursionError

@birth.setter
def birth(self, value):
self._birth = value

# age只定义getter不定义setter就是只读
@property
def age(self):
retuen 2015 - self._birth

多重继承

一个子类可以通过多重继承同时获得多个父类的所有功能.

假设要实现4种动物:

  • Dog - 狗
  • Bat - 蝙蝠
  • Parrot - 鹦鹉
  • Ostrich - 鸵鸟

我们可以按照哺乳动物和鸟类来归类,可以设计出这样的类的层次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
                ┌───────────────┐
│ Animal │
└───────────────┘

┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Mammal │ │ Bird │
└─────────────┘ └─────────────┘
│ │
┌─────┴──────┐ ┌─────┴──────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Dog │ │ Bat │ │ Parrot │ │ Ostrich │
└─────────┘ └─────────┘ └─────────┘ └─────────┘

如果按能跑能飞来划分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
                ┌───────────────┐
│ Animal │
└───────────────┘

┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Runnable │ │ Flyable │
└─────────────┘ └─────────────┘
│ │
┌─────┴──────┐ ┌─────┴──────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Dog │ │ Ostrich │ │ Parrot │ │ Bat │
└─────────┘ └─────────┘ └─────────┘ └─────────┘

如果合并两种划分方式,就要设计更多层次:

  • 哺乳类: 能跑的哺乳类, 能飞的哺乳类
  • 鸟类: 能跑的鸟类, 能飞的鸟类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
                ┌───────────────┐
│ Animal │
└───────────────┘

┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Mammal │ │ Bird │
└─────────────┘ └─────────────┘
│ │
┌─────┴──────┐ ┌─────┴──────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ MRun │ │ MFly │ │ BRun │ │ BFly │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Dog │ │ Bat │ │ Ostrich │ │ Parrot │
└─────────┘ └─────────┘ └─────────┘ └─────────┘

如果还要增加”宠物类”和”非宠物类”呢?层次将会越搞越多,类的数量也会呈指数级增长.此时就应该用到多重继承.

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
# 最开始的设计,只分哺乳类和鸟类
class Animal(object):
pass

# 大类:
class Mammal(Animal):
pass

class Bird(Animal):
pass

# 各种动物:
class Dog(Mammal):
pass

class Bat(Mammal):
pass

class Parrot(Bird):
pass

class Ostrich(Bird):
pass

# 加入可跑和可飞
class Runnable(object):
def run(self):
print('Running...')

class Flyable(object):
def fly(self):
print('Flying...')

# 多重继承
class Dog(Mammal, Runnable):
pass

class Bat(Mammal, Flyable):
pass

MixIn

一般在设计类的继承关系时,通常都是单一继承,但是如果需要混入额外功能时候,可以通过多重继承实现.这种设计称为MixIn.

MixIn的目的就是给一个类增加多个功能.在设计类的时候,我们优先考虑通过多重继承组合多个MixIn功能,而不是设计多层次的继承关系

为了更好地看出继承关系,一般我们会把需要加上的类名改为xxxMixIn,MixIn类的定义通常不定义__init__方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SpeakMixin:
def speak(self):
print(f"I can speak.")

class EatMixin:
def eat(self):
print(f"I can eat.")

class Animal:
pass

class Dog(Animal, SpeakMixin, EatMixin):
pass

dog = Dog()
dog.speak() # 输出:I can speak.
dog.eat() # 输出:I can eat.

python自带的很多库也使用了MixIn,比如TCPServerUDPServer这两类网络服务,如果要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixInThreadingMixIn提供.

1
2
class MyTCPServer(TCPServer, ForkingMixIn):
pass

方法解析顺序(MRO)

python中用于确定类方法调用顺序的算法.在多重继承下,当一个类实例调用一个方法时,MRO会决定应该使用哪个父类的实现.确保方法调用的确定性和一致性,避免命名冲突和歧异.

基本规则:

  • 深度优先搜索: MRO从当前类开始,一次搜索父类和祖先类,直到找到要调用的方法
  • 从左到右: 在每个类中,MRO会从左到右搜索其基类列表
  • 线性化: MRO会将所有继承关系转换为一个线性顺序,从而避免循环引用和无限递归

具体步骤:

  1. 获取当前类的MRO属性: 每个类都有一个__mro__属性,其中包含了该类的MRO列表
  2. 遍历MRO列表: 对于MRO列表中的每个类,检查,检查该类是否定义了要调用的方法
  3. 找到第一个定义该方法的类: 如果找到,则使用该类的实现
  4. 没有找到: raise AttributeError异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# coding=utf-8
import inspect

class A:
pass

class B(A):
pass

class C(B):
pass

class D(C, B):
pass

mro = inspect.getmro(D)
print(mro) # 输出:(<class '__main__.D'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
print(D.__mro__) # 也可以执行print MRO 属性

定制类

魔术方法

双下划线开头和结尾的特殊方法,用于扩展类的功能和行为.这些方法允许你控制类的实例化,属性访问,运算符重载,迭代等行为,从而实现更强大的类的设计.

常见的魔术方法及用途:

魔术方法用途
__init__初始化方法,用于创建类的实例时设置属性
__str__字符串表示方法,用于将类实例转换成字符串
__repr__表示方法,用于提供更详细的类实例表示
__iter__迭代器方法,用于使类实例可用于for循环
__next__迭代器方法,用于返回迭代的写一个元素
__getitem__索引方法,用于获取类实例的元素
__setitem__索引方法,用于设置类实例的元素
__add__加法运算符重载方法,用于定义自定义加法运算
__sub__减法运算符重载方法,用于定义自定义减法运算
__mul__乘法运算符重载方法,用于定义自定义乘法运算
__eq__等价运算符重载方法,用于定义自定义相等性检查
__call__调用方法,用于使类实例可想函数一样调用

使用这些魔术方法来自定义类的行为,就称为定制类,下面是一些例子

  1. __init__(self, ...): 初始化实例。

    1
    2
    3
    4
    class MyClass:
    def __init__(self, attr1, attr2):
    self.attr1 = attr1
    self.attr2 = attr2
  2. __str__(self): 返回一个可读的字符串表示该类的一个实例。

    1
    2
    3
    4
    5
    6
    class MyClass:
    def __init__(self, value):
    self.value = value

    def __str__(self):
    return f"MyClass({self.value})"
  3. __add__(self, other): 实现加法操作。注意,这个方法通常用于实现类与数字之间的相加。

    1
    2
    3
    4
    5
    6
    class MyClass:
    def __init__(self, value):
    self.value = value

    def __add__(self, other):
    return MyClass(self.value + other)
  4. __len__(self): 返回对象的长度(如字符串或列表的长度)。

    1
    2
    3
    4
    5
    6
    class MyClass:
    def __init__(self, value):
    self.value = value

    def __len__(self):
    return len(self.value)
  5. __eq__(self, other): 实现等于操作 ==。通过实现这个方法,你定义了两个类的实例相等的标准。

    1
    2
    3
    4
    5
    6
    class MyClass:
    def __init__(self, value):
    self.value = value

    def __eq__(self, other):
    return self.value == other.value

枚举类

枚举

python中通常使用大写字母+下划线来定义常量,python中的常量没有严格的限制,没有关键字声明,它在代码中还是可变的.当常量数量变多,声明和引用可能会变得麻烦或混乱,此时可以使用枚举类来定义和管理这些常量.

枚举是一种数据类型,用于定义一组固定且有意义的值。想象一下,你想要表示一个星期中的某一天,你可能会用数字 1 到 7 来表示,但这样容易让人混淆。枚举允许你用更具描述性的名字来表示这些值,例如 MONDAY, TUESDAY, WEDNESDAY 等。

python中使用enum模块来实现枚举类.

优点:

  • 代码可读性提升: 使用枚举类,代码更清晰易懂,因为每个值都有一个有意义的名字。
  • 类型安全: 枚举类定义了常量的类型,避免了使用错误的值。
  • 代码维护方便: 修改枚举值只需修改枚举类定义,无需修改使用该枚举类的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 定义枚举类
from enum import Enum
class Weekday(Enum):
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
SATURDAY = 6
SUNDAY = 7

# 访问枚举类(使用.表达式)
print(Weekday.MONDAY) # 输出Weekday.MONDAY
print(Weekday.MONDAY.value) # 输出1

枚举类可以定义方法,比如定义__str__自定义输出

1
2
3
4
5
6
7
8
9
10
11
12
from enum import Enum

class TrafficLight(Enum):
RED = "stop"
YELLOW = "caution"
GREEN = True

def __str__(self):
return self.value

print(TrafficLight.RED)
# 直接输出stop

实际使用例子

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
import requests
from enum import Enum

class HttpStatusCode(Enum):
OK = 200
CREATED = 201
BAD_REQUEST = 400
NOT_FOUND = 404
INTERNAL_SERVER_ERROR = 500

def handle_response(status_code):
if status_code == HttpStatusCode.OK: #可以直接使用HttpStatusCode.OK来判断
print("Request successful!")
elif status_code == HttpStatusCode.CREATED:
print("Resource created successfully!")
elif status_code == HttpStatusCode.BAD_REQUEST:
print("Invalid request parameters.")
elif status_code == HttpStatusCode.NOT_FOUND:
print("Resource not found.")
elif status_code == HttpStatusCode.INTERNAL_SERVER_ERROR:
print("Server encountered an error.")
else:
print(f"Unknown status code: {status_code}")

response = requests.get('http://www.example.com')
response_status_code = response.status_code

handle_response(response_status_code)

元类与type

在python这种动态语言中,函数和类的定义不是编译时发生的,而是在运行时动态创建的.

比如写一个hello.py模块

1
2
3
class Hello(object):
def hello(self, name='world'):
print(f'Hello {name}')

当python解释器载入hello模块,会依次执行该模块的代码,结果就是动态创建一个Hello的class对象

1
2
3
4
5
6
7
8
>>> from hello import Hello
>>> h = Hello()
>>> h.hello()
Hello world
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class 'hello.Hello'>

可以看到Hello是一个class,类型就是type,h是一个实例,也是一个class,类型是Hello.


先来了解几个概念:

  • 类: 面向对象编程中,类是对象的蓝图,包含了对象的属性(数据)和方法(行为).
  • 实例: 类是模板,实例是根据模板创建的具体对象
  • 元类: 元类是类的一种特殊类型,用于创建类,简单来说:类是用来创建对象的,元类用来创建类

type()是一个内置函数,用来获取对象的类型信息,也可以用来动态创建类.

1
2
3
4
5
# type()创建类的语法
type(name, bases, attrs)
# name: 新类名称
# bases: 父类列表
# attrs: 新类的属性和方法字典
  • type()是动态类创建的工具或接口,而元类是这个接口背后的机制.

  • 当我们使用type()创建动态类时,type()会调用python的元类系统.

  • python的默认元类就是type,这意味着当我们使用type()创建类时,默认会使用type元类

这就解释了为什么Hello这个class的类型是type.因为python默认是使用type(),基于type元类来创建动态类的.

metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况到。

定义

1
2
3
4
5
# 自定义元类,来自定义类创建的机制,自定义元类需要继承自`type`类
class MyMeta(type): # 按照习惯元类名总是以Meta或Metaclass结尾
def __new__(cls, name, bases, attrs): # __new__方法是元类的关键方法,负责创建新的类
# 在这里添加自定义逻辑,例如修改类属性,添加新方法等
return super().__new__(cls, name, bases, attrs)
  • __new__() 方法是元类的关键方法,它负责创建新的类。
  • cls: 元类自身
  • name: 新类的名称
  • bases: 新类的父类列表
  • attrs: 新类需要包含的属性和方法字典
1
2
3
4
5
6
7
8
9
return super().__new__(cls, name, bases, attrs)
# super(): 这个函数用于调用父类的函数或方法.这里,super()用于调用默认元类type的__new__()方法
# __new__(): 这是元类type的一个特殊方法,用于创建新的类对象
'''
因此这行代码的的具体作用是:
1. 调用父类type的__new__()方法: 因为我们继承自type类,需要使用父类的机制来创建新类
2. 传递参数: 传递元类MyMeta自身(cls), 新类名称(name),新类父列表(bases)和新类属性和方法字典(attrs)到父类的__new__()方法中
3. 返回新类对象: 父类__new__()方法最终返回一个新的类对象
'''

使用

使用metaclass参数来指定自定义元类创建类

1
2
3
class MyClass(metaclass=MyMeta):
def __init__(self, value):
self.value = value

MyClass使用MyMeta元类创建.MyMeta__new__()方法将被调用,并能修改MyClass的属性和行为.

实际使用

元类虽然不是最常见的编程技巧,但它在一些特定场景下能发挥强大的作用,提升代码的可维护性和可扩展性.

比如你在开发一个大型电商平台,需要管理各种商品类型,比如衣服,书籍,电子产品等,每个都有特定的属性和行为,比如衣服需要尺寸,颜色等属性,书记需要作者,出版社等

  • 传统方式:

    分别定义各种类,比如Clothing,Book,Electronics

    每个类都有各自的属性和方法,代码架构会变大且难以维护

  • 使用元类

    定义一个元类ProductMeta,用于创建商品类型的类

    ProductMeta可以接受商品类型的属性和方法作为参数,并根据这些参数动态创建类

    这样,我们可以用一种通用的方式来定义商品类型,避免重复代码,并更容易添加新的商品类型

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
class ProductMeta(type):
def __new__(cls, name, bases, attrs):
attrs['get_product_info'] = lambda self: f"Product name: {self.__class__.__name__}"
return super().__new__(cls, name, bases, attrs)

class Product(metaclass=ProductMeta):
def __init__(self, name, price):
self.name = name
self.price = price

class Clothing(Product):
def __init__(self, name, price, size, color):
super().__inti__(name, price):
self.size = size
self.color = color

class Book(Product):
def __init__(self, name, price, author, publisher):
super().__init__(name, price)
self.author = author
self.publisher = publisher

# 使用
clothing = Clothing("T-shirt", 20, "M", "Red")
book = Book("The Hitchhiker's Guide to the Galaxy", 10, "Douglas Adams", "Pan Books")

print(clothing.get_product_info()) # Output: Product name: Clothing
print(book.get_product_info()) # Output: Product name: Book

这里,直接使用元类创建具体商品类也是可行的,但是中间添加一层Product中间类有以下优点:

  1. 代码组织性: 将Product作为基类,可以将所有商品类型代码组织在一起,形成更清晰的层次结构,便于理解和维护
  2. 代码复用: Product类可以包含所有商品类共有的属性和方法,提高代码复用性
  3. 扩展性: 如果将来需要添加新商品,只需要继承Product类,并实现具体的属性和方法即可
  4. 类型安全: 所有商品类都继承Product保证所有商品类都拥有共同的接口,提高代码的类型安全和可维护性

python-面向对象高级特性
http://example.com/2024/02/24/python-oop-advance/
作者
Peter Pan
发布于
2024年2月24日
许可协议