作为一名Java程序员,接下来从我个人的经验和认知中,从语言特性方面总结Python中比较特别的地方。方便能够更好的进行开发实践,成为一名合格的Python程序员。
可读性设计哲学
Python中倾向英文单词而非符号
not比!更接近自然语言- 类似的设计:
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是一样的。
字典
- 字典的方法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')]
关于函数
属性
没错函数有一些固有的属性,也叫做函数的元数据
getattr内置函数,能够获取对象的属性。getattr(obj,attribute)等效于obj.attribute__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思想比较接近。
装饰器:
- python代码编写的语法糖
- 功能相当于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
| 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是区分方法和函数最快的方式。
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)
- 特性(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')
__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__()
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
"""
方法重写
子类重写的方法与父类的方法具有相同的名称,用来覆盖从父类继承来的方法,以达到继承父类的绝大部分功能,从而实现定制化的功能。
【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
小结
模块
不像Java文件一样,一个文件是一个Java类。在Python中一个.py文件,里面可以有类,有函数有变量。所以每一个.py文件都是一个模块。
常用内置函数
type,id
help
help自己写的类,可以方便查看可以使用的方法说明
常用标准模块
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]]
小结
- 浅拷贝只复制最外层容器,内部元素只是引用复制,不递归创建新对象
- 如果内层对象是
int、str、tuple等不可变类型,浅拷贝的“共享”是安全的(反正改不了)。
- 例如
[1, 2, 3]的浅拷贝完全没问题。
- 深拷贝需要递归,对于大对象、多层嵌套对象,代价可能非常大