作为一名使用Java语言多年的程序员,我早以为自己搞懂了面向对象开发OOP,当我接触Python的时候,没想到它玩的是这么花,这篇文章记录哪些让我这个Java程序员大开眼界的特性。
对于较小的程序而言,OOP并没有增加调理性,反而是繁文缛节。在Java中代码的组织必须以类的形式存在,而在Python中,OOP特性是可选的,程序员在需要时使用类,不需要时则可以忽略它们。
当你需要将数据和代码组织在一起的时候,或者需要新的数据类型,就可以使用OOP的思想了。
类的命名
按照惯例,模块名是小写,而类名应该是驼峰的形式。但是Python标准库中很多内置类都并未遵循这一惯例。
可以看到datetime是模块,date不是函数也不是方法,而是类型
>>> type(datetime)
<class 'module'>
>>> type(datetime.date)
<class 'type'>
>>> class Person:
... pass
...
>>> type(Person)
<class 'type'>
<class 'type'>type元类说明,对比Java
| Python | Java | 说明 |
|---|---|---|
type | java.lang.Class | 描述"类"的元类/类对象 |
class Person | class Person | 定义的类 |
Person 的类型是 type | Person.class 的类型是 Class | 类的运行时表示 |
type(Person) | Person.class.getClass() | 获取类的元信息 |
>>> import datetime
>>> datetime.date(2026,4,29) # 构造函数
datetime.date(2026, 4, 29)
self
- 类的所有方法都有
self作为第一个参数 - 在阅读代码的时候,第一个参数是否为
self是区分方法和函数最快的方式。(当然接下来你还会看到一个以cls作为第一个参数的方法)
class WizCoin:
"""一种虚构的魔法货币
基于《哈利·波特》系列的货币系统:
- Galleons(加隆):最高面值,金币
- Sickles(西可):中等面值,银币
- Knuts(纳特):最小面值,铜币
兑换关系:
1 加隆 = 17 西可
1 西可 = 29 纳特
1 加隆 = 493 纳特
"""
def __init__(self, galleons, sickles, knuts):
"""初始化 WizCoin 实例
Args:
galleons (int): 加隆数量(金币)
sickles (int): 西可数量(银币)
knuts (int): 纳特数量(铜币)
"""
self.galleons = galleons # 加隆(最高面值,金币)
self.sickles = sickles # 西可(中等面值,银币)
self.knuts = knuts # 纳特(最小面值,铜币)
__init__初始化方法
注意这是初始化方法并不是像Java一样的构造方法,这在继承的时候体现的很明显,而且只能有一个__init__方法,我们在实例化一个对象的时候,python会自动调用这个魔术方法
创造一个对象,注意不像Java一样,没有new
coin = WizCoin(10, 5, 20)
print(coin.galleons) # 输出: 10
等同于这样写
coin2 = WizCoin.__new__(WizCoin)
WizCoin.__init__(coin2, 10, 5, 20)
print(coin2.galleons) # 输出: 1
特性(attribute)与属性(properties)
Python中的这几个概念对于Java开发者来说非常容易搞混
| Python | Java | 说明 |
|---|---|---|
| 对象特性 (Instance Attribute) | 实例字段 (Instance Field) | 属于具体对象的属性 |
| 类特性 (Class Attribute) | 静态字段 (Static Field) | 属于类本身的属性 |
| 属性 (Property) | Getter/Setter + 私有字段 | 控制属性访问的机制 |
对象特性
- 对象特性(attribute)是与对象相关联的
- 在Python中没有像Java一样有private关键字,在Python中是没有私有特性和私有方法的这种强约束的。
Python的惯例是为私有特性或者方法的名字使用下划线前缀,这是Python社区的一种规范形式,君子协定,但并不能解决类外的代码进行访问和修改。
下面的代码来说明从外部来修改一个用户的账户余额,在账簿中金额一下变大。
import datetime as dt
class BankAccount:
def __init__(self, account_holder):
"""初始化 BankAccount 实例
Args:
account_holder (str): 账户持有人
"""
self._balance = 0
self._name = account_holder
with open(self._name + '_ledger.txt', 'w', encoding='utf-8') as ledger_file:
ledger_file.write(f'{dt.datetime.now()} 开户余额:0\n')
ledger_file.write('-' * 20 + '\n')
def deposit(self, amount):
"""存款
Args:
amount (int): 存款金额
"""
if amount < 0: return
self._balance += amount
with open(self._name + '_ledger.txt', 'a', encoding='utf-8') as ledger_file:
ledger_file.write(f'{dt.datetime.now()} 存款:{amount}\n')
ledger_file.write(f' 账户余额:{self._balance}\n')
ledger_file.write('-' * 20 + '\n')
def withdraw(self, amount):
"""取款
Args:
amount (int): 取款金额
"""
if amount < 0 or amount > self._balance: return
self._balance -= amount
with open(self._name + '_ledger.txt', 'a', encoding='utf-8') as ledger_file:
ledger_file.write(f'{dt.datetime.now()} 取款:{amount}\n')
ledger_file.write(f'账户余额:{self._balance}\n')
ledger_file.write('-' * 20 + '\n')
if __name__ == '__main__':
pkmer = BankAccount('pkmer')
类特性
像Java的静态字段,从属于类
class CreateCounter:
count = 0 # 类特性(class attribute)
def __init__(self):
CreateCounter.count += 1
self.id = CreateCounter.count
def __str__(self):
return f"id: {self.id}"
很明显有一个作用域范围。
>>> from cls_attr import *
>>> print(CreateCounter())
id: 1
>>> print(CreateCounter())
id: 2
>>> print(CreateCounter())
id: 3
同样的,python不能限制外界修改访问类特性。我们可以进行更改
>>> CreateCounter.count = 100
>>> print(CreateCounter())
id: 101
>>> print(CreateCounter())
id: 102
>>> print(CreateCounter())
id: 103
属性properties
Python OOP相对Java这样的语言,有独有的特性,比如属性就是这样的一种独特的特性
特性(attribute)在python社区中经常在变量前面加上_短横线这种惯例,来告诉其他开发者这是一个私有属性,但是从技术上来讲,我们还是很容易从外界直接修改它,python并没有像Java一样有private等权限属性。它只提供约定层面的保护,而非强制性的访问控制
【Python的设计哲学】:
- "我们都是成年人了" (We are all consenting adults)
- 不强制限制访问,而是通过约定
(_前缀)告知"这是内部实现细节"
Python中用属性来防止私有特性被意外地修改为无效状态。
【属性与特性的关系】 在python中属性是指getter方法,setter方法和deleter方法的特性
将方法变成了属性一样的访问
class Person:
def __init__(self):
self._age = 18 # 幕后变量
@property
def age(self):
print("准备获取年龄")
return self._age
@age.setter
def age(self,value):
if not isinstance(value,int):
raise TypeError("年龄必须是整数")
if value < 0 or value > 120:
raise ValueError("年龄必须在0-120之间")
self._age = value
@age.deleter
def age(self):
del self._age
注意:但是属性并没有阻止我们直接更改背幕后变量
只读属性
只提供getter方法
class Person:
def __init__(self):
self._age = 18 # 幕后变量
@property
def age(self):
print("准备获取年龄")
return self._age
# @age.setter @age.deleter
小结
Python的property不是为了阻止你访问_age,而是为了:
- 提供更好的设计模式 - 让你以属性的方式使用,以方法的方式实现
- 保证接口的向后兼容性 - 这是Java等语言无法比拟的优势
- 通过约定而非强制来引导正确的使用方式
__dict__
__dict__是一个字典,存储了对象的可写属性。几乎所有Python对象都有__dict__属性(除非定义了__slots__)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p = Person("Pkmer", 18)
# 这两种写法等价
print(p.__dict__) # {'name': 'Pkmer', 'age': 18}
print(vars(p)) # {'name': 'Pkmer', 'age': 18} 推荐用法
__repr__与__str__
__str__这个魔术方法好理解对应的就是Java类的toString方法,由python的内置函数str()调用,主要用于给用户显示
>>> from tictactoe_oop import TTTBoard
>>> print(TTTBoard())
| | 1 2 3
| | 4 5 6
| | 7 8 9
__repr__ 由python的内置函数repr(),常用于技术环境中,比如错误信息和日志。在shell环境中输入对象,就会得到repr字符串
>>> TTTBoard()
<tictactoe_oop.TTTBoard object at 0x00000248804F2990>
如果不做处理,默认会显示尖括号包裹,包含对象内存地址和类名的字符串。
在类中可以巧妙的处理:__repr__ = __str__复用__str__
# 井字棋棋盘字典key
ALL_SPACES = [str(i) for i in range(1, 10)]
# 字符串常量
X,O,BLANK = 'X','O',' '
class TTTBoard:
def __init__(self):
"""初始化棋盘"""
self._spaces = { space:BLANK for space in ALL_SPACES}
def __str__(self):
"""返回棋盘字符串"""
return f"""
{self._spaces['1']} | {self._spaces['2']} | {self._spaces['3']} 1 2 3
{self._spaces['4']} | {self._spaces['5']} | {self._spaces['6']} 4 5 6
{self._spaces['7']} | {self._spaces['8']} | {self._spaces['9']} 7 8 9
"""
__repr__ = __str__
此时输出,在shell中再输入对象,就好看多了
>>> TTTBoard()
| | 1 2 3
| | 4 5 6
| | 7 8 9
注意,python的语法糖又来了
__repr__ = __str__
# 等价写法,
def __repr__(self):
return self.__str__()
继承
Python的继承,对于熟悉Java的程序员来说,真是一个非常奇怪的东西:
- 它没有Java的
extends关键字 - 其次它能继承多个类
- super的使用方式也不同
super的用法
【tip】 允许调用魔术方法,例如:
super().__init__(),super().__str__()Java程序员注意:
super不能简单理解为就是父类,而是MRO中的下一个类,因为Python是允许继承多个父类的。不理解没关系,在后面的多继承下的MRO会详细讲解它实际工作的原理
class Parent:
def __init__(self,name):
print("Parent")
self.name = name
def hello(self):
print("Parent: hello")
class Child(Parent):
def __init__(self,name):
print("Child")
super().__init__(name) # 不像Java需要放在第一行,需要显示调用父类的__init__方法
def hello(self): # 重写父类方法
super().hello() # 调用父类方法
print(f"Child: hello {self.name}") # 能够访问父类特征(attribute)
Child("Pkmer").hello()
"""输出
Child
Parent
Parent: hello
Child: hello Pkmer
"""
关于__init__
- Python 不会自动调用父类的
__init__方法- super显示调用
__init__
【tip】 如果子类写了
__init__这属于方法重写,再次证明了这不是构造方法。
- 如果子类写了
__init__,需要手动调用父类的初始化- 如果子类没有重写,默认使用父类的
__init__
class Parent:
def __init__(self,name):
print("Parent")
self.name = name
def hello(self):
print(f"Parent: hello {self.name}")
class Child(Parent):
"""使用父类的__init__"""
pass
Child("Pkmer").hello()
"""输出
Parent
Parent: hello Pkmer
"""
方法重写
子类重写的方法与父类的方法具有相同的名称,用来覆盖从父类继承来的方法,以达到继承父类的绝大部分功能,从而实现定制化的功能。
注意: 在Python中,一个类是没有像Java一样方法重载的,同样的方法名,后面的会覆盖前面的。
【TIP-1】 就好像微调预训练模型一样,预训练模型需要花费大量的资金和时间,绝大部分个人和机构都能基本实现不了,但是个人或者机构可以通过微调预训练大模型,即继承了预训练模型的通用知识,又垂直自己应用的领域。
【TIP-2】
__init__子类也属于重写父类的__init__,它不是构造方法
isinstance
判断对象是否属于给定类,或者给定类的子类
【tip】
isinstance(b,(int,str,bool,A))第二个参数可以是元组,只要符合一个就行。正常写是isinstance(b,A)或isinstance(b,B)
>>> class A:
... pass
...
>>> class B(A):
... pass
...
>>> b = B()
>>> isinstance(b,(int,str,bool,A))
True
类方法@classmethod
Python中的类方法与Java中的静态方法不是一样的东西,Java根本就没有这个概念。只不过Python中的这个类方法使用cls作为参数。
它也不是对应Java的静态方法,因为Python中有@staticmethod静态方法
class ExampleClass:
def example_regular_method(self):
"""常规方法与对象绑定
"""
print("常规方法")
@classmethod
def example_class_method(cls):
"""类方法与类绑定
"""
print("类方法")
# <class '__main__.ExampleClass'> <class 'type'> <class '__main__.ExampleClass'>
print(f"{cls} {ExampleClass.__class__} {ExampleClass().__class__}") # True
【调用方式】可以直接通过类调用,无须像普通方法一样进行实例化才能调用
# 类直接调用
ExampleClass.example_class_method()
# 对象也可以调用
obj = ExampleClass()
obj.example_class_method()
# 通过__class__
obj.__class__.example_class_method()
工厂方法
由于一个类只允许有一个__init__方法,如果初始化既要接收字符串也可以接收文件名。那么此时可以用类方法来做工厂方法
class AsciiArt:
def __init__(self, characters):
self._characters = characters
def __str__(self):
return self._characters
@classmethod
def from_file(cls, file_path):
with open(file_path) as f:
return cls(f.read())
if __name__ == '__main__':
ascii_art = AsciiArt.from_file("./face.txt")
print(ascii_art)
输出:
_______
| . . |
| ___/ |
|_______|
静态方法@staticmethod
没有self和cls参数,python中的静态方法完全是Python模仿了其他语言的特性,比如Java。在灵活的Python中,完全可以用常规函数代替
class StaticMethodDemo:
@staticmethod
def static_method():
print("静态方法")
if __name__ == '__main__':
StaticMethodDemo.static_method()
多重继承
什么可以继承多个父类?Javaer真实垫付认知。没错,在Python中是允许继承多个类的。
mixin
只要父类的方法名称不重复,多重继承是非常简单的。被继承的类被称为mixin。
class AirplaneMixin:
"""飞机混入类"""
def fly_in_the_air(self):
print("飞...")
class ShipMixin:
"""船混入类"""
def sail_on_the_sea(self):
print("游...")
class FlyingBoat(AirplaneMixin, ShipMixin):
pass
>>> from mixin import FlyingBoat
>>> sea_duck = FlyingBoat()
>>> sea_duck.fly_in_the_air()
飞...
>>> sea_duck.sail_on_the_sea()
游...
>>>
MRO
mixin很简单,因为父类的方法都不重复,但是如果重复了,那么该调用哪个父类的方法?比如继承的父类都有__init___这个方法,此时会调用谁呢?
我们需要了解MRO(method resolution order)方法解析顺序和Super()实际工作的原理
来看下面的例子: 我们分别创建了Son和Daughter来多继承Father和Mother,但是注意:
- 这里的继承顺序是不一样的,所以最终的输出也不一样。
Son和Daughter没有重写__init__,默认会使用父类的__init__
class Father:
def __init__(self):
print("Father")
super().__init__()
class Mother:
def __init__(self):
print("Mother")
super().__init__()
class Son(Father,Mother):
pass
class Daughter(Mother,Father):
pass
if __name__ == '__main__':
Son() # Father Mother
Daughter() # Mother Father
针对上面的输出,现在必须要回答下面这两个问题,弄清楚这两个问题基本上就理解了什么MRO。
【问题1】 为什么
Son()实例化和Daughter()实例化,调用父类的顺序不同呢?
MRO是python在继承方法或者在方法中调用super()函数时查找类的有序列表,通过mro方法可以查看这个有序列表。
>>> Son.mro()
[<class 'mro_example.Son'>, <class 'mro_example.Father'>, <class 'mro_example.Mother'>, <class 'object'>]
>>> Daughter.mro()
[<class 'mro_example.Daughter'>, <class 'mro_example.Mother'>, <class 'mro_example.Father'>, <class 'object'>]
可以看到Son和Daught的mro有序列表是不一样,这主要和继承时和类声明顺序有关。
【问题2】以
Son为例,为什么调用Father之后,Father中的__init__方法中写了super().__init__(),Mother也会被调用呢?Father并没有继承Mother啊?
从mro()方法返回的有序列表,我们可以看到,当python要调用一个方法时,会一直顺着这个有序列表进行查找,直到最后一项是内置object类(因为他是Python中所有类的父类)。
在Son中,父类Father的__init__方法中写了super().__init__,此时super代表的是MRO有序列表的下一个类,对应的就是Mother。
【重要结论】
super它并不总是指向父类,更严格的来说它执行MRO有序列表中的下一个类
小结
@dataclass
熟悉Java的朋友一定接触过Lombok的@Data,这个@dataclass基本功能差不多,只不过这个是python内置的模块的,不像Java中的lombok是第三方库的。
@dataclasses,能够自动为用户生成__init__,__repr__,__eq__等方法,换而言之能够干掉很多样板代码。
初始
观察下面的代码:
- 首先这些都是对象特征attribute,尽管定义在了类里面,但是它们却不是类特征attribute.(注意,只要加了类型的才会被@dataclass处理)
- 类特征有另外的写法。
@dataclass
class User:
# 对象特性
name: str
age: int = 18
# 类特性 共享的
email = "pkmer@py.cn"
直接创建实例
>>> User("Pkmer")
User(name='Pkmer', age=18)
查看__init__的签名
>>> inspect.signature(User)
<Signature (name: str, age: int = 18) -> None>
email才是类特性,全部实例共享,而age却不是
查看实例的fields