Python面向对象编程从零入门:用Rectangle类理解封装与self本质
2026/7/14 5:23:03 网站建设 项目流程

1. 为什么学Python的OOP,不能从“定义”开始讲?

我带过几十期Python入门班,每次讲到面向对象,总能看到学生盯着PPT上“类是抽象的模板,对象是具体的实例”这句话发呆。不是他们笨,是这句话本身就没解决任何实际问题。你刚写完三行print("Hello World"),突然被扔进“抽象”“封装”“继承”“多态”四个大词里,就像教人骑自行车,先发一本《牛顿力学在两轮机械系统中的应用白皮书》——方向没错,但脚根本没碰到踏板。

这系列文章,就是为了解决这个断层。我们不从哲学定义出发,而是从你已经会写的代码里长出OOP。比如你肯定写过这样的函数:

def calculate_area(length, width): return length * width def calculate_perimeter(length, width): return 2 * (length + width) # 调用它 room_area = calculate_area(5.2, 4.8) room_perim = calculate_perimeter(5.2, 4.8)

这段代码没问题,但问题藏在细节里:5.24.8这两个数字,代表什么?是房间的长宽?还是桌子的?还是画布的像素尺寸?函数本身不告诉你。下次你要算另一个房间,得再传一遍5.2, 4.8;要是房间数据变了,得改所有调用点。更麻烦的是,如果除了面积、周长,你还想算对角线长度、判断是否为正方形、甚至打印一句“这是我的客厅”,这些新功能就得不断给函数加参数、加if分支,函数会越来越臃肿,越来越难维护。

OOP要解决的,就是这个“数据和行为脱节”的问题。它不是否定函数,而是把相关的数据(长、宽)和操作这些数据的函数(算面积、算周长)打包捆在一起,起个名字叫Rectangle。从此,5.24.8不再是一串孤立的数字,而是属于某个Rectangle对象的固有属性;calculate_area()也不再是飘在空中的函数,而是这个对象自带的能力。你创建一个Rectangle,它就天然拥有长、宽、算面积、算周长这些“身份”和“技能”。这才是OOP最原始、最朴素的动机——让代码更像现实世界里的东西:一张桌子有长宽高(属性),能承重、能放东西(方法),它自己就知道怎么描述自己、怎么做事。

所以,Part 1的核心,就是带你亲手把这个“打包”过程做出来。不讲虚的,就从class Rectangle:这一行开始,一行一行敲,看它怎么把零散的数据和函数,变成一个有血有肉的“东西”。你不需要记住“封装是将数据和方法捆绑在一起的过程”,你只需要记住:当我需要管理一组相关数据,并且这些数据总要一起被某些操作处理时,我就该考虑建一个类了。这就是你今天能立刻用上的判断标准。

2. 类与对象:从“图纸”到“房子”的完整建造流程

2.1 类:不是模板,而是“制造说明书”

很多教程说“类是对象的模板”,这个比喻容易让人误解。模板听起来是静态的、只读的。但Python里的class,更像一份动态的“制造说明书”,它不仅规定了造出来的东西长什么样(有哪些属性),还规定了它能干什么(有哪些方法),甚至规定了它出厂时的默认状态(初始化逻辑)。

我们来写第一份说明书:

class Rectangle: pass

就这么简单?对,pass在这里不是偷懒,它是一个占位符,告诉Python:“这份说明书我先立个flag,内容稍后补。” 此刻,Rectangle这个名字在Python里已经存在了,但它还什么都不会。你可以把它理解成一个刚注册的公司名,营业执照拿到了,但还没招人、没租办公室、没买设备。

验证一下:

print(Rectangle) # <class '__main__.Rectangle'> print(type(Rectangle)) # <class 'type'>

看到<class 'type'>了吗?这说明Rectangle本身也是一个对象,它的类型是type。在Python里,“类”本身就是一种特殊的对象,叫做“元类”的实例。这个知识点现在不用深究,但记住一点:class语句执行后,Python就在内存里创建了一个type类型的对象,名字叫Rectangle。这个对象,就是我们后续制造所有Rectangle实例的“母体”。

2.2 创建对象:调用类,就是启动一次“生产流水线”

有了说明书,怎么造出具体的东西?答案是:调用它。就像你拿着建筑图纸去找施工队,你得说“请按这份图纸,给我建一栋房子”。在Python里,调用类,就是在启动一次“生产流水线”。

my_room = Rectangle() your_room = Rectangle()

这两行代码,就是两次独立的“生产指令”。my_roomyour_room就是两个全新的、彼此独立的Rectangle对象(也叫实例)。它们都遵循同一份说明书(Rectangle类),但它们是各自独立的个体。

验证它们的独立性:

print(my_room) # <__main__.Rectangle object at 0x7f8b1c0a1d90> print(your_room) # <__main__.Rectangle object at 0x7f8b1c0a1e10>

注意看内存地址(at 0x...),完全不同。这证明它们是两个不同的对象,就像两栋不同的房子,即使图纸一样,它们的门牌号(内存地址)也不同。

提示:my_roomyour_room这两个变量,存储的不是对象本身,而是指向对象的“指针”或“引用”。你可以把它们想象成两把钥匙,一把开my_room这栋楼的门,一把开your_room那栋楼的门。钥匙不同,门后的空间也完全隔离。

2.3 属性:给对象“贴标签”,让它记住自己是谁

现在,my_roomyour_room都是空壳子。它们知道自己的身份是Rectangle,但不知道自己有多长、多宽。我们需要给它们“贴标签”,也就是设置属性(Attribute)

最直接的方式,是在对象创建后,用点号(.)语法动态添加:

my_room.length = 5.2 my_room.width = 4.8 your_room.length = 3.5 your_room.width = 2.2

现在,my_room有了lengthwidth两个标签,your_room也有了一套完全独立的标签。你可以随时取用:

print(f"My room area: {my_room.length * my_room.width}") # My room area: 24.96 print(f"Your room area: {your_room.length * your_room.width}") # Your room area: 7.7

这种动态添加属性的方式,在Python里是完全合法的,也是它灵活性的体现。但问题来了:如果每个Rectangle对象都需要手动设置lengthwidth,代码会变得冗长且容易出错。万一有人忘了给your_roomwidth,后面计算时就会报AttributeError。我们需要一种更可靠、更自动化的办法。

2.4__init__方法:对象的“出生仪式”,确保每个实例都带着必需品

这就是__init__方法登场的时候了。它不是一个普通的方法,它是Python的“构造器”(Constructor),当每次调用Rectangle()创建新对象时,Python会自动、强制地执行它。你可以把它理解为对象的“出生仪式”——每个新生命诞生,都必须完成这个仪式,领取自己的初始装备。

我们来升级Rectangle说明书:

class Rectangle: def __init__(self, length, width): self.length = length self.width = width

关键点解析:

  • def __init__(self, length, width)::定义了一个特殊方法。__init__是固定名称,前后各有两个下划线,这是Python的“魔法方法”(Magic Method)命名规范。
  • self:这是一个约定俗成的名字,代表正在被创建的那个新对象本身。你可以把它想象成“我”或者“当前实例”。在__init__里,self就是即将诞生的my_roomyour_room
  • self.length = length:这行代码的意思是,“把传进来的length参数的值,赋给self这个对象的length属性”。同理,self.width = width

现在,创建对象就变成了一个“带参数的调用”:

my_room = Rectangle(5.2, 4.8) # 创建时,自动执行 __init__(my_room, 5.2, 4.8) your_room = Rectangle(3.5, 2.2) # 创建时,自动执行 __init__(your_room, 3.5, 2.2)

整个过程是全自动的:

  1. 你写下Rectangle(5.2, 4.8)
  2. Python在内存里分配一块新空间,准备造一个Rectangle对象;
  3. Python把这个新对象的引用,作为第一个参数self,连同你传的5.24.8,一起交给__init__方法;
  4. __init__方法执行self.length = 5.2self.width = 2.2,给这个新对象贴上标签;
  5. __init__执行完毕,Python把那个已经贴好标签的新对象,返回给你,赋值给my_room

注意:__init__方法没有return语句,或者说,它默认返回None。但Python会自动把那个初始化好的对象返回。你不需要、也不应该在__init__里写return self

2.5 方法:让对象“自己动手”,而不是靠外部函数

现在,my_roomyour_room都有了lengthwidth属性。下一步,让它们能“自己动手”算面积,而不是每次都靠外面的calculate_area()函数。

在类内部定义的函数,就叫方法(Method)。它和普通函数最大的区别在于,第一个参数必须是self,这样它才能访问到调用它的那个对象的属性。

我们给Rectangle加上一个area()方法:

class Rectangle: def __init__(self, length, width): self.length = length self.width = width def area(self): return self.length * self.width

使用它:

my_room = Rectangle(5.2, 4.8) print(f"My room area: {my_room.area()}") # My room area: 24.96

看这行my_room.area():它看起来像在调用一个函数,但其实发生了三件事:

  1. Python找到my_room这个对象;
  2. my_room的类(Rectangle)里,找到名为area的方法;
  3. 自动把my_room作为第一个参数(self),然后执行area(my_room)

所以,area()方法里的self.length,指的就是my_room.lengthself.width就是my_room.width。这个self,就是连接方法和它所属对象的桥梁。

实操心得:我最初学OOP时,总忘记在方法定义里写self,结果一运行就报TypeError: area() takes 0 positional arguments but 1 was given。这是因为Python在调用时,总会自动传入self,如果你没在定义里声明它,Python就懵了:“咦?我给了你一个参数,你怎么说不要?” 这个错误非常经典,几乎每个初学者都会踩一次。记住:类里的所有方法,第一个参数必须是self(或任何你喜欢的名字,但强烈建议用self

3. 深度拆解:self__init__与属性背后的内存真相

3.1self到底是什么?一个被严重低估的“隐身人”

self这个词本身没有任何魔法,它只是一个参数名。你完全可以把它改成thisme甚至banana

class Rectangle: def __init__(banana, length, width): banana.length = length banana.width = width def area(banana): return banana.length * banana.width

这段代码完全合法,也能正常工作。但没人这么干,因为self是Python社区的铁律,它清晰地表达了“这个方法作用于自身”的语义。放弃self,就像开车不打转向灯,技术上可行,但会让别人看不懂你的意图。

self的真正价值,在于它揭示了Python对象模型的核心:一切都是对象,对象通过引用传递

当你写my_room = Rectangle(5.2, 4.8)时,my_room这个变量里存的,不是Rectangle对象本身,而是一个指向该对象在内存中位置的“地址”。self,就是这个地址的别名。它让方法能精准地找到“我是谁”,从而访问“我的数据”。

我们可以用一个生活化类比:self就像快递员手里的运单号。运单号本身不是包裹,但它唯一对应着一个特定的包裹(对象)。area()方法就像一个质检员,他不关心包裹长什么样,他只认运单号(self)。拿到运单号,他就能去仓库(内存)里找到对应的包裹,打开它(访问属性),检查里面的货物(lengthwidth)。

3.2__init__不是“初始化”,而是“实例化”的核心环节

很多人把__init__翻译成“初始化方法”,这容易产生歧义。初始化(Initialization)听起来像是在设置一些默认值。但__init__的真正角色,是实例化(Instantiation)过程中不可或缺的一环

实例化,指的是“从类创建一个具体对象”的全过程。这个过程在Python里分为两步:

  1. __new__方法:这是真正的“造物主”。它负责在内存中开辟一块新空间,创建一个空的对象。__new__object类的静态方法,通常你不需要重写它。
  2. __init__方法:这是“灵魂注入师”。它接收__new__造好的空壳子(self),并往里面填充数据(属性)。

所以,__init__的职责非常明确:给一个刚刚诞生的、空的实例,赋予它初始的状态和身份。它不负责创建对象,只负责塑造对象。

这也是为什么__init__里不能return一个新对象。因为对象已经由__new__造好了,__init__只是在给它“化妆”。你试图return,Python会忽略它(除非你返回None,这是默认行为)。

3.3 属性的本质:字典(dict)里的键值对

在Python底层,每个对象(instance)都维护着一个名为__dict__的特殊字典,用来存储它所有的实例属性。我们可以直接查看它:

my_room = Rectangle(5.2, 4.8) print(my_room.__dict__) # {'length': 5.2, 'width': 4.8}

看到了吗?my_room.length = 5.2,本质上就是在my_room.__dict__这个字典里,增加了一个键'length',值为5.2my_room.width = 4.8,就是再加一个键'width',值为4.8

这个发现非常重要,因为它解释了所有关于属性的“为什么”:

  • 为什么可以动态添加属性?因为__dict__就是一个普通的字典,你可以随时my_room.color = "white",这等价于my_room.__dict__['color'] = "white"
  • 为什么属性查找有顺序?当你写my_room.length时,Python首先在my_room.__dict__里找'length'这个键;找不到,再去Rectangle类的__dict__里找;还找不到,就向上搜索父类(如果有继承的话)。
  • 为什么类属性和实例属性会混淆?类属性是定义在class块里,但不在__init__里的变量,它属于类本身(Rectangle.__dict__),所有实例共享。而实例属性是定义在__init__里,用self.xxx赋值的,它属于每个实例自己的__dict__。这是OOP里最容易出错的地方之一。

我们来演示这个关键区别:

class Rectangle: # 这是一个类属性,所有Rectangle实例共享 unit = "meters" def __init__(self, length, width): # 这些是实例属性,每个对象独有一份 self.length = length self.width = width # 创建两个实例 r1 = Rectangle(1, 2) r2 = Rectangle(3, 4) print(r1.unit) # meters print(r2.unit) # meters print(r1.__dict__) # {'length': 1, 'width': 2} print(r2.__dict__) # {'length': 3, 'width': 4} print(Rectangle.__dict__['unit']) # meters

现在,如果我修改r1.unit

r1.unit = "feet" # 这行代码做了什么? print(r1.unit) # feet print(r2.unit) # meters print(r1.__dict__) # {'length': 1, 'width': 2, 'unit': 'feet'}

神奇的事情发生了!r1.unit变成了"feet",但r2.unit还是"meters"。这是因为r1.unit = "feet"这行代码,并没有修改类属性Rectangle.unit,而是在r1自己的__dict__里,新增了一个键'unit',值为"feet"。下次r1.unit,Python在r1.__dict__里就找到了,就不会再去Rectangle.__dict__里找了。而r2__dict__里没有'unit',所以它还是会去类里找,得到"meters"

注意事项:这个特性既是Python的灵活之处,也是坑的来源。如果你本意是想修改所有Rectangle的单位,你应该写Rectangle.unit = "feet"。如果你想给某个实例设置独特的单位,那就用r1.unit = "feet"。务必分清你想操作的是“类”还是“实例”。

3.4__str____repr__:让对象“开口说话”,告别丑陋的内存地址

还记得我们之前打印my_room时,看到的是<__main__.Rectangle object at 0x7f8b1c0a1d90>吗?这对调试毫无帮助。我们希望对象能“自我介绍”,比如打印出来是Rectangle(5.2, 4.8)。这就需要__str____repr__这两个魔法方法。

  • __repr__:面向开发者,目标是“无歧义”和“可复现”。理想情况下,eval(repr(obj))应该能重新创建出这个对象。它应该尽可能详细、准确。
  • __str__:面向用户,目标是“可读性”。它应该简洁、友好,告诉用户这个对象“是什么”。

我们来给Rectangle加上它们:

class Rectangle: def __init__(self, length, width): self.length = length self.width = width def area(self): return self.length * self.width def __repr__(self): return f"Rectangle({self.length}, {self.width})" def __str__(self): return f"A rectangle with length {self.length} and width {self.width}"

现在试试:

r = Rectangle(5.2, 4.8) print(r) # A rectangle with length 5.2 and width 4.8 (调用了 __str__) print(repr(r)) # Rectangle(5.2, 4.8) (调用了 __repr__)

print()函数默认调用__str__,而你在交互式环境(如IPython)里直接输入r,它会调用__repr__。这是Python的约定。

实操心得:我习惯在开发时,先写好__repr__,因为它对调试至关重要。一个清晰的__repr__,能让你一眼看出对象的关键状态,省去大量print(obj.length, obj.width)的功夫。__str__则可以根据项目需要决定是否编写,比如在做一个面向用户的命令行工具时,它就很有价值。

4. 完整实操:从零构建一个可运行的Rectangle类,并扩展其能力

4.1 基础版本:一个能算面积和周长的矩形

让我们把前面学到的所有知识,整合成一个完整的、可直接运行的Rectangle类。我们将包含:

  • __init__:接收长和宽,设置实例属性。
  • area():计算面积。
  • perimeter():计算周长。
  • __repr__:提供清晰的调试信息。
class Rectangle: """A simple Rectangle class to demonstrate OOP concepts.""" def __init__(self, length, width): """Initialize a new Rectangle instance. Args: length (float): The length of the rectangle. width (float): The width of the rectangle. """ self.length = length self.width = width def area(self): """Calculate and return the area of the rectangle.""" return self.length * self.width def perimeter(self): """Calculate and return the perimeter of the rectangle.""" return 2 * (self.length + self.width) def __repr__(self): """Return a string representation for debugging.""" return f"Rectangle({self.length}, {self.width})"

测试它:

# 创建几个矩形 living_room = Rectangle(6.5, 4.2) kitchen = Rectangle(3.8, 3.0) bedroom = Rectangle(4.5, 4.0) # 使用它们 print(living_room) # Rectangle(6.5, 4.2) print(f"Living room area: {living_room.area():.2f} sq.m") # Living room area: 27.30 sq.m print(f"Kitchen perimeter: {kitchen.perimeter():.1f} m") # Kitchen perimeter: 13.6 m print(f"Bedroom is square? {bedroom.length == bedroom.width}") # Bedroom is square? True

这个版本已经非常实用。它把数据(长、宽)和行为(算面积、算周长)完美地捆绑在了一起。living_roomkitchenbedroom不再是三个孤立的数字对,而是三个有身份、有能力的“矩形公民”。

4.2 进阶版本:添加数据验证与默认值

现实世界中,长度和宽度不能是负数或零。我们的Rectangle也应该有基本的“健康检查”。同时,有时我们可能只想指定一个边长,希望它默认是个正方形。我们可以利用__init__的参数默认值和条件判断来实现。

class Rectangle: """A Rectangle class with data validation and optional square creation.""" def __init__(self, length, width=None): """Initialize a new Rectangle instance. If width is not provided, it defaults to length, creating a square. Args: length (float): The length of the rectangle. Must be > 0. width (float, optional): The width of the rectangle. Must be > 0. Defaults to None, which makes it equal to length. Raises: ValueError: If length or width is not positive. """ # 数据验证:长度必须为正数 if length <= 0: raise ValueError("Length must be a positive number.") self.length = length # 如果没提供宽度,则默认为正方形 if width is None: self.width = length else: if width <= 0: raise ValueError("Width must be a positive number.") self.width = width def area(self): return self.length * self.width def perimeter(self): return 2 * (self.length + self.width) def is_square(self): """Check if this rectangle is a perfect square.""" return self.length == self.width def __repr__(self): if self.is_square(): return f"Square({self.length})" else: return f"Rectangle({self.length}, {self.width})" def __str__(self): shape = "Square" if self.is_square() else "Rectangle" return f"{shape} with length {self.length} and width {self.width}"

现在,我们可以这样创建对象:

# 创建一个正方形(只提供一个参数) my_square = Rectangle(5.0) print(my_square) # Square with length 5.0 and width 5.0 # 创建一个普通矩形 my_rect = Rectangle(4.0, 2.5) print(my_rect) # Rectangle with length 4.0 and width 2.5 # 尝试创建一个非法的矩形(会抛出异常) # bad_rect = Rectangle(-1.0, 2.0) # ValueError: Length must be a positive number.

这个版本展示了OOP的另一个强大之处:封装了业务规则。数据验证的逻辑(length > 0)被牢牢地封在了__init__方法里。任何外部代码,只要想创建一个Rectangle,就必须遵守这个规则。你不需要在每个调用点都写一遍if length <= 0: raise ...,规则只写一次,处处生效。

4.3 高级版本:支持运算符重载,让矩形“可比较”、“可相加”

OOP的终极魅力,在于让自定义对象的行为,和内置对象(如int,str)一样自然。Python允许我们通过重载“魔法方法”,来改变对象的运算符行为。

我们来为Rectangle添加:

  • ==:两个矩形相等,当且仅当它们的长和宽分别相等(顺序无关,即Rectangle(2,3)等于Rectangle(3,2))。
  • +:两个矩形相加,返回一个新矩形,其长和宽分别是原矩形长宽的和。
class Rectangle: """An advanced Rectangle class with operator overloading.""" def __init__(self, length, width=None): if length <= 0: raise ValueError("Length must be a positive number.") self.length = length if width is None: self.width = length else: if width <= 0: raise ValueError("Width must be a positive number.") self.width = width def area(self): return self.length * self.width def perimeter(self): return 2 * (self.length + self.width) def is_square(self): return self.length == self.width def __repr__(self): if self.is_square(): return f"Square({self.length})" else: return f"Rectangle({self.length}, {self.width})" def __str__(self): shape = "Square" if self.is_square() else "Rectangle" return f"{shape} with length {self.length} and width {self.width}" def __eq__(self, other): """Define equality: two rectangles are equal if they have the same dimensions, regardless of order (i.e., 2x3 == 3x2).""" if not isinstance(other, Rectangle): return NotImplemented # Check both orientations return ((self.length == other.length and self.width == other.width) or (self.length == other.width and self.width == other.length)) def __add__(self, other): """Define addition: create a new rectangle whose sides are the sums of the originals.""" if not isinstance(other, Rectangle): return NotImplemented new_length = self.length + other.length new_width = self.width + other.width return Rectangle(new_length, new_width) def __lt__(self, other): """Define less-than: compare by area.""" if not isinstance(other, Rectangle): return NotImplemented return self.area() < other.area()

测试这些新能力:

r1 = Rectangle(2, 3) r2 = Rectangle(3, 2) r3 = Rectangle(1, 1) print(r1 == r2) # True (因为2x3和3x2是同一个矩形) print(r1 == r3) # False r4 = r1 + r2 # (2+3) x (3+2) = 5x5 print(r4) # Square(5.0) # 现在可以排序了! rectangles = [Rectangle(1, 4), Rectangle(2, 2), Rectangle(3, 1)] rectangles.sort() # 按面积从小到大排序 print([str(r) for r in rectangles]) # ['Rectangle with length 1 and width 4', 'Square with length 2 and width 2', 'Rectangle with length 3 and width 1']

__eq____add____lt__这些方法,让Rectangle对象拥有了和intstr一样的“人格”。你可以用==比较它们,用+组合它们,用sorted()给它们排序。这不再是“模拟”现实,而是让代码本身,就成为现实世界的一个精确映射。

常见问题排查:我在第一次实现__eq__时,犯了一个经典错误:写了return self.length == other.length and self.width == other.width,但没考虑other可能不是Rectangle类型。结果,当r1 == "hello"时,程序直接崩溃。正确的做法是,先用isinstance(other, Rectangle)检查类型,如果不是,就返回NotImplemented。Python看到NotImplemented,会尝试调用other.__eq__(self)(如果other有这个方法的话),如果other也没有,最后才返回False。这是一个安全的、符合Python协议的写法。

5. 初学者必踩的10个坑与独家避坑指南

5.1 坑1:self缺失或错位——最频繁的SyntaxError

现象TypeError: method_name() takes 0 positional arguments but 1 was given

原因:在类的方法定义里,忘记了第一个参数self

避坑指南

  • 养成肌肉记忆:在PyCharm或VS Code里,输入def后,IDE通常会自动补全def method_name(self):。不要删掉self
  • 如果你确实需要一个不依赖实例状态的“工具函数”,把它定义在类外面,或者用@staticmethod装饰器。

5.2 坑2:在__init__里忘记用self.——属性“消失”了

现象AttributeError: 'Rectangle' object has no attribute 'length'

原因:在__init__里写了length = length,而不是self.length = length。这行代码只是创建了一个局部变量length,函数执行完就消失了,对象本身并没有获得这个属性。

避坑指南

  • __init__想象成一个“贴标签”的过程。所有你想贴在对象身上的标签,都必须以self.xxx = ...的形式书写。
  • __init__末尾,加一行print(self.__dict__),运行一下,立刻就能看到你贴上去的标签是不是真的存在。

5.3 坑3:类属性 vs 实例属性混淆——“蝴蝶效应”式Bug

现象:修改了一个实例的某个属性,结果其他所有实例的同名属性也跟着变了。

原因:误把应该定义在__init__里的实例属性,定义成了类属性(写在class块里,但不在任何方法内)。

避坑指南

  • 黄金法则:所有需要每个实例都有一份独立副本的变量,都必须在__init__里,用self.xxx来定义。
  • 类属性只用于那些所有实例都共享的、不变的常量,比如Rectangle.PI = 3.14159,或者一个计数器Rectangle.count = 0(但修改它时,必须用Rectangle.count += 1,而不是self.count += 1)。

5.4 坑4:__init__里调用未定义的方法——“鸡生蛋”问题

现象NameError: name 'some_method' is not defined

原因:在__init__里,调用了另一个方法,但这个方法的定义写在了__init__之后。

避坑指南

  • Python是自上而下执行的。虽然类定义是一个整体,但方法的定义顺序仍然重要。把__init__放在最上面,然后是其他常用方法,最后是__str____repr__等魔法方法。
  • 更好的实践是:在__init__里,只做最基础的属性赋值。复杂的初始化逻辑,可以封装成一个私有方法(如_validate_inputs()),并在__init__里调用它。

5.5 坑5:return__init__里——“画蛇添足”

现象:代码能跑,但对象创建后,某些属性是None,或者程序行为诡异。

原因:在__init__里写了return some_value。这会提前结束__init__,导致后面的属性赋值没有执行。

避坑指南

  • 牢记:__init__的唯一使命是“初始化”,它不应该有返回值。把它当作一个“void”函数。如果你需要返回一个值,那应该是一个工厂函数,而不是__init__

5.6 坑6:字符串格式化时,误用self——“张冠李戴”

现象f"Area: {self.area}"打印出来是<bound method Rectangle.area of <__main__.Rectangle object at 0x...>>

原因:漏掉了括号()self.area是方法对象本身,self.area()才是调用方法得到的结果。

避坑指南

  • 在f-string或format()里,凡是想显示计算结果的地方,一定要确认后面跟了()
  • 开启IDE的语法高亮,方法名通常是蓝色的,而方法

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询