
写Python写了大半年代码量上来之后你大概率会遇到一个问题明明用一个函数就能搞定的事为什么还要写一个类刚开始接触类和实例化的时候我也觉得这东西抽象绕来绕去不如直接写函数顺手。真到项目里要维护一批相似的对象才发现类的价值不是“炫技”而是把数据和操作数据的代码捆在一起让代码的结构跟着业务走。这篇文章就想把“类的基本构造与实例化”这件事讲透从class关键字怎么写到__init__到底干了什么再到实例化的底层逻辑和容易踩的坑适合刚学完函数、准备往面向对象走的读者也适合写了一段时间类但总感觉哪里没想明白的同学。1. class关键字类的第一块积木怎么搭1.1 一个最小可用的类很多人第一次写类是从抄代码开始的抄着抄着发现最简单的类甚至可以只有一行内容class Empty: pass e Empty() print(type(e)) # class __main__.Empty这段代码看起来不起眼但它把面向对象里最核心的三个概念都包含了Empty是类e是Empty的实例e Empty()这个过程叫实例化。pass没有实际逻辑作用只是占位告诉你类体可以暂时是空的。理解类的一个好角度是把它当成“模板”或“图纸”。图纸本身不能住人但它定义了房子该有的结构实例则是按这个图纸真正建出来的那套房住进去的是每一个具体的人各自有各自的家具和状态。类在定义的时候并不会产生独立的数据真正干活的是实例也就是对象。有一个细节容易忽略class语句本身在执行的时候会创建一个对象这个对象就是类对象。所有的类都是type的实例听起来像套娃但你可以先只记结论类也是Python世界里的普通对象它可以被赋值给变量、放进列表、传给函数。比如MyClass Empty之后再MyClass()跟直接Empty()是一样的效果这种“类即对象”的思维对后面理解元类、装饰器都很有帮助。1.2 命名规范与类的结构习惯类名我建议一律用驼峰命名比如UserProfile、PaymentService不要用user_profile这种下划线风格那是函数和变量的语法。类名首字母大写这一条看起来只是风格问题但一旦代码量上去看到名字就能判断它是类是函数省下的心智成本非常可观。类体内部的结构推荐按照固定的顺序来写先写类属性或常量再写__init__接着是公开方法最后是私有辅助方法。比如class Order: STATUS_NEW new STATUS_PAID paid def __init__(self, order_id): self.order_id order_id self.status self.STATUS_NEW def pay(self): if self.status ! self.STATUS_NEW: raise ValueError(当前状态不能支付) self.status self.STATUS_PAIDSTATUS_NEW和STATUS_PAID是类属性它们属于类本身所有实例共享同一份self.order_id和self.status是实例属性每个实例各有一份。这个区别细说起来能写一大段下一章我会专门聊。1.3 属性与方法一个类的左右手类的核心内容只有两样属性和方法。属性描述“这个对象是什么样”方法描述“这个对象能干什么”。举个例子定义一个Dog类class Dog: def __init__(self, name): self.name name def bark(self): print(f{self.name} 在汪汪叫) d Dog(旺财) d.bark()这里name就是实例属性bark是实例方法。方法跟普通函数最大的区别就是它的第一个参数是self调用的时候由Python自动传入当前实例。你看上去写的是d.bark()实际解释器执行的是Dog.bark(d)所以方法定义里那个self永远不能省。很多初学者问为什么不直接让解释器隐式处理非要手动写self我的理解是Python的设计哲学就是“显式优于隐式”把self写出来让函数和方法的定义保持一致也方便你明确知道方法操作的是哪个对象。如果不写self你调用d.bark()的时候解释器会把d当作第一个参数传进去然后因为参数个数不匹配直接报错。2.init实例化时自动触发的初始化钩子2.1 构造还是初始化先分清这两个概念不少教程把__init__叫构造函数严格说不太准确。Python里对象创建分为两步第一步调用__new__这个方法去申请内存、创建一块空白对象第二步调用__init__去给这块空白填充属性。所以__init__是“初始化方法”真正负责构造空白对象的是__new__。日常写代码你基本不用动__new__除非写单例模式或者元类。我们只需要知道当执行Order(A001)时Python会先给新对象分配内存然后把A001传给__init__里的order_id参数。把__init__理解为“搬家当天做的事”更形象房子是__new__盖好的墙和屋顶都有了但里面是空的__init__就是往屋里搬家具、挂窗帘、布置灯。所以你在__init__里写的所有self.xxx ...本质都是给这个新对象“布置自己的状态”。2.2 self到底是什么身份self是实例化过程里最容易被误解的一个参数。你可以把self理解成“当前这个对象自己的名字”它指向正在被初始化的那个实例。看这段代码class Person: def __init__(self, name, age): self.name name self.age age def introduce(self): print(f我叫{self.name}今年{self.age}岁) p Person(小明, 18) print(p.name) # 小明 p.introduce() # 我叫小明今年18岁在Person(小明, 18)执行的时候Python悄悄创建了一个新的Person实例然后把这个实例作为第一个参数传给__init__所以self指向的就是那个新对象。接着self.name name做的事是给这个新对象添加一个name属性并把参数里传进来的小明存进去。同样p.introduce()翻译过来就是Person.introduce(p)。方法里的self和p是同一个对象所以能读取到刚才存的name和age。如果你硬要猎奇也可以直接通过类调用实例方法Person.introduce(p) # 输出我叫小明今年18岁这种写法不常见但它能帮你彻底看清self的本质它不是魔法就是普普通通的参数位置约定。2.3 初始化参数与默认值的处理技巧__init__跟普通函数一样参数可以设置默认值也可以接受可变参数。比如class Student: def __init__(self, name, score0, tagsNone): self.name name self.score score self.tags tags if tags is not None else []这里有个很经典的天坑默认值不要用可变对象。新手最容易写成def __init__(self, tags[])看起来没问题实际上所有调用这个默认值的实例会共享同一个列表。原因是默认参数在函数定义那一刻就被创建并固定下来了后面所有实例没传tags时拿到的都是这同一个列表对象。所以我的习惯是可变类型参数一律先给None然后在函数体里判断并新建。这个坑在后面的“常见问题”里还会再展开一次因为它真的很容易踩。2.4 实例化发生时的一整套流程把整个过程串起来当执行obj MyClass(arg)时Python内部大致做四件事调用MyClass.__new__(MyClass, arg)分配内存得到一个空白实例这一步一般不写默认是object.__new__如果__new__返回的类型是MyClass的实例继续调用MyClass.__init__(obj, arg)__init__里把外部传入的参数填到self对应的属性上可能还会做数据校验、计算衍生字段整个表达式返回这个已经初始化好的实例赋值给obj。理解这个流程最大的好处是你写__init__的时候会主动思考“在对象正式交付给使用者之前还有什么状态必须准备好”。比如分页数据可以传page和size在__init__里顺手把offset算好后面用到的时候就不用到处重复计算再比如邮箱格式校验、金额转小数这类逻辑放__init__里能让所有入口都保持一致。3. 实例与类图纸和房子到底怎么对应3.1 一个类能造出一堆互不干扰的实例类能存在的最大意义就是批量创建结构相同、状态不同的对象。同样是Student可以造出一个又一个实例彼此之间互不影响s1 Student(Alice, 90) s2 Student(Bob, 80) s1.score 60 print(s2.score) # 依然是80这里s1.score改成60不影响s2因为每个实例的score都是独立存储在对象自己的命名空间里。如果你拿id(s1)和id(s2)看两个是完全不同的内存地址。这个特性对写业务代码很重要一份用户列表、一批订单记录循环实例化的时候每个对象都带着自己的独立数据你想怎么改就怎么改不用担心“改了一个别的全跟着变”。3.2 实例属性与类属性的查找顺序前面提到类属性是共享的实例属性是独立的。看个例子class Counter: count 0 def __init__(self): self.count 0 c1 Counter() c1.count 1 print(Counter.count) # 0 print(c1.count) # 1这里其实藏着一个精妙的机制c1.count 1并不是去修改类属性Counter.count而是先在实例上创建了一个新的实例属性count初始值是类属性的0然后加一变成1。类属性纹丝不动。查找一个对象属性的时候Python的规则是“先实例、再类、再继承的父类”。实例上没有找到属性才会去找类的属性。这就是为什么静态方法和类方法里操作同一个类属性能实现所有实例共享同一份状态。要真正修改类属性应该用Counter.count 100这样的类级赋值或者在类方法里通过cls修改。直接c1.count 1只会在实例上做文章这也是初学者最容易困惑的点。3.3 动态属性、限制出现与__slots__Python对象默认可以在运行时增加新的属性看着很方便p Person(小李, 30) p.address 某小区 print(p.address)类型本身没有定义address但运行时可以硬塞进去。这在写小脚本时很爽但在规范的项目里也是个隐患属性随手挂类到底有什么字段越来越难查。如果你希望对象只允许固定的属性存在可以用__slots__限制class Person: __slots__ (name, age) def __init__(self, name, age): self.name name self.age age p Person(小李, 30) p.address 某小区 # AttributeError加了__slots__之后类不再创建__dict__来存属性属性只能是被列出的那几个名字。好处是省内存——当你需要同时创建成千上万个对象时这个省下来的内存非常可观坏处是灵活性下降。我的建议是普通业务代码不必强上__slots__追求性能的数据模型可以考虑。3.4 引用与复制别让两个名字指到同一个对象实例化的反方向问题是有人分不清“新对象”和“新引用”。看这段代码a Student(Alice, 88) b a b.score 50 print(a.score) # 50把a赋值给b并没有创建新实例只是让两个变量同时指向同一个对象。你改b的属性a肉眼可见地跟着变因为它们本来就是同一个东西。想真正复制一个独立对象要么自己写一个方法去创建新实例并拷贝属性要么用标准库的copy模块。浅拷贝用copy.copy深拷贝用copy.deepcopy具体选哪个取决于属性里有没有嵌套的可变对象。这是一块很容易出bug的领域后面也会再聊。4. 方法家族实例方法、类方法、静态方法怎么选4.1 三种方法的定位对比类里的方法不只有实例方法一种还有类方法和静态方法。写法区别主要在于装饰器和第一个参数类型装饰器第一个参数通过实例调用通过类调用实例方法无self指向实例推荐需手动传实例类方法classmethodcls指向类可以推荐静态方法staticmethod无固定参数可以推荐粗糙地记忆实例方法处理“实例的属性”类方法处理“类的属性”静态方法只是放在类命名空间里的普通函数。4.2 实例方法是最常用的默认选择绝大多数场景你就用实例方法因为它能访问实例属性。比如前面写的bark、introduce、pay全是实例方法。实例方法获得self之后既能看到self.name这种数据又能调用self.some_other_method()是最灵活的。4.3 类方法的核心价值操作类状态与提供替代构造函数类方法的典型场景有两个。第一个是操作类属性class Config: mode dev classmethod def switch_to_prod(cls): cls.mode prod通过Config.switch_to_prod()修改mode不管通过类还是实例调用最终改变的都是类级状态。第二个用途是提供替代的构造方式。一个日期类可以从字符串创建也可以从时间戳创建如果全都塞进__init__参数判断会很混乱。更清晰的做法是class Date: def __init__(self, year, month, day): self.year year self.month month self.day day classmethod def from_string(cls, text): year, month, day map(int, text.split(-)) return cls(year, month, day)from_string内部用cls(...)而不是Date(...)好处是如果将来这个类被继承子类调用from_string会返回子类的实例动态绑定不会写死父类。4.4 静态方法跟类和实例无关的工具函数静态方法既不接收self也不接收cls它就是一个放在类内部的普通函数。什么时候需要它当函数从业务逻辑上跟这个类紧密相关但函数体又不需要访问任何类或实例状态时。比如温度转换class Temperature: scale celsius staticmethod def to_fahrenheit(celsius): return celsius * 9 / 5 32to_fahrenheit纯粹是一个换算公式不依赖self也不依赖cls。写成一个静态方法调用方用Temperature.to_fahrenheit(100)这样带着类名前缀语义上比一个独立函数更清晰。有一类问题是为什么要用静态方法不用模块级函数主要是组织代码。静态方法把功能归属到类名下既照顾了调用语义又避免了在方法里写一堆跟类和实例无关、却硬要用self的假实例方法。5. 构造与实例化阶段最容易掉的五个坑5.1 忘了在方法里写self新手经常写出class Dog: def __init__(self, name): self.name name def bark(): # 忘了self print(汪汪叫) d Dog(旺财) d.bark() # TypeError: bark() takes 0 positional arguments but 1 was given报错信息其实已经把答案说得很明白实例调用d.bark()时Python会自动把d作为第一个位置参数传入而bark没有接受任何参数于是冲突。解决方式就是在方法定义里写self即使方法体内不用它也要占住这个位置。5.2 默认参数用了可变对象这是Python最经典的坑之一class Student: def __init__(self, name, hobbies[]): self.name name self.hobbies hobbies看起来没问题但两个不传hobbies的实例会共享同一个列表s1 Student(Alice) s1.hobbies.append(running) s2 Student(Bob) print(s2.hobbies) # [running]原因就在前面的默认参数机制里。正确写法是用Nonedef __init__(self, name, hobbiesNone): self.name name self.hobbies hobbies if hobbies is not None else []每次实例化时都重新生成一个新的空列表互不干扰。5.3 试图通过实例修改类属性前面提到的Counter.count例子就是这个问题。想在实例层面修改共享状态其实会新建一个实例属性把类属性遮住原类属性没变。如果确实需要改变类属性请通过类名修改或者写一个类方法去操作cls。同时也要根据实际业务想清楚这个状态到底是所有实例共享的还是每个实例独立的类型设计错了后面改起来很痛苦。5.4 拿普通赋值“复制”对象再做一次强调import copy a [hello] b a c copy.copy(a) d copy.deepcopy(a)对自定义类也一样。浅拷贝复制的是外层对象里面的嵌套列表还是和大家共享如果你改了内层元素原对象的嵌套数据也会变深拷贝则会递归复制所有嵌套结构代价是更耗时、更占内存。判断逻辑很简单如果对象内部只有不可变类型浅拷贝基本够用一旦属性里挂着列表、字典、自定义对象多半要上深拷贝。实在拿不准时先浅拷贝出现数据联动再认真检查。5.5 手动调用__init__造成双重初始化有人为了“重新设置对象状态”会直接这么干p Person(小明, 18) p.__init__(小红, 20)虽然这种调用能成功但它绕过了正常流程容易让代码可读性变差也容易在特殊场景下踩到资源重复初始化的坑。如果需要重置状态更清晰的方案是提供一个公开的reset()方法内部再调用初始化逻辑class Person: def __init__(self, name, age): self.reset(name, age) def reset(self, nameNone, age0): self.name name self.age age这样想重新赋值时调用p.reset(小红, 20)语义清楚也避免了__init__被反复执行。6. 设计一个类之前我建议你先想清楚这几件事6.1 先分清“不变的”和“各自独立的”拿到需求先别急着敲代码。把所有字段在草稿上分两类一个是所有同类对象不需要各自保存的东西比如配置常量、状态枚举、转换比例一个是每个对象都必须独立持有的数据比如姓名、余额、订单号。前者放类属性或常量后者放实例属性。不要嫌麻烦。类的边界一旦划错后续加字段、改方法牵一发动全身。我见过太多代码把所有数据全塞进实例属性连“圆周率”这样的常量都写进每个实例既不美观又浪费内存。6.2 用__repr__让对象可以“被看懂”调试类代码最烦的就是打印一个对象只看到内存地址。写类的时候顺手加上__repr__class Student: def __init__(self, name, score): self.name name self.score score def __repr__(self): return fStudent(name{self.name!r}, score{self.score})之后再print(s1)就能看到Student(nameAlice, score88)。别小看这一行排查问题的时候它能省一半眼力。有同学问__str__和__repr__的区别__str__更偏向给人读的展示__repr__更偏向重建对象的信息但如果只想加一个加__repr__因为str()找不到__str__时也会退回用__repr__。6.3 构造参数太多时考虑类方法或数据类如果__init__参数超过四五个代码可读性会明显下降。与其堆一堆参数不如提炼参数对象或者用标准库dataclasses来减负from dataclasses import dataclass dataclass class Point: x: float y: floatdataclass装饰器会根据类型注解自动生成__init__、__repr__、__eq__等常见方法写数据模型类的时候非常方便。但要注意dataclass不能替代你思考类的方法它只是把样板代码替你写了业务逻辑该自己写还是要自己写。6.4 判断对象类型慎用type多用isinstance实例化之后不可避免要判断某个对象是不是某个类的实例。新手喜欢用if type(obj) is Person:这种写法在继承场景下会吃亏。如果你有一个Student(Person)type(student) is Person会是False。更稳的做法是if isinstance(obj, Person):isinstance会沿着继承链向上匹配Student的实例也会被认为是Person的实例符合面向对象的多态思想。除非你极其确定只要精确类型匹配否则一律用isinstance。写类写到一定次数会发现构造和实例化本身并不难难的是一开始就设计出合适的关系。我的一个小习惯是定义一个类之前先在脑子里过一遍“谁能创建这个对象”“创建完它要做什么”“它跟别的对象怎么协作”。这三个问题想清楚__init__需要哪些参数、需要哪些方法基本就八九不离十了。真正的高手不是会写复杂的类而是能把一个类写得让别人接手时觉得理所当然。这也是我从一次次重构和Debug里得到的最真实的体会。