古法编程: 面向Java的Python风格代码

0 阅读12分钟

作为一名Java程序员,接下来从我个人的经验和认知中,从语言特性方面总结Python中比较特别的地方。方便能够更好的进行开发实践,成为一名合格的Python程序员。

eb3ad12128cb34304df27b01915467e1_720.jpg

可读性设计哲学

Python中倾向英文单词而非符号

  1. not 比 ! 更接近自然语言
  2. 类似的设计:and / or 代替 && / ||

Java中

boolean save = true;
if (!save) {
    System.out.println("not saved");
}

Python中

save = True
if not save:  # ✅ 正确
    print("not saved")
    
# ❌ 错误写法(语法错误)
if !is_save:
    print("error")

但是不等于!=仍然与Java是一样的。

字典

  1. 字典的方法copy是浅拷贝

列表表达式一样的推导拷贝

# _spaces是dict
s = {
    space: '.' if val == BLANK else val
    for space, val in self._spaces.items()
}

使用元组解包

使用元组解包,告别 temp 变量

a = 2
b = 6
a,b = b,a   # 直接进行交换,此时a=6,b=2

# 不同类型
c = "Hello"
a,c = c,a  # 此时a="Hello",c=6

a,b = b,a 这段代码虽然没有用()包裹起来,但是默认是一种元组。

在shell中输入,可以看到输出的是(2, 6)

>>> a = 2
>>> b = 6
>>> a,b  # 这样写
(2, 6)   # 输出的是元组

冒泡排序

import random


def bubble_sort(data):
    for i in range(len(data) - 1, 0, -1):
        for j in range(i):
            if data[j] > data[j + 1]:
                # 交换彼此的值
                data[j], data[j + 1] = data[j + 1], data[j]


original = [random.randint(1, 100) for _ in range(10)]
print(f"original = {original}")
bubble_sort(original)
print(f"sorted   = {original}")

"""输出
original = [31, 3, 88, 65, 72, 38, 43, 62, 67, 25]
sorted   = [3, 25, 31, 38, 43, 62, 65, 67, 72, 88]
"""

迭代器

常常把迭代器包装在一个list()构造函数中,这是因为list()传递一个迭代器会把它耗尽

>>> range(7)
range(0, 7)
>>> list(range(7))
[0, 1, 2, 3, 4, 5, 6]

enumerate

能够返回迭代的下标,并且能够自定义起始的下标,默认是0

也可以指定起始下标

>>> from string import ascii_lowercase
>>> ascii_lowercase
'abcdefghijklmnopqrstuvwxyz'
>>> [(k,v) for k,v in enumerate(ascii_lowercase)]
[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e'), (5, 'f'), (6, 'g'), (7, 'h'), (8, 'i'), (9, 'j'), (10, 'k'), (11, 'l'), (12, 'm'), (13, 'n'), (14, 'o'), (15, 'p'), (16, 'q'), (17, 'r'), (18, 's'), (19, 't'), (20, 'u'), (21, 'v'), (22, 'w'), (23, 'x'), (24, 'y'), (25, 'z')]
>>> [(k,v) for k,v in enumerate(ascii_lowercase,10)]
[(10, 'a'), (11, 'b'), (12, 'c'), (13, 'd'), (14, 'e'), (15, 'f'), (16, 'g'), (17, 'h'), (18, 'i'), (19, 'j'), (20, 'k'), (21, 'l'), (22, 'm'), (23, 'n'), (24, 'o'), (25, 'p'), (26, 'q'), (27, 'r'), (28, 's'), (29, 't'), (30, 'u'), (31, 'v'), (32, 'w'), (33, 'x'), (34, 'y'), (35, 'z')]

关于函数

属性

没错函数有一些固有的属性,也叫做函数的元数据

  1. getattr内置函数,能够获取对象的属性。getattr(obj,attribute)等效于obj.attribute
  2. __doc__这个属性,能够获取函数的注释,这非常有用,可以在定义Agent的Tool的描述。
def multiplication(a, b=1):
    """Return a multiplied by b. """
    return a * b


if __name__ == "__main__":

    special_attributes = [
        "__doc__", "__name__", "__module__", "__qualname__",
        "__defaults__", "__code__", "__globals__", "__dict__",
        "__closure__", "__annotations__", "__kwdefaults__",
    ]

    for attribute in special_attributes:
        print(attribute, '->', getattr(multiplication, attribute))

装饰器

在Python中的装饰器与设计模式的装饰器模式完全是两种概念,除了名字相同,其他的完全不一样。 在Java中有注解@,而python的装饰器也有@但这两种也是不同的方式,在python中@语法糖,是可执行的代码,而Java中的注解@只是一种数据的标识,还需要通过底层的反射来进行处理。

更严格的说这个装饰器,与Spring的AOP思想比较接近。

装饰器:

  1. python代码编写的语法糖
  2. 功能相当于AOP的面相切面的增强

Java中的注解,只是一种标记,背后还需要处理注解的逻辑

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class RestServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(RestServiceApplication.class, args);
    }

    @GetMapping("/")
    public String helloWorld() { 
        return "Hello World";
    }
}

而Python中使用Flask,几行简洁的代码,就能快速实现相同的功能

from flask import Flask

app = Flask(__name__)


@app.route("/")
def hello():
    return "Hello World"


if __name__ == "__main__":
    app.run(debug=True, port=8080)

@app.route("/")这行就是python的装饰器,太让人眼前一亮了。

它只是一种语法糖

是的,python的装饰器只是一种增强功能的一种编程语法糖写法,来看一个例子

我们写了一个tool方法,这个方法用于提取方法的名称以及注释。这在LLM应用开发中很有用,因为Function Call作为LLM的一种tool,我们需要描述清楚这个工具tool的用途,LLM才能推断是否使用这个工具。

def tool(func):
    def wrapper(*args, **kargs):
        doc = getattr(func, "__doc__")
        name = getattr(func, "__name__")
        print(
            f"""你是一个Python专家,你可以使用的工具有
【tools】
  1. {name}: {doc}
"""
        )
        return func(*args, **kargs) # 也可以不调用

    return wrapper

定义一个获取天气的工具:

get_weather = tool(get_weather)
get_weather('深圳')

输出结果

你是一个Python专家,你可以使用的工具有
【tools】
  1. get_weather: 获取一个城市的工具
:param city: 城市名称
今天深圳23°C~小雨

使用装饰器语法糖,输出结果一样

@tool
def get_weather(city):
    """获取一个城市的工具
    :param city: 城市名称
    """
    print(f"今天{city}23°C~小雨")


# 调用函数触发装饰器
get_weather("深圳")

小结

使用语法糖的写法

@tool
def get_weather(city):
    ...

等价于,自己手动写代码的调用

get_weather = tool(get_weather)

对于较小的程序而言,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是区分方法和函数最快的方式。
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)

  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')

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

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

方法重写

子类重写的方法与父类的方法具有相同的名称,用来覆盖从父类继承来的方法,以达到继承父类的绝大部分功能,从而实现定制化的功能。

【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

小结

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

模块

不像Java文件一样,一个文件是一个Java类。在Python中一个.py文件,里面可以有类,有函数有变量。所以每一个.py文件都是一个模块。

image.png

常用内置函数

type,id

help

help自己写的类,可以方便查看可以使用的方法说明

image.png

常用标准模块

copy

浅拷贝

>>> import copy
>>> a = [1,3,5]
>>> b = a    # 只是引用的复制
>>> id(a) == id(b)  # 同一个对象
True
>>> b = copy.copy(a) # 浅拷贝
>>> id(a) == id(b)  # 不再是同一一个对象
False
>>> a[1] = 10  # 修改一个不印象另外一个
>>> a
[1, 10, 5]
>>> b
[1, 3, 5]

浅拷贝的问题

浅拷贝只复制了外层对象,但内层(嵌套)对象依然是共享的引用

>>> import copy
>>> a = [[1,2],[3,5]]
>>> b = copy.copy(a)
>>> id(a) == id(b)
False
>>> a.append('APPENDED')  # 到这里还是正常的
>>> a
[[1, 2], [3, 5], 'APPENDED']
>>> b
[[1, 2], [3, 5]]
>>> a[0][0] = 'CHANGED'  # 浅拷贝问题开始出现
>>> a
[['CHANGED', 2], [3, 5], 'APPENDED']
>>> b
[['CHANGED', 2], [3, 5]]

深拷贝

>>> import copy
>>> a = [[1,2],[3,5]]
>>> b = copy.deepcopy(a)
>>> a[0][0] = 'CHANGED'
>>> a
[['CHANGED', 2], [3, 5]]
>>> b
[[1, 2], [3, 5]]

小结

  1. 浅拷贝只复制最外层容器,内部元素只是引用复制,不递归创建新对象
  2. 如果内层对象是 intstrtuple 等不可变类型,浅拷贝的“共享”是安全的(反正改不了)。
  • 例如 [1, 2, 3] 的浅拷贝完全没问题。
  1. 深拷贝需要递归,对于大对象、多层嵌套对象,代价可能非常大

常用第三方模块