AI 技术博客
返回首页
Python 基础 20 分钟阅读

方法与 self:实例方法、类方法与静态方法

Python面向对象方法selfclassmethod

引言

前两篇完成了「模板(类)→ 具体对象(实例)」的思维转变,并让每个实例在出生那一刻就自动初始化。但到目前为止,我们的类还只是装数据的容器——程序真正要做什么,仍靠散落在类外层的顶层函数。本篇给类装上「行为」:方法(method)。

Python 的方法不止一种,这是它与其他语言很不一样的地方。同一个 def,写法和意图不同,会有三种截然不同的「活法」:普通写在类里的函数默认是实例方法(instance method),第一个参数收到实例;加上 @classmethod 装饰器变成类方法(class method),第一个参数收到类本身;加上 @staticmethod 装饰器变成静态方法(static method),什么绑定都没有。本篇的目标是让你彻底搞懂三者的机制差异(尤其是那个反复出现的 self 和 cls 在调用时到底被塞了什么),并学会在实际工程里选型。本篇还会回答几个常见疑问:为什么 print 某个方法时显示 bound method?为什么第六章的 namedtuple 要设计成「工厂」的形式?类方法为什么是工厂方法的标准答案?

阅读前提:class 语法与 __init__(第一、二篇)、函数与装饰器概念(第四章,我们只用装饰器的字面用法,不深究其原理)、namedtuple 的工厂形态(第六章)。封装、继承、多态留给第四到六篇——本篇的方法体系是它们的地基。

概念与原理

方法也是属性:同一套查找链

先建立一个观念:方法就是定义在类体里的函数(function)。第四章讲过「函数是一等对象」,这个结论在类里同样成立——类体里的 def 语句创建的依然是普通函数对象,它作为属性存在类的 __dict__ 里(回想第一篇的账本图景)。你调用 s.method() 时,走的正是第一篇讲的那条属性查找链:实例账本找不到这个函数,去类账本找,找到之后再加一步「绑定」——这一步的细节是本篇重点。

所以方法名与属性名共用同一个命名空间:给类加一个叫 name 的方法,就会遮住同名的类属性或实例属性。名字冲突时要小心,这也是为什么实例属性一般用名词、方法一般用动词(price 是数据,discount() 是动作),让命名负担降到最低。

实例方法:self 自动传递

实例方法是我们最熟悉的形式:第一个参数写 self,调用时无需手动传。机制是:Python 从类的函数构造出一个「绑定方法(bound method)」对象,把实例牢牢绑在里面,调用时自动放进第一个参数位。

实测最直观——打印方法对象本身,看它的类型:

class Greeter:
    def greet(self, name):
        return f"你好,{name}!"

g = Greeter()
bound = g.greet          # 从实例上取方法
plain = Greeter.greet    # 从类上取方法

print(type(bound), type(plain))
print(bound("Bob"))
print(plain(g, "Bob"))
print(bound == plain)
print(bound.__self__ is g)
print(bound.__func__ is plain)
<class 'method'> <class 'function'>
你好,Bob!
你好,Bob!
False
True
True

逐行解读:

  • type(g.greet) 是 <class 'method'>——从实例上取到的是绑定方法对象;type(Greeter.greet) 是 <class 'function'>——从类上取到的是原始函数。同一个 def,两种取法,两个不同的对象。
  • bound("Bob") 与 plain(g, "Bob") 输出相同:绑定方法替你做了「把 g 塞进第一个参数位」这件事,g.greet("Bob") 就是 Greeter.greet(g, "Bob") 的简写(在上篇我们验证过等价性,本篇从对象层面看到了原因)。
  • bound == plain 为 False:绑定方法对象与原始函数不是同一个对象。
  • bound.__self__ is g 为 True、bound.__func__ is plain 为 True:绑定方法内部确实持有「被绑定的实例」和「原始函数」两个引用,__self__ 与 __func__ 是它的两个公开属性。

如果从类上调用却忘记传 self,参数就对不上:

class Greeter:
    def greet(self, name):
        return f"你好,{name}!"

Greeter.greet()   # 缺了 self 和 name
TypeError: Greeter.greet() missing 2 required positional arguments: 'self' and 'name'

这行报错完美印证了机制:self 就是一个普通的位置参数,只是平时由绑定方法自动填充,从类上直接调用时它一个都不会替你补。

类方法:@classmethod 与 cls

给方法加上 @classmethod 装饰器后,第一个参数约定写成 cls,调用时被自动塞进的是类本身,而不是实例。注意一个容易混淆的细节:即使你通过实例调用类方法,塞进去的依然是类。

class Demo:
    def instance_method(self):
        return f"实例方法:我看到了实例 {type(self).__name__}"

    @classmethod
    def class_method(cls):
        return f"类方法:我收到了类 {cls.__name__}"

    @staticmethod
    def static_method(x):
        return f"静态方法:只收到参数 {x}"

d = Demo()
print(d.instance_method())
print(d.class_method())
print(d.static_method(42))
print(Demo.class_method())
print(Demo.static_method(7))
实例方法:我看到了实例 Demo
类方法:我收到了类 Demo
静态方法:只收到参数 42
类方法:我收到了类 Demo
静态方法:只收到参数 7

注意第二行:d.class_method()(通过实例调用)的输出与第四行 Demo.class_method()(通过类调用)完全一样,都是「收到了类 Demo」——实例调用类方法时,绑定的是类而非该实例。这一点必须背下来,它是类方法与实例方法在绑定对象上的本质区别。

类方法的两个典型用途。其一是工厂方法(factory method):用不同的参数形态创造出类的实例。第六章我们见过 collections 模块的 namedtuple 工厂——它接收类型名与字段名,返回一个新类;类方法工厂干的是相似的事:接收一种「别样的输入」,返回一个新实例。把创建逻辑收敛在类内部,调用方不必关心内部换算。其二是读写类级状态:比如改类属性、查询类级配置,这类操作不针对某个具体实例,却天然属于这个类。

静态方法:@staticmethod 与纯工具函数

@staticmethod 既不接收 self 也不接收 cls,它就是把一个普通函数放进类的命名空间,没有任何绑定发生——反过来说,Python 根本无法知道它是「谁的方法」。既然没有绑定,它的调用方式(用类调用、用实例调用)对结果没有任何影响,参数原样传递。

它存在的唯一理由是归属:这个函数从业务上属于这个类(比如「校验优惠券编码格式」显然和优惠券强相关),但实现上既不需要实例数据也不需要类状态。归类到类里,命名空间清爽,调用点 Coupon.is_valid_code(...) 也自带语境,比裸奔在模块顶层更可读。

三种方法的对比与选型

方法类型装饰器第一个参数通过实例调用时第一个参数典型场景
实例方法无self(实例)该实例读写实例属性(状况与行为)
类方法@classmethodcls(类)类(不是实例!)工厂方法、读写类级状态
静态方法@staticmethod无——(参数原样)与类强相关的纯规则/工具函数

选型的决策顺序可以记成一句口诀:要碰实例数据,用实例方法;要碰类级数据或要造新实例,用类方法;谁也不碰,用静态方法。很多新人纠结「静态方法和模块顶层函数有什么区别」,答案只有归属感与调用语境,机制上毫无差别——这也是为什么静态方法在三种方法里最容易被误用(比如往里面塞本应属于实例方法的逻辑)。

命名惯例:self 与 cls 的改名风险

self 与 cls 都是纯惯例(PEP 8 建议),语言层面没有任何强制。上篇我们验证过 self 可以改成 this 照样运行,cls 同理。改名的真实风险在于生态:所有 Python 开发者、绝大多数 IDE 与静态检查工具(类型检查器、lint 工具)都按 self/cls 惯例识别方法的第一参数并推断其类型,一旦改名,自动补全、跳转、类型推断全部失效,徒增沟通成本。结论很简单:别改。这也顺带回答了一个常见疑问:「为什么这个参数没有默认值却不用传?」——因为它由绑定机制自动填充,从实例调用时永远轮不到你操心。

操作与实现

机制讲完,逐个落到完整可运行的代码。

绑定方法对象与显式传参

class Student:
    def __init__(self, name):
        self.name = name

    def introduce(self):
        return f"我是 {self.name}"

s = Student("Alice")
print(s.introduce())
print(Student.introduce(s))
我是 Alice
我是 Alice

两条调用路径殊途同归:s.introduce() 是绑定方法自动传 self,Student.introduce(s) 是手动传。理解这一点后,你会明白为什么「实例方法里调用另一个实例方法」必须写 self.xxx()——裸写 xxx() 只是普通名字解析,类命名空间并不参与。

类方法工厂:把「不同输入」换算成标准构造

第六章的 namedtuple 本身就是工厂:接收类型名与字段名,返回一个新类。类方法工厂把同一思想用在「制造实例」上——调用方手里只有直径,类内部负责换算成半径再构造:

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @classmethod
    def from_diameter(cls, diameter):
        # 类方法工厂:直径除以 2 得到半径,再调用 cls() 完成构造
        return cls(diameter / 2)

c1 = Circle(4)
c2 = Circle.from_diameter(8)

print(c1.radius, c2.radius)
print(type(c2))
print(isinstance(c2, Circle))
4 4.0
<class '__main__.Circle'>
True

from_diameter 内部写的是 cls(...) 而不是 Circle(...)——这是关键细节:cls 指向「当前类」,将来如果出现 Circle 的子类,通过子类调用该工厂时,cls 会自动变成子类,工厂无需改动就能产出子类实例(这一点的威力在第 5 篇继承中会彻底释放)。type(c2) 证实实例由 Circle 正常构造,各种属性、方法继承链一切如常。

静态方法:规则判断与实例无关

class Coupon:
    def __init__(self, code, discount):
        self.code = code
        self.discount = discount

    @staticmethod
    def is_valid_code(code):
        # 静态方法:不触碰 self 与 cls,只做纯规则判断
        return len(code) == 8 and code.isalnum()

print(Coupon.is_valid_code("SUMM8888"))
print(Coupon.is_valid_code("abc"))
c = Coupon("SUMM8888", 0.8)
print(c.is_valid_code(c.code))
True
False
True

is_valid_code 只依赖传入参数与自身规则,不读任何实例状态——所以在 Coupon 创建实例之前、甚至不创建任何实例,都能用它做校验。这是表单校验、密码强度检测、日期格式判断这类纯规则函数的典型形态。

综合示例:员工薪资系统

把三种方法放进同一个真实场景:员工薪资。税率是全员类级状态(类方法改)、净薪依赖具体员工数据(实例方法算)、薪资合理性是纯规则(静态方法判):

class Employee:
    company = "云杉科技"
    tax_rate = 0.08   # 类属性:全员统一税率

    def __init__(self, name, base_salary):
        if base_salary <= 0:
            raise ValueError("base_salary 必须为正数")
        self.name = name
        self.base_salary = base_salary

    def net_salary(self):
        # 实例方法:需要具体员工的数据
        return self.base_salary * (1 - self.tax_rate)

    @classmethod
    def update_tax_rate(cls, new_rate):
        # 类方法:修改统一税率,影响所有员工
        cls.tax_rate = new_rate

    @staticmethod
    def is_reasonable_salary(salary):
        # 静态方法:与具体实例无关的规则判断
        return 0 < salary < 1000000

alice = Employee("Alice", 12000)
print(alice.net_salary())

Employee.update_tax_rate(0.10)
print(Employee.tax_rate)
print(alice.net_salary())

print(Employee.is_reasonable_salary(12000))
print(Employee.is_reasonable_salary(10 ** 9))
11040.0
0.1
10800.0
True
False

三种方法各司其职:net_salary 读取各自的 base_salary,同一员工在税率调整前后净薪从 11040 变 10800,说明 tax_rate 确实被类方法全局更新;is_reasonable_salary 不需要任何实例,直接以类名调用。一个类里同时出现三种方法并不可怕——可怕的是分不清该用哪一种。

易错点与陷阱

陷阱一:忘了 self(或把 self 当普通参数乱传)

漏写 self 是新手最高频的错误之一:定义时方法只有一个参数,实例调用时却被塞进「实例 + 参数」两个值,语法上没问题,运行时立刻露馅(消息见上文「绑定方法对象」一节的实测输出:TypeError: Greeter.greet() missing 2 required positional arguments: 'self' and 'name')。反之,从类上直接调用 Greeter.greet() 忘记传实例,报错信息里点名缺的正是 'self'——它确实是一个真实存在的参数,只是平时被绑定机制代填罢了。

陷阱二:忘了写 @classmethod / @staticmethod 装饰器

不加装饰器的函数永远是实例方法——即使你把第一个参数命名为 cls:

class Demo:
    def class_method(cls):   # 忘记 @classmethod
        return cls

d = Demo()
print(type(d.class_method()))
<class '__main__.Demo'>

一切「正常」运行,没有任何报错——但 cls 收到的不是类,而是实例 d!这类 bug 最阴险:程序不崩溃,行为却悄悄错掉(比如你以为在改类属性,实际在改实例属性)。同理,静态方法忘了装饰器会变成实例方法,参数全部错位。排查经验:看到类体里的函数第一个参数叫 cls 却不是类方法,或函数逻辑完全不使用 self 却定义成了实例方法,基本可以断定装饰器漏了。

陷阱三:在类方法/静态方法里访问 self

类方法与静态方法没有绑定实例,方法体里写 self.xxx 会直接抛 NameError(在本文实测中信息为 NameError: name 'self' is not defined)。反过来说,如果你发现自己在类方法里疯狂访问实例属性,那说明这个方法本应是实例方法——选型错了,不是名字错了。

陷阱四:实例方法内部裸调用另一个方法

class Student:
    def greet_a(self):
        return "你好"

    def greet_b_broken(self):
        return greet_a() + "!"   # 忘了 self.

s = Student()
# print(s.greet_b_broken())
NameError: name 'greet_a' is not defined. Did you mean: 'self.greet_a'?

方法名只生活在类命名空间里,裸写 greet_a() 走的是普通名字解析,自然找不到。Python 3.12 的报错还贴心给出了建议 Did you mean: 'self.greet_a'?——看到这个提示,直接改成 self.greet_a() 即可。这也是一个本体感很强的自查信号:方法互相调用时,self. 前缀是唯一合法的入口。

小结

方法就是定义在类体里的函数,沿属性查找链被发现,再根据「绑定到谁」分成三类:实例方法绑定实例(self 自动填充,obj.m(...) 等价于 Class.m(obj, ...),实例上取到的是携带 __self__/__func__ 的绑定方法对象);类方法绑定类(cls 即使经实例调用也指向类,适合工厂方法与类级状态);静态方法零绑定(纯规则工具函数,只是寄居在类命名空间)。选型口诀:碰实例数据用实例方法,造实例或碰类数据用类方法,谁也不碰用静态方法。self/cls 只是强惯例,改名不报错却损害可读性与工具链。下一篇进入守护实例数据的第一步——封装与 property。

练习与思考题

  1. 为第二篇的 Laptop 类补充一个类方法工厂 from_config(cls, config),接收字典 {"model": ..., "cores": ..., "ram": ..., "price": ...} 返回对应实例;再补充一个静态方法 is_budget(price) 判断价格是否低于 3000。分别用「类调用」和「实例调用」验证类方法与静态方法的行为。

  2. 不运行,推断下面代码输出什么,再实际运行核对:

    class Demo:
        @classmethod
        def who(cls):
            return cls
    
    d = Demo()
    print(type(d.who()))
    print(d.who() is Demo)
  3. 动手实验:在 Employee 类(本文综合示例)里加一个方法 def get_company(self): return self.company,思考它与类方法读取 company 的区别;再试着把 update_tax_rate 的 @classmethod 装饰器删掉运行,观察行为如何悄悄变化(对照「陷阱二」)。

  4. 思考题:namedtuple 本身是一个工厂函数而不是类,为什么 Python 的容器库里倾向于用「工厂」而不是「多重构造参数」来设计它的入口?结合类方法工厂的思想,谈谈「把对象创建的知识收进类内部」这件事对调用方的好处。