面向对象(二)
前言
- 承接《面向对象(一)》:那篇讲「怎么写一个类」,这篇讲类之间的关系和几种特殊方法——继承与
super()、访问权限与名字改写、类属性 / 类方法 / 静态方法、多态与鸭子类型、@property、可调用对象的判断。 - 所有示例都在本机 Python 3.14.10 上实跑过,输出原样贴在代码块下面。
- 数据持久化(
pickle/json/shelve/sqlite3/h5py)已拆到单独一篇《数据持久化存储》,这篇只留面向对象的内容。
类的继承
-
基本继承
子类自动拥有父类的方法和属性,在此基础上再加自己的东西。
1
2
3
4
5
6
7
8
9
10
11class Parent:
def parent_method(self):
print('这是父类方法')
class Child(Parent): # 括号里写父类,就是继承
def child_method(self):
print('这是子类方法')
c = Child()
c.parent_method() # 类里没写,照样能调用
c.child_method()输出:
1
2这是父类方法
这是子类方法 -
方法重写(override)
子类里写一个同名方法就把父类的盖掉了;查找时沿 MRO 从子类往父类走,先命中谁用谁。
1
2
3
4
5
6
7
8
9
10class Animal:
def speak(self):
return '动物发出声音'
class Dog(Animal):
def speak(self): # 同名即重写
return '汪汪汪!'
print(Animal().speak(), '|', Dog().speak())
print('Dog 的实例同时也是 Animal:', isinstance(Dog(), Animal))输出:
1
2动物发出声音 | 汪汪汪!
Dog 的实例同时也是 Animal: True -
用
super()调父类super()不是「父类」的硬编码,而是按 MRO 取下一个类;最常见的用法是在子类的__init__里把父类负责的那部分初始化做掉。1
2
3
4
5
6
7
8
9
10
11
12class Animal:
def __init__(self, name):
self.name = name # name 这个属性由父类创建
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # 不调这句,self.name 根本不存在
self.breed = breed
d = Dog('旺财', '金毛')
print(d.name, d.breed)
print('实例字典:', list(vars(d)))输出:
1
2旺财 金毛
实例字典: ['name', 'breed'] -
多继承与 MRO
Python 允许一个类同时继承多个父类。多个父类有同名方法时,按 MRO(方法解析顺序) 决定用谁,MRO 由 C3 线性化算法算出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Father:
def skills(self):
return '编程、修理'
class Mother:
def skills(self):
return '烹饪、艺术'
class Child(Father, Mother): # 括号里的顺序就是优先级
pass
c = Child()
print('不重写时取到:', c.skills())
print('MRO:', [k.__name__ for k in Child.__mro__])
print('想两个都要就显式点名:', Father.skills(c), '+', Mother.skills(c))输出:
1
2
3不重写时取到: 编程、修理
MRO: ['Child', 'Father', 'Mother', 'object']
想两个都要就显式点名: 编程、修理 + 烹饪、艺术把顺序换成
Child(Mother, Father),skills()就会取到烹饪、艺术。菱形继承也是同一套规则:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Base:
def hi(self):
return 'base'
class Left(Base):
pass
class Right(Base):
pass
class Bottom(Left, Right): # Base 被间接继承两次
pass
print([k.__name__ for k in Bottom.__mro__])
print(Bottom().hi())输出:
1
2['Bottom', 'Left', 'Right', 'Base', 'object']
baseC3 保证
Base排在Left、Right之后,不会被提前命中。如果写出无法线性化的继承关系(class C(A, B),而B又是A的父类),定义类时会直接抛TypeError: Cannot create a consistent method resolution。 -
混入类(Mixin)
多继承的一种常见用法:父类只提供「能力」,不打算当成类型来用,习惯上名字带
Mixin。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import json
class JsonMixin:
def to_json(self):
return json.dumps(self.__dict__, ensure_ascii=False)
class Person:
def __init__(self, name):
self.name = name
class JsonPerson(Person, JsonMixin): # 人是「是什么」,Mixin 是「会什么」
pass
p = JsonPerson('张三')
print(p.to_json())
print('不加 ensure_ascii=False 会长这样:', json.dumps(p.__dict__))输出:
1
2{"name": "张三"}
不加 ensure_ascii=False 会长这样: {"name": "\u5f20\u4e09"}原笔记这里写的是
json.dumps(self.__dict__),注释却写输出{"name": "张三"}——实际默认ensure_ascii=True,非 ASCII 会被转义成\uXXXX。 -
抽象基类
用
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
26from abc import ABC, abstractmethod
class Shape(ABC):
def area(self): # 只声明接口,不给实现
...
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self): # 实现了才能实例化
return 3.14 * self.radius ** 2
class Half(Shape): # 忘了实现 area
pass
print('Circle(5):', Circle(5).area())
try:
Shape() # 抽象类本身不能被实例化
except TypeError as e:
print('实例化 Shape:', e)
try:
Half()
except TypeError as e:
print('实例化 Half:', e)输出:
1
2
3Circle(5): 78.5
实例化 Shape: Can't instantiate abstract class Shape without an implementation for abstract method 'area'
实例化 Half: Can't instantiate abstract class Half without an implementation for abstract method 'area' -
检查继承关系
1
2
3
4
5
6
7
8
9
10
11
12class Parent:
pass
class Child(Parent):
pass
c = Child()
print('issubclass(Child, Parent):', issubclass(Child, Parent))
print('issubclass(Parent, Child):', issubclass(Parent, Child))
print('isinstance(c, Parent):', isinstance(c, Parent))
print('一切类的根:', issubclass(Child, object))
print('类本身是谁的实例:', type(Child).__name__)输出:
1
2
3
4
5issubclass(Child, Parent): True
issubclass(Parent, Child): False
isinstance(c, Parent): True
一切类的根: True
类本身是谁的实例: type顺带区分:
isinstance(x, C)是「x 是 C 或 C 的子类的实例」;issubclass(A, B)只比类,拿实例进去会TypeError。 -
继承里两个容易踩的坑
-
私有属性不会被子类「原样」继承。
__x在类定义时按所在类改名,子类里写self.__x会解析成_子类名__x,取不到父类那个。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Parent:
def __init__(self):
self.__secret = 42 # 真实键名是 _Parent__secret
class Child(Parent):
def peek(self):
return self.__secret # 这里找的是 _Child__secret
c = Child()
try:
c.peek() # 子类里这句找的是 _Child__secret
except AttributeError as e:
print('子类里按原名取:', e)
print('显式写父类改名后的键:', c._Parent__secret)
print('实例字典:', list(vars(c)))输出:
1
2
3子类里按原名取: 'Child' object has no attribute '_Child__secret'
显式写父类改名后的键: 42
实例字典: ['_Parent__secret']要拿父类的值只能显式写
self._Parent__secret,或者由父类提供公开方法——后者才是正路。 -
子类不写
__init__时直接用父类的;一旦写了又忘了super().__init__(),父类建的属性就全没了,而且这个错往往要到后面访问属性时才暴露。
-
访问权限
-
说明
Python 没有 Java / C++ 那种
public/private关键字,访问权限靠命名约定表达,原则是「约定优于强制」:除了带双下划线的名字会被改写之外,其他都能从外部访问到。 -
公开成员(public)
默认就是公开,外部随便访问。
1
2
3
4
5
6
7
8
9
10class Demo:
def __init__(self):
self.public_attr = 'I am public'
def public_method(self):
return 'Public method'
obj = Demo()
print(obj.public_attr)
print(obj.public_method())输出:
1
2I am public
Public method -
受保护成员(protected,单下划线)
_x只是提示「这是内部实现,别直接碰」,语言层面不做任何限制;唯一的实际效果是from module import *不会带上它。1
2
3
4
5
6
7
8
9
10class Demo:
def __init__(self):
self._protected = 'I am protected'
def _hint(self):
return 'Protected method'
obj = Demo()
print(obj._protected) # 照样能访问,只是不该这么写
print(obj._hint())输出:
1
2I am protected
Protected method -
私有成员(private,双下划线)
__x会被名字改写(name mangling)成_类名__x,目的是避免和子类的属性撞名,不是安全机制。注意__x__(前后都有双下划线)不算私有,不会被改写。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Demo:
def __init__(self):
self.__secret = 'I am private'
def __peek(self):
return 'Private method'
def show(self): # 类内部用原名,编译时已经改好了
print(self.__secret)
print(self.__peek())
obj = Demo()
obj.show()
try:
obj.__secret
except AttributeError as e:
print('外部按原名取:', e)
print('改写后照样能拿到:', obj._Demo__secret)
print('实例字典里的真实键:', list(vars(obj)))输出:
1
2
3
4
5I am private
Private method
外部按原名取: 'Demo' object has no attribute '__secret'
改写后照样能拿到: I am private
实例字典里的真实键: ['_Demo__secret']两个细节值得记住:
- 一是改写只发生在类定义体内,所以在类外面写
obj.__secret,找的就是字面上的__secret(报错信息里也是它),并不会帮你改成_Demo__secret; - 二是
vars(obj)里只有_Demo__secret,说明属性在创建那一刻就已经改名,实例字典里根本不存在__secret这个键。
- 一是改写只发生在类定义体内,所以在类外面写
-
特殊成员(前后都是双下划线)
__init__、__str__、__len__这类不会被改写,它们是协议方法,由解释器在特定时机自动调用。1
2
3
4
5
6
7
8
9
10
11
12class Demo:
def __init__(self):
self.value = 42
def __str__(self):
return f'Demo with value {self.value}'
obj = Demo()
print(obj) # print 触发 __str__
print(obj.__str__()) # 也能直接调,一般没必要
print(str(obj)) # str() 同样走 __str__
print('没写 __repr__ 时 repr 形如 <__main__.Demo object at 0x地址>')输出:
1
2
3
4Demo with value 42
Demo with value 42
Demo with value 42
没写 __repr__ 时 repr 形如 <__main__.Demo object at 0x地址> -
小结
访问权限 命名约定 实际可访问性 使用建议 公开 无前缀 任何地方 对外接口 受保护 _x任何地方(只是约定) 类内部和子类 私有 __x类内部(外部可用 _类名__x拿到)只给类自己用 特殊 __x__任何地方 实现协议方法,别自造这种名字 真要「藏」数据,Python 的答案是模块边界(不导出该名字)或者把属性做成
@property加校验,而不是双下划线。
类属性
-
说明
写在类里、方法外的属性就是类属性:它属于类对象本身,被所有实例共享;实例里没有副本,读的时候沿 MRO 回落到类上找。
1
2
3
4
5
6
7
8
9
10
11class MyClass:
class_attribute = '我是类属性' # 类属性:挂在类对象上
def __init__(self):
self.instance_attribute = '我是实例属性' # 实例属性:挂在每个实例上
print(MyClass.class_attribute) # 通过类名读
obj = MyClass()
print(obj.class_attribute) # 通过实例读,回落到类上
print('在实例字典里吗:', 'class_attribute' in vars(obj))
print('在类字典里吗:', 'class_attribute' in MyClass.__dict__)输出:
1
2
3
4我是类属性
我是类属性
在实例字典里吗: False
在类字典里吗: True -
读得到不等于改得动
通过实例给同名属性赋值,不会改类属性,而是在实例上新建一个属性,把类属性挡住。
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Test:
class_attr = 10
t1 = Test()
t2 = Test()
t1.class_attr = 20 # 新建实例属性,不是改类属性
print('t1:', t1.class_attr)
print('t2:', t2.class_attr)
print('Test:', Test.class_attr)
Test.class_attr = 30 # 要真改类属性得走类名
print('t2 现在:', t2.class_attr)
print('t1 仍被自己挡着:', t1.class_attr)输出:
1
2
3
4
5t1: 20
t2: 10
Test: 10
t2 现在: 30
t1 仍被自己挡着: 20 -
可变类属性更危险
类属性是
list/dict这种可变对象时,实例上的append会真的改到类属性,所有实例一起变。1
2
3
4
5
6
7
8
9
10
11
12class Team:
members = [] # 可变类属性:一个列表被全体共享
def join(self, name):
self.members.append(name) # 没有赋值语句,改的是类上那个列表
a = Team()
b = Team()
a.join('小明')
print('b 看到的:', b.members)
print('类属性:', Team.members)
print('a 自己有吗:', 'members' in vars(a))输出:
1
2
3b 看到的: ['小明']
类属性: ['小明']
a 自己有吗: False默认值要放可变容器时,宁可写进
__init__(self.members = []),或者用None占位、在方法里再建。 -
常见用途
- 类级常量:
Circle.PI这种,方法里用self.PI读。 - 统计实例数量:
Person.count += 1写在__init__里(必须用类名改,self.count += 1会变成给实例建属性)。 - 提供默认值:
Car.wheels = 4,个别实例可以覆盖。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Person:
nation = '中国' # 类属性:全体共享
count = 0 # 用来计数
def __init__(self, name):
self.name = name
self.nation = 'china' # 同名实例属性,会挡住类属性
Person.count += 1 # 改类属性必须走类名
p = Person('xiaoming')
q = Person('xiaohong')
print('类上的 nation:', Person.nation)
print('实例上的 nation:', p.nation)
print('已创建实例数:', Person.count)
Person.city = '杭州' # 类属性可以动态添加
print('动态加的:', q.city)输出:
1
2
3
4类上的 nation: 中国
实例上的 nation: china
已创建实例数: 2
动态加的: 杭州 - 类级常量:
-
几个特殊的类属性
1
2
3
4
5
6
7
8
9
10
11
12
13class Person:
"""一个用来演示的人"""
nation = '中国'
def __init__(self, name):
self.name = name
print('类名:', Person.__name__)
print('父类元组:', Person.__bases__)
print('文档字符串:', Person.__doc__)
print('__dict__ 的类型:', type(Person.__dict__).__name__)
print('类里定义的名字:', sorted(k for k in Person.__dict__ if not k.startswith('__')))
print('所属模块:', Person.__module__)输出:
1
2
3
4
5
6类名: Person
父类元组: (<class 'object'>,)
文档字符串: 一个用来演示的人
__dict__ 的类型: mappingproxy
类里定义的名字: ['nation']
所属模块: __main__Person.__dict__是mappingproxy(只读视图),往里塞键会TypeError;要动态加属性就写Person.city = ...。__init__是方法、不算「类里定义的数据属性」,但它同样在Person.__dict__里,只是键名以双下划线开头所以被上面那行过滤掉了。
类方法
-
说明
用
@classmethod装饰的方法,第一个参数是类本身(习惯写cls)。它拿到的是「这次调用发生在哪个类上」,所以在子类上调用时cls就是子类——这是它和「把类名写死」的关键区别。 -
语法
1
2
3
4
5
6
7
8
9
10
11class MyClass:
class_attr = '类属性'
def show(cls):
print(f'我是类方法,cls 是 {cls.__name__},能读 {cls.class_attr}')
return cls() # 常见用法:顺手造一个实例
MyClass.show() # 通过类调用
obj = MyClass()
obj.show() # 通过实例调用,cls 仍然是类输出:
1
2我是类方法,cls 是 MyClass,能读 类属性
我是类方法,cls 是 MyClass,能读 类属性 -
最典型的用途:替代构造器
一个类需要好几种「入口」时,用类方法返回实例,比在
__init__里堆if清楚。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21from datetime import date
class Order:
def __init__(self, oid, amount):
self.oid = oid
self.amount = amount
def from_dict(cls, d): # 入口 1:从字典建
return cls(d['oid'], d['amount'])
def today(cls, oid, amount): # 入口 2:附带创建日期
o = cls(oid, amount)
o.created = date.today().isoformat()
return o
o1 = Order.from_dict({'oid': 'A1', 'amount': 30})
o2 = Order.today('A2', 50)
print(o1.oid, o1.amount)
print(o2.oid, o2.amount, o2.created)输出(
created是当天日期):1
2A1 30
A2 50 2026-11-02 -
继承时的差别:
cls会跟着变1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Base:
tag = 'base'
def who(cls):
return f'{cls.__name__} / {cls.tag}'
def who_static():
return f'Base / {Base.tag}' # 静态方法里把类名写死了
class Sub(Base):
tag = 'sub'
print('Base.who():', Base.who())
print('Sub.who():', Sub.who()) # cls 是 Sub,tag 也跟着变
print('Sub.who_static():', Sub.who_static()) # 仍然是 Base,子类换不动它输出:
1
2
3Base.who(): Base / base
Sub.who(): Sub / sub
Sub.who_static(): Base / base -
综合示例:用类方法拼一个平方和
题目:设计一个数字类,有两个属性,能做加减乘除,再要求算两个数的平方和。
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 Number:
def __init__(self, num1, num2):
self.num1 = num1
self.num2 = num2
def add(self):
return self.num1 + self.num2 # 两数之和
def sub(self):
return self.num1 - self.num2 # 两数之差
def mul(self):
return self.num1 * self.num2 # 两数之积;传 (n, n) 就是 n 的平方
def div(self):
if self.num2 == 0:
return 0 # 简单起见除零给 0,生产环境建议抛异常
return self.num1 / self.num2
def pingfanghe(cls, num1, num2): # 平方和:n1^2 + n2^2
n12 = cls(num1, num1).mul() # 第一个数的平方
n22 = cls(num2, num2).mul() # 第二个数的平方
return cls(n12, n22).add() # 两个平方再相加
print(Number.pingfanghe(3, 4))
print(Number.pingfanghe(0.5, 1))输出:
1
225
1.25原笔记这里写的是
n1 = Number(num1, num1),换成cls(...)更合理:子类调用时才会用子类自己。
静态方法
-
说明
用
@staticmethod装饰的方法不会自动收到self或cls,它就是一个放在类命名空间里的普通函数,只是逻辑上属于这个类。1
2
3
4
5
6
7
8
9
10
11
12
13
14class MathUtils:
factor = 10
def add(x, y):
return x + y # 拿不到 self,也拿不到 cls
def scale(x):
return x * MathUtils.factor # 想读类属性只能把类名写死
print(MathUtils.add(2, 3))
calc = MathUtils()
print(calc.scale(4)) # 实例调用和类调用等价输出:
1
25
40 -
静态方法能不能访问类属性
能,但要显式写类名,而且写了就没有多态:子类改了同名类属性,从子类调用静态方法读到的还是父类那份。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Base:
factor = 3
def via_static(x):
return x * Base.factor # 写死 Base
def via_class(cls, x):
return x * cls.factor # 跟着调用方走
class Sub(Base):
factor = 10
s = Sub()
print('静态方法:', s.via_static(2), '| 类方法:', s.via_class(2))
print('Base 上调用:', Base.via_static(2), Base.via_class(2))输出:
1
2静态方法: 6 | 类方法: 20
Base 上调用: 6 6所以「静态方法不能访问类属性」这个说法不准确:不能自动访问(没有
self/cls可依赖),硬写类名是可以的,但也就放弃了继承体系里的多态。 -
三种方法对比
特性 实例方法 类方法 静态方法 装饰器 无 @classmethod@staticmethod第一个参数 self(实例)cls(类)无自动传参 读实例属性 可以 不可以 不可以 读类属性 可以( self.x回落)可以( cls.x,随子类变)只能写死类名,随子类不变 调用方式 一般通过实例 类或实例都行 类或实例都行 典型用途 对象行为 替代构造器、操作类状态 与类相关的工具函数 -
什么时候用哪个
- 方法里要用到
self→ 实例方法。 - 要用到类本身(改类属性、造实例、给子类留扩展点)→ 类方法。
- 两者都不用到,只是这个函数归在这个类名下更合适 → 静态方法;否则就该是模块级函数,不必塞进类里。
- 方法里要用到
多态特性
-
说明
多态(Polymorphism)指同一个调用作用于不同类型的对象时产生不同结果。Python 是多态默认的语言:变量没有类型,对象才有,函数只看「你有没有这个方法」。
-
实现方式一:继承 + 方法重写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Animal:
def speak(self):
return '……'
class Dog(Animal):
def speak(self):
return '汪汪!'
class Cat(Animal):
def speak(self):
return '喵喵!'
def animal_sound(animal):
print(animal.speak()) # 不关心具体类型
animal_sound(Dog())
animal_sound(Cat())输出:
1
2汪汪!
喵喵! -
实现方式二:鸭子类型(推荐)
Python 更推崇「走起来像鸭子、叫起来像鸭子,那就是鸭子」:不要求继承关系,只要有对应方法就能进同一个函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Dog:
def speak(self):
return '汪汪!'
class Cat:
def speak(self):
return '喵喵!'
class Car: # 跟动物毫无继承关系
def speak(self):
return '滴滴!'
def make_sound(obj):
print(obj.speak())
for o in (Dog(), Cat(), Car()):
make_sound(o)输出:
1
2
3汪汪!
喵喵!
滴滴!想要「显式一点」的约束,用
typing.Protocol(3.8+)声明接口,运行时不强制、静态检查会管;或者用上一节的抽象基类,运行时直接拦。 -
实现方式三:运算符重载
同一个
+作用在数字、字符串、自定义对象上表现不同,靠的是特殊方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other): # v1 + v2 走这里
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, k): # v * 2
return Vector(self.x * k, self.y * k)
def __rmul__(self, k): # 2 * v,不写就会 TypeError
return self * k
def __str__(self):
return f'Vector({self.x}, {self.y})'
v1 = Vector(2, 3)
v2 = Vector(4, 5)
print(v1 + v2)
print(v1 * 2)
print(2 * v1)输出:
1
2
3Vector(6, 8)
Vector(4, 6)
Vector(4, 6) -
多态的价值
- 调用方不写类型判断:新增一个类只要能提供同名方法,老代码不用改。
- 可测试:传一个假的对象进去就能测,不需要真连数据库。
- 接口统一:配合
Protocol/ 抽象基类,把「约定」写进代码里。
代价是 Python 没有编译期的「你有没有漏写 override」检查。3.12 起可以加
@typing.override显式标注「我是重写」,漏写或父类签名变了会被类型检查器报错:1
2
3
4
5
6
7
8
9
10
11
12from typing import override
class Animal:
def speak(self):
return '……'
class Dog(Animal):
# 只给静态检查器看,运行时不拦
def speek(self): # 拼错了,mypy 会报 override 无效
return '汪汪!'
print('运行时照样能跑:', Dog().speek())输出:
1
运行时照样能跑: 汪汪!
属性方法
-
说明
用
@property装饰器把方法伪装成属性:读的时候自动执行 getter,赋值时执行 setter,del时执行 deleter。它是 Python 实现 getter / setter 的常规做法,也让「先写字段、以后再加拉逻辑」不至于破坏调用方代码。 -
基本用法:getter / setter / deleter
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 MyClass:
def __init__(self, value):
self._value = value # 真正存数据的地方,用单下划线表示内部
def value(self): # getter:读 obj.value 时触发
print('Getting value')
return self._value
def value(self, new_value): # setter:写 obj.value = ... 时触发
print('Setting value')
self._value = new_value
def value(self): # deleter:del obj.value 时触发
print('Deleting value')
del self._value
obj = MyClass(10)
print(obj.value)
obj.value = 20
del obj.value
try:
obj.value
except AttributeError as e:
print('删掉之后再读:', type(e).__name__, e)
print('实例字典:', obj.__dict__)输出:
1
2
3
4
5
6
7Getting value
10
Setting value
Deleting value
Getting value
删掉之后再读: AttributeError 'MyClass' object has no attribute '_value'
实例字典: {}注意最后两行:
del obj.value删的是_value,getter 还是会跑(所以先打印Getting value),然后在self._value这一行抛AttributeError。报错信息指向_value而不是value,排查时别被带偏。 -
典型用法:校验 + 计算属性
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
34class Circle:
def __init__(self, radius):
self.radius = radius # 这里就已经走 setter 了
def radius(self):
return self._radius
def radius(self, value):
if value <= 0:
raise ValueError('Radius must be positive')
self._radius = value
def diameter(self): # 只读的计算属性
return 2 * self._radius
def area(self):
return 3.14159 * (self._radius ** 2)
c = Circle(5)
print(c.radius, c.diameter, c.area)
c.radius = 10 # 赋值会走校验
print(c.radius, c.diameter, c.area)
try:
c.radius = -1
except ValueError as e:
print('非法值:', e)
try:
c.diameter = 30 # 只给了 getter,没给 setter
except AttributeError as e:
print('只读属性:', e)输出:
1
2
3
45 10 78.53975
10 20 314.159
非法值: Radius must be positive
只读属性: property 'diameter' of 'Circle' object has no setter -
注意事项
- getter 不能收额外参数(
obj.value没有传参的地方),要参数就别用 property。 - 别在 property 里做耗时或有副作用的操作:读代码的人以为在取字段,看不出它在发请求。
__init__里self.radius = radius会走 setter,校验逻辑因此对构造参数也生效——通常是好事,但要知道有这回事。- 一堆字段都要校验时,
__set_name__描述符或dataclasses+ 手动校验比写十几个@property更省事。
- getter 不能收额外参数(
函数判断
-
问题
怎么判断一个对象能不能像函数那样被调用?
-
方法一:内置
callable()1
2
3
4
5
6
7
8
9
10
11
12def my_func():
pass
class MyClass:
def __call__(self): # 实例有了这个方法就可调用
pass
print(callable(my_func))
print(callable(MyClass)) # 类本身也可调用:调用就是建实例
print(callable(MyClass()))
print(callable('hello'))
print(callable(123))输出:
1
2
3
4
5True
True
True
False
False官方文档的说法是:
callable()返回True只表示「看起来可调」,真调用仍可能失败;返回False则一定调不动。另外它有一段历史——Python 3.0 把它移除了,3.2 又加了回来(原笔记写成「Python 2 中曾被移除」,方向反了)。 -
方法二:查
__call__1
2
3
4
5
6
7
8
9
10
11def is_callable(obj):
return hasattr(obj, '__call__')
print(is_callable(lambda x: x + 1))
print(is_callable([1, 2, 3]))
class C:
pass
print('普通类的实例有 __call__ 吗:', hasattr(C(), '__call__'))
print('但类本身有:', hasattr(C, '__call__'))输出:
1
2
3
4True
False
普通类的实例有 __call__ 吗: False
但类本身有: True类本身有
__call__是因为它的类型是type,实例化走的是元类的__call__,所以hasattr(C, '__call__')为真而hasattr(C(), '__call__')为假。 -
方法三:
collections.abc.Callable类型检查场景用这个,它能和
isinstance以及类型注解配合。1
2
3
4
5
6
7
8
9
10
11
12
13from collections.abc import Callable
def my_func():
return 42
class C:
def __call__(self):
return 1
print(isinstance(my_func, Callable))
print(isinstance(C(), Callable))
print(isinstance(int, Callable)) # 类是可调用的
print(isinstance(123, Callable))输出:
1
2
3
4True
True
True
False注意
Callable的isinstance检查只看有没有__call__,是「结构」判断,不看继承关系。注解里现在推荐直接写collections.abc.Callable[[int], str],typing.Callable从 3.9 起就属于历史写法了。 -
别用
inspect.isfunction()判断可调用它只认
def/lambda造出来的纯函数对象,其他可调用对象一律False。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16from inspect import isfunction
def my_func():
pass
class CallableClass:
def __call__(self):
pass
print(isfunction(my_func)) # 纯函数
print(isfunction(lambda x: x)) # lambda 也算
print(isfunction(print)) # 内置函数
print(isfunction(int)) # 类
print(isfunction(str.upper)) # 方法描述符
print(isfunction(CallableClass())) # 实现了 __call__ 的实例
print(isfunction(my_func.__call__)) # 绑定方法输出:
1
2
3
4
5
6
7True
True
False
False
False
False
False -
怎么选
方法 覆盖范围 典型用例 callable(obj)最广:函数、类、方法、有 __call__的实例通用判断,优先用它 isinstance(obj, Callable)同 callable(),但能进类型注解静态检查 / 声明接口 hasattr(obj, '__call__')同上,但会触发属性查找 一般不必自己写 inspect.isfunction(obj)只认 def/lambda确实只接受纯函数时 inspect.isroutine(obj)函数 + 方法 + 内置函数 想放宽一点时用 实际写法:先
callable()兜住,再在调用处捕获TypeError,比穷举类型可靠。1
2
3
4
5
6
7
8
9
10def safe_call(func, *args, **kwargs):
if not callable(func):
raise TypeError(f'对象 {func!r} 不可调用')
return func(*args, **kwargs)
print(safe_call(print, 'Hello, World!'))
try:
safe_call('not a function')
except TypeError as e:
print(e)输出:
1
2
3Hello, World!
None
对象 'not a function' 不可调用