面向对象(一)
前言
- 这一篇是面向对象的入门三块:四个基本特性(封装 / 继承 / 多态 / 抽象)、类的语法(
__init__、实例方法、类属性、类方法、静态方法)、以及魔术方法(生命周期、字符串表示、属性访问、容器协议、运算符、上下文管理)。 - 所有示例都在 Python 3.13.9 上实跑过,输出原样贴在代码块下面。原笔记里有几处注释和实际行为对不上(比如
__setattr__在__init__里也会触发、只写__eq__会让对象不能当字典键),都按实测改过来了。 - 继承的深入内容(
super()、访问权限、多态细节)在《面向对象(二)》,这里只到「能写出来、知道为什么」为止。
面向对象:四个基本特性
面向对象编程(Object-Oriented Programming, OOP)把「数据」和「操作数据的行为」绑在一起。Python 里一切皆对象,类就是你自己造对象的模板。
-
封装(Encapsulation)
把内部状态藏起来,只暴露该暴露的方法。Python 没有真正的 private,靠命名约定:单下划线
_x表示「内部使用,别碰」,双下划线__x会触发名字改写(name mangling)。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
27class BankAccount:
def __init__(self, holder, balance=0):
self._holder = holder # 保护属性:约定俗成别直接改
self.__balance = balance # 私有属性:会被改名
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
def get_balance(self):
return self.__balance
acc = BankAccount('小明', 100)
acc.deposit(50)
acc.withdraw(30)
print('余额:', acc.get_balance())
print('直接读 __balance:', end=' ')
try:
acc.__balance
except AttributeError:
print('AttributeError')
print('改名后仍能读到:', acc._BankAccount__balance) # 所以「私有」只是改名,不是真隐藏
print('实例字典里的真实键:', list(vars(acc)))输出:
1
2
3
4余额: 120
直接读 __balance: AttributeError
改名后仍能读到: 120
实例字典里的真实键: ['_holder', '_BankAccount__balance'] -
继承(Inheritance)
子类拿到父类的属性和方法,可以重写(override),也可以多层继承。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return 'Some sound'
class Dog(Animal):
def speak(self):
return 'Woof!'
class Cat(Animal):
def speak(self):
return 'Meow!'
class RobotDog(Dog): # 继承 Dog,再重写一次
def speak(self):
return 'Beep boop!'
print(Dog('Buddy').speak(), Cat('Whiskers').speak(), RobotDog('Rex').speak())
print('RobotDog 也是 Animal:', isinstance(RobotDog('Rex'), Animal))
print('没重写的属性照样继承:', RobotDog('Rex').name)输出:
1
2
3Woof! Meow! Beep boop!
RobotDog 也是 Animal: True
没重写的属性照样继承: Rex -
多态(Polymorphism)
同一个调用,不同对象给出不同响应。调用方只关心「有没有这个方法」,不关心具体是哪个类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return 'Some sound'
class Dog(Animal):
def speak(self):
return 'Woof!'
class Cat(Animal):
def speak(self):
return 'Meow!'
class RobotDog(Dog):
def speak(self):
return 'Beep boop!'
def animal_sound(animal):
print(animal.speak()) # 不检查类型,只要有 speak 就行
for a in (Dog('Buddy'), Cat('Whiskers'), RobotDog('Rex')):
animal_sound(a)输出:
1
2
3Woof!
Meow!
Beep boop! -
抽象(Abstraction)
用抽象基类(ABC)规定「子类必须实现哪些方法」,把接口和实现分开。没实现完就实例化会直接报错。
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
36from abc import ABC, abstractmethod
class Shape(ABC):
def area(self):
...
def perimeter(self):
...
class Rectangle(Shape):
def __init__(self, width, height):
self.width, self.height = width, height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
r = Rectangle(3, 4)
print('面积 / 周长:', r.area(), r.perimeter())
try:
Shape() # 抽象类本身不能实例化
except TypeError as e:
print('实例化抽象类:', e)
class Half(Shape): # 只实现了一个抽象方法
def area(self):
return 0
try:
Half()
except TypeError:
print('没实现完抽象方法: TypeError')输出:
1
2
3面积 / 周长: 12 14
实例化抽象类: Can't instantiate abstract class Shape without an implementation for abstract methods 'area', 'perimeter'
没实现完抽象方法: TypeError -
整体示例(四个特性一起用)
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
32from abc import ABC, abstractmethod
# 抽象:规定接口
class Account(ABC):
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance # 封装:状态藏起来
def fee(self, amount):
...
def deposit(self, amount):
self.__balance += amount - self.fee(amount)
def balance(self):
return self.__balance
# 继承 + 多态:两种账户手续费不同
class FreeAccount(Account):
def fee(self, amount):
return 0
class VipAccount(Account):
def fee(self, amount):
return amount * 0.01
accounts = [FreeAccount('小明'), VipAccount('小红')]
for acc in accounts: # 多态:同一个 deposit,不同手续费
acc.deposit(100)
print(type(acc).__name__, acc.owner, acc.balance())
print('都是 Account:', all(isinstance(a, Account) for a in accounts))实跑输出:
1
2
3FreeAccount 小明 100
VipAccount 小红 99.0
都是 Account: True
类的语法
-
定义与实例化
1
2
3
4
5
6
7class MyClass:
"""这是一个简单的类示例""" # 类的文档字符串,help() 会显示
pass
obj = MyClass() # 调用类 = 创建实例
print(type(obj).__name__, isinstance(obj, MyClass))
print('文档字符串:', MyClass.__doc__)输出:
1
2MyClass True
文档字符串: 这是一个简单的类示例 -
__init__与实例方法__init__是初始化方法(不是「构造函数」,真正创建对象的是__new__),第一个参数固定是self,指向当前实例。1
2
3
4
5
6
7
8
9
10
11class Person:
def __init__(self, name, age):
self.name = name # 实例属性:每个实例各一份
self.age = age
def greet(self):
return f"Hello, my name is {self.name} and I'm {self.age} years old."
p = Person('小明', 18)
print(p.greet())
print('实例属性存在实例字典里:', vars(p))输出:
1
2Hello, my name is 小明 and I'm 18 years old.
实例属性存在实例字典里: {'name': '小明', 'age': 18} -
类属性 vs 实例属性
类属性挂在类上、所有实例共享;实例属性挂在实例上。读的时候先找实例再找类,所以给实例赋值同名属性会「遮蔽」类属性,而不是改它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Circle:
pi = 3.14159 # 类属性
def __init__(self, radius):
self.radius = radius
def area(self):
return self.pi * self.radius ** 2
c1, c2 = Circle(1), Circle(2)
print('共享类属性:', c1.pi == c2.pi, '| 面积:', c1.area(), c2.area())
c1.pi = 3 # 这只是给 c1 加了个实例属性
print('遮蔽后 c1 / c2 / 类:', c1.pi, c2.pi, Circle.pi)
print('c1 字典里多了 pi:', 'pi' in vars(c1), '| c2 没有:', 'pi' not in vars(c2))输出:
1
2
3共享类属性: True | 面积: 3.14159 12.56636
遮蔽后 c1 / c2 / 类: 3 3.14159 3.14159
c1 字典里多了 pi: True | c2 没有: True另一个经典坑:类属性是可变对象时,所有实例共享同一个对象。
1
2
3
4
5
6
7
8
9
10
11
12
13class Bad:
tags = [] # 类属性是列表 -> 大家共用一个
class Good:
def __init__(self):
self.tags = [] # 每个实例自己建
b1, b2 = Bad(), Bad()
b1.tags.append('x')
print('Bad 共享:', b2.tags)
g1, g2 = Good(), Good()
g1.tags.append('x')
print('Good 独立:', g2.tags)输出:
1
2Bad 共享: ['x']
Good 独立: [] -
类方法与静态方法
装饰器 第一个参数 能访问什么 典型用途 无(实例方法) self实例 + 类 操作实例状态 @classmethodcls类(含类属性),拿不到实例 替代构造函数、计数器 @staticmethod无 都拿不到(除非显式传) 与类相关但不依赖状态的工具函数 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Counter:
count = 0 # 类属性当计数器
def __init__(self):
Counter.count += 1
def total(cls):
return cls.count # cls 就是类本身,子类调用时 cls 是子类
def add(a, b):
return a + b # 不需要 self / cls
Counter()
Counter()
print('实例方法造了两个:', Counter.total())
print('类方法也能从实例调:', Counter().total())
print('静态方法:', Counter.add(1, 2), Counter().add(3, 4))输出:
1
2
3实例方法造了两个: 2
类方法也能从实例调: 3
静态方法: 3 7 -
整体示例(扑克牌换手)
题目:小明左手 ♥K、右手 ♠A,交换两手的牌后分别是什么?思路是先找对象(人、手、牌),抽象成类,再按题意创建对象、调方法。
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
36class Poker:
def __init__(self, color, number):
self.color = color
self.number = number
def __str__(self):
return '{}{}'.format(self.color, self.number)
class Hand:
def __init__(self, poker=None):
self.poker = poker
def hold_poker(self, poker):
self.poker = poker
class Person:
def __init__(self, name, left_hand, right_hand):
self.name = name
self.left_hand = left_hand
self.right_hand = right_hand
def show(self):
print('{}张开手'.format(self.name))
print('左手:{}'.format(self.left_hand.poker))
print('右手:{}'.format(self.right_hand.poker))
def swap(self):
# 元组解包一行完成交换,不需要临时变量
self.left_hand.poker, self.right_hand.poker = self.right_hand.poker, self.left_hand.poker
print('{}交换两手的牌'.format(self.name))
left_hand, right_hand = Hand(Poker('♥', 'K')), Hand(Poker('♠', 'A'))
xiaoming = Person('小明', left_hand, right_hand)
xiaoming.show()
xiaoming.swap()
xiaoming.show()实跑输出:
1
2
3
4
5
6
7小明张开手
左手:♥K
右手:♠A
小明交换两手的牌
小明张开手
左手:♠A
右手:♥K
高级特性
-
多重继承与 MRO
一个类可以有多个父类。方法查找按 MRO(Method Resolution Order,C3 线性化)走,
类.__mro__能直接看。1
2
3
4
5
6
7
8
9
10
11
12
13
14class Flyable:
def fly(self):
return 'I can fly!'
class Swimmable:
def swim(self):
return 'I can swim!'
class Duck(Flyable, Swimmable):
pass
d = Duck()
print(d.fly(), d.swim())
print('MRO:', [c.__name__ for c in Duck.__mro__])输出:
1
2I can fly! I can swim!
MRO: ['Duck', 'Flyable', 'Swimmable', 'object']两个父类有同名方法时,排在前面的父类优先;
super()也是按 MRO 找下一个,不是「找父类」。 -
类装饰器
装饰器不仅能装饰函数,也能装饰类:拿到类对象,改它,再还回去。
1
2
3
4
5
6
7
8
9
10
11
12def add_method(cls):
def new_method(self):
return 'This is a dynamically added method'
cls.new_method = new_method # 往类上挂一个方法
return cls
# 等价于 MyClass = add_method(MyClass)
class MyClass:
pass
print(MyClass().new_method())
print('方法真的在类上:', 'new_method' in vars(MyClass))输出:
1
2This is a dynamically added method
方法真的在类上: True -
数据类(Python 3.7+)
@dataclass按字段声明自动生成__init__/__repr__/__eq__,省掉一堆样板代码。1
2
3
4
5
6
7
8
9
10
11
12from dataclasses import dataclass
class Point:
x: float
y: float
z: float = 0.0 # 带默认值的字段要放后面
p = Point(1.5, 2.5)
print(p) # 自动 __repr__
print('自动 __eq__:', Point(1, 2) == Point(1, 2))
print('字段:', [f.name for f in __import__('dataclasses').fields(Point)])输出:
1
2
3Point(x=1.5, y=2.5, z=0.0)
自动 __eq__: True
字段: ['x', 'y', 'z'] -
整体示例(高级特性)
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
26from dataclasses import dataclass, field
# 1) 类装饰器:给一批类统一加「描述」能力
def describable(cls):
cls.describe = lambda self: f'{type(self).__name__}({vars(self)})'
return cls
# 2) dataclass + 多重继承
class Serializable:
def to_dict(self):
return dict(vars(self))
class User(Serializable):
name: str
age: int = 0
tags: list = field(default_factory=list) # 可变默认值必须用 default_factory
u = User('小明', 18)
u.tags.append('vip')
print(u)
print('describe:', u.describe())
print('to_dict:', u.to_dict())
print('MRO:', [c.__name__ for c in User.__mro__])
print('两个实例的 tags 独立:', User('a').tags == [])实跑输出:
1
2
3
4
5User(name='小明', age=18, tags=['vip'])
describe: User({'name': '小明', 'age': 18, 'tags': ['vip']})
to_dict: {'name': '小明', 'age': 18, 'tags': ['vip']}
MRO: ['User', 'Serializable', 'object']
两个实例的 tags 独立: True
常见的内置函数(魔术方法)
以双下划线开头结尾的方法叫魔术方法(magic / dunder methods),它们在特定操作时被解释器自动调用,让你的类「长得像内置类型」。
-
对象生命周期
__new__/__init__/__del____new__真正创建实例(静态方法,先于__init__),__init__只做初始化,__del__在对象被回收时调用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class MyClass:
def __new__(cls, *args, **kwargs):
print('创建实例 - __new__')
return super().__new__(cls) # 必须返回一个实例,否则 __init__ 不会跑
def __init__(self, value):
print('初始化实例 - __init__')
self.value = value
def __del__(self):
print('销毁实例 - __del__')
obj = MyClass(10)
del obj # 引用计数归零,立刻触发 __del__
print('del 之后')输出:
1
2
3
4创建实例 - __new__
初始化实例 - __init__
销毁实例 - __del__
del 之后注意
__del__不保证一定执行:循环引用要靠 GC、解释器退出时全局变量可能已被清掉。要确定性地释放资源,用with/close(),别依赖__del__。 -
字符串表示
__str__/__repr__/__format____str__给人看(print/str),__repr__给开发者看(repr、交互式回显、容器里的元素);只写一个时,__repr__可以兜底__str__,反过来不行。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Person:
def __init__(self, name, age):
self.name, self.age = name, age
def __str__(self):
return f'{self.name} ({self.age}岁)'
def __repr__(self):
return f"Person('{self.name}', {self.age})"
def __format__(self, spec): # spec 就是冒号后面那段字符串
return format(self.name, spec) if spec else str(self)
p = Person('张三', 30)
print('str:', str(p))
print('repr:', repr(p))
print('列表里用 repr:', [p])
print('format 带宽度:', f'{p:>6}') # 等价于 format(p, '>6')
print('format 空规格走 __str__:', f'{p}')输出:
1
2
3
4
5str: 张三 (30岁)
repr: Person('张三', 30)
列表里用 repr: [Person('张三', 30)]
format 带宽度: 张三
format 空规格走 __str__: 张三 (30岁)f'{p:>6}'这个写法拆开看:冒号前的p是要格式化的对象,冒号后的>6是格式说明符(format spec)——>表示右对齐(<左对齐、^居中),6表示总宽度,不够就用空格补齐。整段'>6'会原样传进__format__(self, spec),也就是spec == '>6';这里的__format__又把它转手交给format(self.name, '>6'),所以宽度规则其实就是字符串那套。还有一点容易看岔:宽度按字符个数算,不是按屏幕上的显示宽度。
'张三'只有 2 个字符,于是补 4 个空格变成张三,在等宽字体里实际占 6 + 2 = 8 列(中文一个字顶两列)。真要做到视觉对齐,得先按显示宽度算出该补几个空格。 -
属性访问
__getattr__/__setattr__/__delattr__/__getattribute____getattr__:只在属性找不到时调用,适合做兜底 / 代理。__setattr__:每次赋值都调用,包括__init__里的self.x = ...。__getattribute__:每次读属性都调用,优先级高于__getattr__;里面再读self.xxx会无限递归,必须走super().__getattribute__(name)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class AttributeDemo:
def __init__(self):
self.data = {} # 这一行也会触发 __setattr__
def __getattr__(self, name):
print(f'访问不存在的属性: {name}')
return None
def __setattr__(self, name, value):
print(f'设置属性: {name} = {value}')
super().__setattr__(name, value) # 不写这句就永远存不进去
def __delattr__(self, name):
print(f'删除属性: {name}')
super().__delattr__(name)
obj = AttributeDemo()
obj.existing = '值'
print('读已有属性不触发 __getattr__:', obj.existing)
print('读不存在属性:', obj.nonexistent)
del obj.existing输出:
1
2
3
4
5
6设置属性: data = {}
设置属性: existing = 值
读已有属性不触发 __getattr__: 值
访问不存在的属性: nonexistent
读不存在属性: None
删除属性: existing1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Trap:
def __getattribute__(self, name):
return super().__getattribute__(name) # 正确:交给父类
class Bad:
def __getattribute__(self, name):
return self.__dict__[name] # 错误:self.__dict__ 又触发自己
t = Trap()
t.x = 1
print('正确写法:', t.x)
try:
Bad().anything
except RecursionError:
print('错误写法: RecursionError')输出:
1
2正确写法: 1
错误写法: RecursionError -
容器类型模拟
实现
__len__/__getitem__/__setitem__/__delitem__/__contains__/__iter__,你的类就能用len()、下标、in、for。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
28class MyList:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
def __getitem__(self, index):
return self.items[index]
def __setitem__(self, index, value):
self.items[index] = value
def __delitem__(self, index):
del self.items[index]
def __contains__(self, item):
return item in self.items
def __iter__(self):
return iter(self.items) # 交给列表的迭代器
lst = MyList([1, 2, 3])
print('len:', len(lst), '| 下标:', lst[1], '| in:', 3 in lst)
lst[0] = 9
del lst[1]
print('改删之后:', list(lst))
print('for:', [x for x in lst])输出:
1
2
3len: 3 | 下标: 2 | in: True
改删之后: [9, 3]
for: [9, 3]一个有意思的兜底:只实现
__getitem__(从 0 开始的整数下标)也能for,解释器会按 0,1,2… 一直取到IndexError。这是老式序列协议,isinstance(x, Iterable)却仍是False(详见《函数使用(四)》)。1
2
3
4
5
6
7
8
9
10
11class OnlyGet:
def __init__(self, items):
self.items = list(items)
def __getitem__(self, i):
return self.items[i] # 越界抛 IndexError,for 靠它结束
g = OnlyGet([1, 2, 3])
print('没有 __iter__ 也能 for:', [x for x in g])
from collections.abc import Iterable
print('但 isinstance Iterable:', isinstance(g, Iterable))输出:
1
2没有 __iter__ 也能 for: [1, 2, 3]
但 isinstance Iterable: False -
数值运算
__add__/__sub__/__mul__/__truediv__/__floordiv__/__mod__/__pow__/__and__/__or__/__xor__对应+ - * / // % ** & | ^。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __str__(self):
return f'Vector({self.x}, {self.y})'
v1, v2 = Vector(2, 3), Vector(1, 4)
print('v1 + v2:', v1 + v2)
print('v1 - v2:', v1 - v2)
print('v1 * 2:', v1 * 2)
try:
print(2 * v1) # 左操作数是 int,不会调 Vector.__mul__
except TypeError as e:
print('2 * v1:', e)输出:
1
2
3
4v1 + v2: Vector(3, 7)
v1 - v2: Vector(1, -1)
v1 * 2: Vector(4, 6)
2 * v1: unsupported operand type(s) for *: 'int' and 'Vector'想让
2 * v1也成立,补一个反射方法__rmul__(reflect 在这里是「反过来」的意思——左边的类型没有对应方法时,Python 会反过来问右边的类型;和「运行时按名字自省」那个 reflection 不是一回事),它是右操作数版本:1
2
3
4
5
6
7
8
9
10
11
12
13
14class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __str__(self):
return f'Vector({self.x}, {self.y})'
class Vector2(Vector):
__rmul__ = Vector.__mul__ # 复用同一份实现
print('2 * v:', 2 * Vector2(2, 3))输出:
1
2 * v: Vector(4, 6)
-
比较操作
__eq__/__ne__/__lt__/__le__/__gt__/__ge__对应== != < <= > >=。两个容易忽略的行为:a > b在a没有__gt__时会反射去调b.__lt__(a),所以只写__lt__也能让>工作。- 只定义
__eq__会让类变得不可哈希(__hash__被置成None),对象就不能当字典键 / 进集合;要么一起定义__hash__,要么用@dataclass(frozen=True)/functools.total_ordering这类工具。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Book:
def __init__(self, title, pages):
self.title, self.pages = title, pages
def __eq__(self, other):
return self.pages == other.pages
def __lt__(self, other):
return self.pages < other.pages
def __le__(self, other):
return self.pages <= other.pages
b1, b2 = Book('Python入门', 300), Book('高级Python', 500)
print('b1 < b2:', b1 < b2, '| b1 == b2:', b1 == b2)
print('b1 > b2 走反射:', b1 > b2)
print('b1 != b2 默认取反 __eq__:', b1 != b2)
print('定义了 __eq__ 后 __hash__ 是 None:', Book.__hash__ is None)
try:
{b1: 1}
except TypeError:
print('当字典键: TypeError')输出:
1
2
3
4
5b1 < b2: True | b1 == b2: False
b1 > b2 走反射: False
b1 != b2 默认取反 __eq__: True
定义了 __eq__ 后 __hash__ 是 None: True
当字典键: TypeError懒得写全六个比较方法时,用
@total_ordering:给__eq__加任意一个顺序方法,它补齐其余的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18from functools import total_ordering
class Book:
def __init__(self, title, pages):
self.title, self.pages = title, pages
def __eq__(self, other):
return self.pages == other.pages
def __lt__(self, other):
return self.pages < other.pages
class Book2(Book):
pass
c1, c2 = Book2('a', 1), Book2('b', 2)
print('补齐后 > >= :', c1 > c2, c1 >= c2, c2 >= c1)输出:
1
补齐后 > >= : False False True
-
其他重要方法
__call__让实例能像函数一样被调用。__enter__/__exit__让实例支持with。__hash__/__bool__/__dir__分别接管hash()、真值判断、dir()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class CallableDemo:
def __call__(self, *args):
return f'被调用,参数: {args}'
print(CallableDemo()(1, 2, 3))
print('callable() 认它:', callable(CallableDemo()))
class ContextManagerDemo:
def __enter__(self):
print('进入上下文')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('退出上下文')
return False # 返回 True 会吞掉异常,一般别这么干
with ContextManagerDemo() as cm:
print('在上下文中')输出:
1
2
3
4
5被调用,参数: (1, 2, 3)
callable() 认它: True
进入上下文
在上下文中
退出上下文1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Demo:
def __init__(self, n):
self.n = n
def __bool__(self):
return self.n > 0 # 决定 if obj / bool(obj)
def __hash__(self):
return hash(self.n)
def __dir__(self):
return ['n'] # dir() 会排序后返回
d = Demo(3)
print('bool:', bool(d), bool(Demo(0)))
print('能当字典键:', {d: 'ok'}[d])
print('dir:', dir(d))输出:
1
2
3bool: True False
能当字典键: ok
dir: ['n'] -
整体示例(魔术方法)
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
43
44class Playlist:
"""一个既能当容器、又能当上下文、还能直接调用的播放列表"""
def __init__(self, name, songs=()):
self.name = name
self.songs = list(songs)
def __len__(self):
return len(self.songs)
def __getitem__(self, i):
return self.songs[i]
def __iter__(self):
return iter(self.songs)
def __contains__(self, song):
return song in self.songs
def __str__(self):
return f'{self.name}({len(self)}首)'
def __repr__(self):
return f'Playlist({self.name!r}, {self.songs!r})'
def __call__(self, song):
self.songs.append(song) # 像函数一样「加歌」
return self
def __enter__(self):
print('开始播放', self.name)
return self
def __exit__(self, *exc):
print('停止播放', self.name)
return False
pl = Playlist('通勤', ['晴天', '稻香'])
print('str / repr:', str(pl), '|', repr(pl))
print('len / 下标 / in:', len(pl), pl[0], '稻香' in pl)
pl('夜曲') # __call__
with pl as p: # __enter__ / __exit__
print('循环:', [s for s in p])
print('结束后长度:', len(pl))实跑输出:
1
2
3
4
5
6str / repr: 通勤(2首) | Playlist('通勤', ['晴天', '稻香'])
len / 下标 / in: 2 晴天 True
开始播放 通勤
循环: ['晴天', '稻香', '夜曲']
停止播放 通勤
结束后长度: 3
设计原则(SOLID)
写类时常用的五条经验,记名字不如记意思:
- 单一职责(SRP):一个类只负责一件事,改一个需求不该牵动别的类。
- 开放封闭(OCP):对扩展开放、对修改封闭——加功能靠新增子类 / 组合,而不是改老代码。
- 里氏替换(LSP):子类要能无缝替换父类,别在重写里收紧前置条件或抛出父类没有的异常。
- 接口隔离(ISP):别逼调用方依赖它用不到的方法,接口拆小。
- 依赖倒置(DIP):依赖抽象(接口 / ABC)而不是具体实现,上面的「抽象」一节就是它的落地方式。
参考
- 数据模型(魔术方法的权威清单):Python 数据模型
- 类与实例、属性查找:类
- 抽象基类:abc — 抽象基类
- 数据类:dataclasses — 数据类
total_ordering:functools — 高阶函数