类
前面学了函数和装饰器,这一篇来认识 Python 中面向对象编程的核心概念——类(class)。类是创建对象的模板,对象是类的实例。通过类,我们可以把数据和操作数据的方法封装在一起,让代码更结构化、更易复用。
1. 什么是面向对象编程
面向对象编程(Object-Oriented Programming,简称 OOP)是一种非常流行的编程范式。它把现实世界的事物抽象成对象,每个对象都有自己的属性(数据)和行为(方法)。
在 Python 中,面向对象编程有三大核心特性:
- 封装:把数据和操作数据的方法打包在一起,隐藏内部细节,只暴露必要的接口。
- 继承:从已有的类派生出新的类,复用已有代码。
- 多态:同样的消息作用于不同类型的对象,可以产生不同的行为。
类(class)是抽象的模板,对象(object)是类的具体实例。比如「学生」是一个类,而「张三」是这个类的一个对象。
2. 定义类与创建对象
在 Python 中,用 class 关键字定义类。类中定义的函数称为方法,方法的第一个参数通常叫 self,代表调用该方法的对象本身。
class Student:
"""学生类"""
def study(self, course_name):
print(f'{course_name} 学习中')
def play(self):
print('休息一会儿')
# 创建对象
stu1 = Student()
stu2 = Student()
print(stu1) # <__main__.Student object at 0x...>
print(stu2) # <__main__.Student object at 0x...>给对象发消息(调用方法)有两种方式:
# 1. 对象.方法() 最常用
stu1.study('Python')
# 2. 类.方法(对象) 等价写法
Student.study(stu1, 'Python')推荐用
对象.方法()的方式,更符合面向对象的习惯。
3. 初始化方法 init
创建对象时,如果需要给对象设置初始属性,可以使用 __init__ 方法。它在构造对象时自动调用,因此被称为初始化方法。
class Student:
"""学生类"""
def __init__(self, name, age):
self.name = name
self.age = age
def study(self, course_name):
print(f'{self.name} 正在学习 {course_name}')
def play(self):
print(f'{self.name} 正在玩游戏')
stu1 = Student('骆昊', 44)
stu2 = Student('王大锤', 25)
stu1.study('Python 程序设计') # 骆昊 正在学习 Python 程序设计
stu2.play() # 王大锤 正在玩游戏self.name和self.age是对象的属性。self代表当前对象,调用方法时不需要手动传入。
4. 封装与访问可见性
封装的核心思想是:隐藏内部细节,只暴露必要的接口。类中的方法其实就是一种封装。
Python 中没有像 Java 那样严格的 private / protected 关键字,但可以通过属性名前加下划线来表示访问可见性:
__name:双下划线前缀,表示私有属性( name mangling,外部不建议直接访问)。_name:单下划线前缀,表示受保护属性(约定,仍然可以访问)。
class Student:
def __init__(self, name, age):
self.__name = name
self.__age = age
def study(self, course_name):
print(f'{self.__name} 正在学习 {course_name}')
stu = Student('王大锤', 20)
stu.study('Python 程序设计')
# 下面这行会报错
# print(stu.__name) # AttributeError在 Python 中,通常信奉「大家都是成年人」的约定,不会强制把属性私有。多数情况下直接用普通属性即可,需要控制访问时再用
__或@property。
5. @property 装饰器
@property 可以把一个方法变成属性访问,这样获取值时不需要加括号。
class User:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
user = User('juzicoding')
# 像访问属性一样获取值
print(user.name) # juzicoding
# 像设置属性一样设置值
user.name = 'tom'
print(user.name) # tom@property 常用于对属性进行封装,在取值和赋值时可以做额外校验或处理。
6. 类方法和静态方法
类中除了对象方法(第一个参数是 self),还有类方法和静态方法。
- 对象方法:发给对象的消息,第一个参数是
self。 - 类方法:发给类的消息,用
@classmethod装饰,第一个参数是cls。 - 静态方法:既不需要
self也不需要cls,用@staticmethod装饰。
class MyClass:
@staticmethod
def static_method():
print('静态方法')
@classmethod
def class_method(cls):
print(f'类名: {cls.__name__}')
MyClass.static_method() # 静态方法
MyClass.class_method() # 类名: MyClass什么时候用类方法或静态方法?比如判断三条边能否构成三角形,这个判断在三角形对象创建之前就可以做,更适合设计成静态方法或类方法。
class Triangle:
"""三角形"""
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
@staticmethod
def is_valid(a, b, c):
"""判断三条边能否构成三角形"""
return a + b > c and b + c > a and a + c > b
def perimeter(self):
return self.a + self.b + self.c
def area(self):
p = self.perimeter() / 2
return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5
if Triangle.is_valid(3, 4, 5):
t = Triangle(3, 4, 5)
print(t.perimeter()) # 12
print(t.area()) # 6.0
else:
print('无效的边长')7. 继承
继承是从已有的类(父类)创建新类(子类)的机制。子类会自动拥有父类的属性和方法,同时还可以扩展自己的属性和方法。
class Person:
"""人"""
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
print(f'{self.name} 正在吃饭')
def sleep(self):
print(f'{self.name} 正在睡觉')
class Student(Person):
"""学生"""
def study(self, course_name):
print(f'{self.name} 正在学习 {course_name}')
class Teacher(Person):
"""老师"""
def __init__(self, name, age, title):
super().__init__(name, age)
self.title = title
def teach(self, course_name):
print(f'{self.title}{self.name} 正在讲授 {course_name}')
stu = Student('白元芳', 21)
stu.eat() # 白元芳 正在吃饭
stu.study('Python 程序设计') # 白元芳 正在学习 Python 程序设计
tea = Teacher('武则天', 35, '副教授')
tea.teach('Python 程序设计') # 副教授武则天 正在讲授 Python 程序设计Student(Person)表示Student继承自Person。super().__init__(name, age)调用父类的初始化方法。- 所有类默认继承自
object,它是 Python 中的顶级类。
8. 多态与抽象类
多态的意思是:调用相同的方法,不同的对象做出不同的行为。子类继承父类后,可以对方法进行重写。
class Animal:
def speak(self):
print('动物发出声音')
class Dog(Animal):
def speak(self):
print('汪汪')
class Cat(Animal):
def speak(self):
print('喵喵')
for animal in [Dog(), Cat(), Animal()]:
animal.speak()
# 输出:
# 汪汪
# 喵喵
# 动物发出声音当某个父类的方法暂时无法具体实现,只希望子类去重写时,可以把它声明为抽象方法。Python 通过 abc 模块实现抽象类。
from abc import ABCMeta, abstractmethod
class Employee(metaclass=ABCMeta):
"""员工"""
def __init__(self, name):
self.name = name
@abstractmethod
def get_salary(self):
"""结算月薪"""
pass
class Manager(Employee):
def get_salary(self):
return 15000.0
class Programmer(Employee):
def __init__(self, name, working_hour=0):
super().__init__(name)
self.working_hour = working_hour
def get_salary(self):
return 200 * self.working_hour
# 下面的代码会报错,因为 Employee 是抽象类
# emp = Employee('张三')
mgr = Manager('刘备')
print(mgr.get_salary()) # 15000.0抽象类不能被直接实例化,它的作用是定义子类必须实现的接口。
9. 魔术方法
Python 中有很多以双下划线开头和结尾的方法,称为魔术方法(magic method)。它们可以让对象支持更自然的操作。
常用的魔术方法:
| 方法 | 作用 |
|---|---|
__init__ | 初始化对象 |
__str__ | 返回对象的字符串描述,print() 调用 |
__repr__ | 返回对象的可打印表示 |
__eq__ | 判断两个对象是否相等 == |
__lt__ | 小于比较 < |
__gt__ | 大于比较 > |
__len__ | 返回对象长度,len() 调用 |
class Point:
"""平面上的点"""
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __str__(self):
return f'({self.x}, {self.y})'
def __lt__(self, other):
"""比较两个点到原点的距离"""
return self.x ** 2 + self.y ** 2 < other.x ** 2 + other.y ** 2
def distance_to(self, other):
"""计算与另一个点的距离"""
dx = self.x - other.x
dy = self.y - other.y
return (dx * dx + dy * dy) ** 0.5
p1 = Point(3, 5)
p2 = Point(6, 9)
print(p1) # (3, 5)
print(p1.distance_to(p2)) # 5.0
print(p1 < p2) # True10. 动态属性与 slots
Python 是动态语言,可以给对象动态添加属性。
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
stu = Student('王大锤', 20)
stu.sex = '男' # 动态添加属性
print(stu.sex) # 男如果不希望对象动态添加属性,可以使用 __slots__ 限制可拥有的属性。
class Student:
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
stu = Student('王大锤', 20)
# 下面这行会报错
# stu.sex = '男' # AttributeError11. 类装饰器
装饰器不仅可以装饰函数,还可以装饰类。类装饰器通常用于给类添加额外功能。
11.1 函数形式的类装饰器
def log_class(cls):
class Wrapper:
def __init__(self, *args, **kwargs):
self.wrapped = cls(*args, **kwargs)
def __getattr__(self, name):
return getattr(self.wrapped, name)
def display(self):
print('调用前')
self.wrapped.display()
print('调用后')
return Wrapper
@log_class
class MyClass:
def display(self):
print('原方法')
obj = MyClass()
# 输出: 调用前
# 输出: 原方法
# 输出: 调用后
obj.display()11.2 类形式的类装饰器
用类也可以实现装饰器。下面是一个单例模式的示例:
class SingletonDecorator:
def __init__(self, cls):
self.cls = cls
self.instance = None
def __call__(self, *args, **kwargs):
if self.instance is None:
self.instance = self.cls(*args, **kwargs)
return self.instance
@SingletonDecorator
class Database:
def __init__(self):
print('初始化')
db1 = Database()
db2 = Database()
print(db1 is db2) # True单例模式保证一个类只有一个实例。第一次调用 Database() 会创建实例,之后再调用都返回同一个实例。
12. 内置装饰器
Python 提供了几个常用的内置装饰器,专门用于类中。
| 装饰器 | 作用 |
|---|---|
@staticmethod | 定义静态方法 |
@classmethod | 定义类方法 |
@property | 把方法变成属性访问 |
这三个装饰器在前面已经分别介绍过。@staticmethod 和 @classmethod 在第 6 节,@property 在第 5 节。
13. 综合案例
13.1 扑克游戏
简单起见,这里的扑克只有 52 张牌(没有大小王),需要将牌发到 4 个玩家手上,每个玩家 13 张牌,按花色和点数从小到大排列。
首先定义花色枚举。枚举用符号常量代替数字,代码可读性更好。
from enum import Enum
class Suite(Enum):
"""花色"""
SPADE, HEART, CLUB, DIAMOND = range(4)
for suite in Suite:
print(f'{suite}: {suite.value}')
# 输出:
# Suite.SPADE: 0
# Suite.HEART: 1
# Suite.CLUB: 2
# Suite.DIAMOND: 3接下来定义牌类。
class Card:
"""牌"""
def __init__(self, suite, face):
self.suite = suite
self.face = face
def __repr__(self):
suites = '♠♥♣♦'
faces = ['', 'A', '2', '3', '4', '5', '6',
'7', '8', '9', '10', 'J', 'Q', 'K']
return f'{suites[self.suite.value]}{faces[self.face]}'
def __lt__(self, other):
"""比较两张牌的大小,用于排序"""
if self.suite == other.suite:
return self.face < other.face
return self.suite.value < other.suite.value
card1 = Card(Suite.SPADE, 5)
card2 = Card(Suite.HEART, 13)
print(card1) # ♠5
print(card2) # ♥K然后定义扑克类,负责洗牌和发牌。
import random
class Poker:
"""扑克"""
def __init__(self):
# 52 张牌
self.cards = [Card(suite, face)
for suite in Suite
for face in range(1, 14)]
# 记录当前发牌位置
self.current = 0
def shuffle(self):
"""洗牌"""
self.current = 0
random.shuffle(self.cards)
def deal(self):
"""发牌"""
card = self.cards[self.current]
self.current += 1
return card
@property
def has_next(self):
"""是否还有牌"""
return self.current < len(self.cards)再定义玩家类。
class Player:
"""玩家"""
def __init__(self, name):
self.name = name
self.cards = []
def get_one(self, card):
"""摸牌"""
self.cards.append(card)
def arrange(self):
"""整理手牌"""
self.cards.sort()最后创建 4 个玩家,轮流发牌并输出结果。
poker = Poker()
poker.shuffle()
players = [Player('东邪'), Player('西毒'), Player('南帝'), Player('北丐')]
# 每人发 13 张牌
for _ in range(13):
for player in players:
player.get_one(poker.deal())
# 整理并输出
for player in players:
player.arrange()
print(f'{player.name}: ', end='')
print(player.cards)
# 输出示例:
# 东邪: [♠3, ♠7, ♠J, ♥2, ♥8, ♥Q, ♣4, ♣9, ♣K, ♦5, ♦10, ♦A, ♦J]
# ...这个案例把枚举、列表推导式、魔术方法 __repr__ / __lt__、@property、类之间的关系(扑克 has-a 牌、玩家 has-a 牌)都串了起来。
13.2 数字时钟
import time
class Clock:
"""数字时钟"""
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.min = minute
self.sec = second
def run(self):
"""走字"""
self.sec += 1
if self.sec == 60:
self.sec = 0
self.min += 1
if self.min == 60:
self.min = 0
self.hour += 1
if self.hour == 24:
self.hour = 0
def show(self):
"""显示时间"""
return f'{self.hour:0>2d}:{self.min:0>2d}:{self.sec:0>2d}'
clock = Clock(23, 59, 58)
while True:
print(clock.show())
time.sleep(1)
clock.run()13.3 工资结算系统
某公司有三种员工:部门经理月薪固定 15000 元;程序员按工作时间(每小时 200 元)计算;销售员底薪 1800 元加销售额 5% 提成。
from abc import ABCMeta, abstractmethod
class Employee(metaclass=ABCMeta):
"""员工"""
def __init__(self, name):
self.name = name
@abstractmethod
def get_salary(self):
"""结算月薪"""
pass
class Manager(Employee):
"""部门经理"""
def get_salary(self):
return 15000.0
class Programmer(Employee):
"""程序员"""
def __init__(self, name, working_hour=0):
super().__init__(name)
self.working_hour = working_hour
def get_salary(self):
return 200 * self.working_hour
class Salesman(Employee):
"""销售员"""
def __init__(self, name, sales=0):
super().__init__(name)
self.sales = sales
def get_salary(self):
return 1800 + self.sales * 0.05
emps = [
Manager('刘备'),
Programmer('诸葛亮'),
Manager('曹操'),
Programmer('荀彧'),
Salesman('张辽')
]
for emp in emps:
if isinstance(emp, Programmer):
emp.working_hour = 160
elif isinstance(emp, Salesman):
emp.sales = 50000
print(f'{emp.name} 本月工资为: ¥{emp.get_salary():.2f}')
# 输出:
# 刘备 本月工资为: ¥15000.00
# 诸葛亮 本月工资为: ¥32000.00
# 曹操 本月工资为: ¥15000.00
# 荀彧 本月工资为: ¥32000.00
# 张辽 本月工资为: ¥4300.00
isinstance(emp, Programmer)可以判断对象是否属于某个类或其子类,比type()更灵活。
总结
类是创建对象的模板,__init__ 负责初始化对象属性,self 代表对象本身。通过封装可以隐藏内部细节,通过继承可以复用代码,通过多态可以让不同对象对同一消息做出不同反应。再加上 @staticmethod、@classmethod、@property、类装饰器和几个常用魔术方法,基本就能应付日常大部分面向对象编程场景了。想真正掌握 OOP,还是要多写代码、多读代码。