Java程序员大战Python面向对象

0 阅读13分钟

作为一名使用Java语言多年的程序员,我早以为自己搞懂了面向对象开发OOP,当我接触Python的时候,没想到它玩的是这么花,这篇文章记录哪些让我这个Java程序员大开眼界的特性。

92de914234a02f556453edb1aaf58741.jpg

对于较小的程序而言,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

PythonJava说明
typejava.lang.Class描述"类"的元类/类对象
class Personclass Person定义的类
Person 的类型是 typePerson.class 的类型是 Class类的运行时表示
type(Person)Person.class.getClass()获取类的元信息

>>> import datetime
>>> datetime.date(2026,4,29) # 构造函数
datetime.date(2026, 4, 29)

self

  1. 类的所有方法都有self作为第一个参数
  2. 在阅读代码的时候,第一个参数是否为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开发者来说非常容易搞混

PythonJava说明
对象特性 (Instance Attribute)实例字段 (Instance Field)属于具体对象的属性
类特性 (Class Attribute)静态字段 (Static Field)属于类本身的属性
属性 (Property)Getter/Setter + 私有字段控制属性访问的机制

对象特性

  1. 对象特性(attribute)是与对象相关联
  2. 在Python中没有像Java一样有private关键字,在Python中是没有私有特性和私有方法的这种强约束的。

Python的惯例是为私有特性或者方法的名字使用下划线前缀,这是Python社区的一种规范形式,君子协定,但并不能解决类外的代码进行访问和修改。

下面的代码来说明从外部来修改一个用户的账户余额,在账簿中金额一下变大。

image.png

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的设计哲学】:

  1. "我们都是成年人了" (We are all consenting adults)
  2. 不强制限制访问,而是通过约定(_前缀)告知"这是内部实现细节"

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

image.png


注意:但是属性并没有阻止我们直接更改背幕后变量

image.png

只读属性

只提供getter方法

class Person:
    def __init__(self):
        self._age = 18 # 幕后变量

    @property
    def age(self):
        print("准备获取年龄")
        return self._age

    # @age.setter @age.deleter

小结

Python的property不是为了阻止你访问_age,而是为了:

  1. 提供更好的设计模式 - 让你以属性的方式使用,以方法的方式实现
  2. 保证接口的向后兼容性 - 这是Java等语言无法比拟的优势
  3. 通过约定而非强制来引导正确的使用方式

__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__

【Python OOP 井字棋游戏】

__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的程序员来说,真是一个非常奇怪的东西:

  1. 它没有Java的extends关键字
  2. 其次它能继承多个类
  3. 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
"""

image.png

关于__init__

  1. Python 不会自动调用父类的 __init__ 方法
  2. super显示调用__init__

image.png

【tip】 如果子类写了__init__这属于方法重写,再次证明了这不是构造方法。

  1. 如果子类写了__init__,需要手动调用父类的初始化
  2. 如果子类没有重写,默认使用父类的__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

没有selfcls参数,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()实际工作的原理

来看下面的例子: 我们分别创建了SonDaughter来多继承FatherMother,但是注意:

  1. 这里的继承顺序是不一样的,所以最终的输出也不一样。
  2. SonDaughter没有重写__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有序列表是不一样,这主要和继承时和类声明顺序有关。

image.png

【问题2】以Son为例,为什么调用Father之后,Father中的__init__方法中写了super().__init__(),Mother也会被调用呢?Father并没有继承Mother啊?

mro()方法返回的有序列表,我们可以看到,当python要调用一个方法时,会一直顺着这个有序列表进行查找,直到最后一项是内置object类(因为他是Python中所有类的父类)。

Son中,父类Father的__init__方法中写了super().__init__,此时super代表的是MRO有序列表的下一个类,对应的就是Mother。

【重要结论】

super它并不总是指向父类,更严格的来说它执行MRO有序列表中的下一个类


这里有一个实践案例 【MRO super链分析-井字棋游戏,给简约棋盘添加提示的功能就使用了MRO技术】

image.png

小结

【古法编程: Python OOP 井字棋游戏】

@dataclass

熟悉Java的朋友一定接触过Lombok的@Data,这个@dataclass基本功能差不多,只不过这个是python内置的模块的,不像Java中的lombok是第三方库的。

@dataclasses,能够自动为用户生成__init__,__repr__,__eq__等方法,换而言之能够干掉很多样板代码。

初始

观察下面的代码:

  1. 首先这些都是对象特征attribute,尽管定义在了类里面,但是它们却不是类特征attribute.(注意,只要加了类型的才会被@dataclass处理)
  2. 类特征有另外的写法。
@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