查看: 329|回复: 0

Python函数与OOP实战:默认参数坑、MRO与魔术方法

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
前言
Python 的函数比 Java 灵活,默认参数、关键字参数、lambda 能替代不少重载和接口写法;面向对象上,多继承、MRO、动态属性又与 Java 差异明显。下面按函数、类、继承与 MRO 的顺序,把原文涉及的语法、代码和易错点整理成可直接在解释器里验证的实践笔记。

一、函数定义、返回值和默认参数陷阱
Python 用 def 定义函数,不强制声明返回类型和参数类型,但支持类型注解。函数是一等公民,可赋值、传参、返回;Java 的方法必须属于类。返回多个值本质是返回元组,调用处可解包。
  1. def add(a: int, b: int) -> int:
  2.     '''两数相加'''
  3.     return a + b
  4. result = add(1, 2)
  5. print(result)  # 3
  6. def divide(a, b):
  7.     quotient = a // b
  8.     remainder = a % b
  9.     return quotient, remainder
  10. q, r = divide(10, 3)
  11. print(f'商: {q}, 余: {r}')  # 商: 3, 余: 1
复制代码
默认参数最容易踩的坑是使用可变对象。默认参数在函数定义时只创建/求值一次,不是每次调用重新创建:
  1. def append_to(item, lst=[]):
  2.     lst.append(item)
  3.     return lst
  4. print(append_to(1))  # [1]
  5. print(append_to(2))  # [1, 2]
  6. print(append_to(3))  # [1, 2, 3]
  7. def append_to_fixed(item, lst=None):
  8.     if lst is None:
  9.         lst = []
  10.     lst.append(item)
  11.     return lst
复制代码
正确写法是用 None 作为哨兵,在函数体内再创建列表。

二、位置参数、关键字参数与 *args/**kwargs
默认参数可以替代 Java 中多个重载方法。调用时,位置参数按顺序传,关键字参数按名字传,顺序随意;混合使用时位置参数在前,关键字参数在后。Python 3.8+ 还支持 / 和 * 来强制位置参数、强制关键字参数:
  1. def greet(name: str, greeting: str = 'Hello', punctuation: str = '!') -> str:
  2.     return f'{greeting}, {name}{punctuation}'
  3. print(greet('Alice'))                   # Hello, Alice!
  4. print(greet('Bob', 'Hi'))               # Hi, Bob!
  5. print(greet('Charlie', 'Hey', '.'))     # Hey, Charlie.
  6. def create_user(name, age, city='Unknown'):
  7.     print(f'{name}, {age}岁, 来自{city}')
  8. create_user('Alice', 25, 'Beijing')
  9. create_user('Bob', city='Shanghai', age=30)
  10. create_user('Charlie', 28, city='Shenzhen')
  11. def func(a, b, /, c, d, *, e, f):
  12.     pass
复制代码
/ 前面的 a、b 只能按位置传;中间的 c、d 位置或关键字都行;* 后面的 e、f 必须用关键字传。
*args 接收任意数量位置参数并打包成元组,**kwargs 接收任意数量关键字参数并打包成字典:
  1. def print_info(name, *args, **kwargs):
  2.     print(f'Name: {name}')
  3.     print(f'Args: {args}')
  4.     print(f'Kwargs: {kwargs}')
  5. print_info('Alice', 25, 'Beijing', hobby='coding', level='senior')
  6. # Args: (25, 'Beijing')
  7. # Kwargs: {'hobby': 'coding', 'level': 'senior'}
复制代码
常见错误:func(1, a=2) 会报 TypeError: func() got multiple values for argument 'a',因为 1 已经赋给 a。另一个容易忽略的点是,func(1, 2, 3, b=4) 中 b=4 不会报未知参数,而是进入 **kwargs。

三、Lambda 的适用场景与闭包陷阱
lambda 是匿名函数简写,只能写一个表达式,不能写多行逻辑。它最常见的场景是排序 key,例如按字典字段、字符串长度或多条件排序:
  1. double = lambda x: x * 2
  2. print(double(5))  # 10
  3. students = [
  4.     {'name': 'Alice', 'age': 25},
  5.     {'name': 'Bob', 'age': 20},
  6.     {'name': 'Charlie', 'age': 23}
  7. ]
  8. students.sort(key=lambda s: s['age'])
  9. print(students)
  10. words = ['banana', 'pie', 'Washington', 'book']
  11. words.sort(key=lambda w: len(w))
  12. print(words)  # ['pie', 'book', 'banana', 'Washington']
  13. students.sort(key=lambda s: (s['age'], s['name']))
复制代码
选择 lambda 还是 def:排序 key、map/filter 回调适合 lambda;逻辑超过一行、需要复用、需要文档字符串或异常处理时用 def。
闭包陷阱要特别注意。下面代码想得到 f(0)=0、f(1)=1……实际全是 4:
  1. funcs = [lambda: i for i in range(5)]
  2. print([f() for f in funcs])  # [4, 4, 4, 4, 4]
  3. funcs = [lambda i=i: i for i in range(5)]
  4. print([f() for f in funcs])  # [0, 1, 2, 3, 4]
复制代码
原因是 lambda 捕获的是变量 i 的引用,不是当时的值;循环结束后 i=4。用默认参数 i=i 绑定当前值即可修复。Java 的 lambda 要求捕获 effectively final 变量,因此不会出现同样问题。

四、类、self、实例属性与类属性
Python 用 class 定义类,__init__ 是构造方法,self 类似 Java 的 this,但必须显式写,实例方法第一个参数必须是 self。创建对象不需要 new。
  1. class Student:
  2.     school = '清华大学'
  3.     def __init__(self, name: str, age: int):
  4.         self.name = name
  5.         self.age = age
  6.         self._score = 0
  7.     def introduce(self) -> str:
  8.         return f'{self.name}, {self.age}岁, 来自{Student.school}'
  9.     def study(self, course: str):
  10.         print(f'{self.name}正在学习{course}')
  11. stu = Student('Alice', 20)
  12. print(stu.introduce())
  13. stu.study('Python')
复制代码
类属性由所有实例共享,实例属性每个实例独有。读属性时先找实例再找类;但通过实例赋值不会修改类属性,而是创建同名实例属性,产生遮蔽:
  1. class Counter:
  2.     count = 0
  3.     def __init__(self, name):
  4.         self.name = name
  5.         Counter.count += 1
  6. c1 = Counter('A')
  7. c2 = Counter('B')
  8. print(Counter.count)  # 2
  9. print(c1.count)       # 2
  10. print(c2.count)       # 2
  11. c1.count = 100
  12. print(c1.count)       # 100,实例属性
  13. print(c2.count)       # 2,类属性未变
  14. print(Counter.count)  # 2,类属性未变
复制代码
忘记写 self 的典型报错是 TypeError: wrong_method() takes 0 positional arguments but 1 was given。因为 Python 自动把实例作为第一个参数传入,但函数没有接收。

五、@staticmethod、@classmethod 与 @property
@staticmethod 不需要 self 或 cls,适合纯工具方法;@classmethod 第一个参数是 cls,可访问类属性,常用于工厂方法,并且在继承时 cls 会变成子类:
  1. class MathUtil:
  2.     @staticmethod
  3.     def add(a: int, b: int) -> int:
  4.         return a + b
  5.     @staticmethod
  6.     def is_even(n: int) -> bool:
  7.         return n % 2 == 0
  8. print(MathUtil.add(1, 2))     # 3
  9. print(MathUtil.is_even(4))    # True
  10. class Date:
  11.     def __init__(self, year: int, month: int, day: int):
  12.         self.year = year
  13.         self.month = month
  14.         self.day = day
  15.     @classmethod
  16.     def from_string(cls, date_str: str) -> 'Date':
  17.         year, month, day = map(int, date_str.split('-'))
  18.         return cls(year, month, day)
  19.     def __str__(self):
  20.         return f'{self.year}-{self.month:02d}-{self.day:02d}'
  21. d1 = Date.from_string('2026-09-18')
  22. print(d1)  # 2026-09-18
复制代码
实例方法能访问实例属性和类属性;@classmethod 能访问类属性;@staticmethod 都不能直接访问。Java 的 static 方法大致对应 @staticmethod,@classmethod 在 Java 中没有直接对应。
@property 把方法变成属性访问,适合在赋值时加校验、暴露只读计算属性:
  1. class Student:
  2.     def __init__(self, name: str, score: int):
  3.         self.name = name
  4.         self._score = score
  5.     @property
  6.     def score(self) -> int:
  7.         print(' [调用getter]')
  8.         return self._score
  9.     @score.setter
  10.     def score(self, value: int):
  11.         if not 0 <= value <= 100:
  12.             raise ValueError('分数必须在0-100之间')
  13.         self._score = value
  14.     @property
  15.     def grade(self) -> str:
  16.         if self._score >= 90:
  17.             return 'A'
  18.         elif self._score >= 80:
  19.             return 'B'
  20.         elif self._score >= 60:
  21.             return 'C'
  22.         else:
  23.             return 'D'
  24. stu = Student('Alice', 85)
  25. print(stu.score)   # 85
  26. stu.score = 92
  27. print(stu.grade)   # A
复制代码
只提供 getter 不提供 setter 的属性是只读的,对 stu.grade 赋值会触发 AttributeError。

六、继承、多继承与 MRO
Python 继承用 class Dog(Animal),子类通过 super().__init__(name) 调用父类构造,方法覆盖与 Java 类似:
  1. class Animal:
  2.     def __init__(self, name: str):
  3.         self.name = name
  4.     def speak(self) -> str:
  5.         return f'{self.name} makes a sound'
  6.     def __str__(self):
  7.         return f'Animal({self.name})'
  8. class Dog(Animal):
  9.     def __init__(self, name: str, breed: str):
  10.         super().__init__(name)
  11.         self.breed = breed
  12.     def speak(self) -> str:
  13.         return f'{self.name} barks: Woof!'
  14.     def fetch(self, item: str):
  15.         return f'{self.name} fetches {item}'
  16. dog = Dog('Buddy', 'Golden Retriever')
  17. print(dog.speak())      # Buddy barks: Woof!
  18. print(dog.fetch('ball'))# Buddy fetches ball
  19. print(dog)              # Animal(Buddy)
复制代码
Python 支持多继承,Java 只能单继承和多个接口:
  1. class Flyable:
  2.     def fly(self):
  3.         return f'{self.__class__.__name__} is flying'
  4. class Swimmable:
  5.     def swim(self):
  6.         return f'{self.__class__.__name__} is swimming'
  7. class Duck(Flyable, Swimmable):
  8.     def quack(self):
  9.         return 'Quack! Quack!'
  10. duck = Duck()
  11. print(duck.fly())
  12. print(duck.swim())
  13. print(duck.quack())
复制代码
多继承下同名方法的查找顺序由 MRO 决定,Python 使用 C3 线性化算法:
  1. class A:
  2.     def who(self): return 'A'
  3. class B(A):
  4.     def who(self): return 'B'
  5. class C(A):
  6.     def who(self): return 'C'
  7. class D(B, C):
  8.     pass
  9. d = D()
  10. print(d.who())      # B
  11. print(D.__mro__)    # D, B, C, A, object
复制代码
原文提醒不需要死记 C3 算法细节,关键是理解查找顺序会按 MRO 从左到右、从子类到父类依次解析。私有属性、名称改写与魔术方法在原文目录中列出,但正文提供的代码事实到此为止,本文不额外编造未给出的实现。

结尾实践建议
默认参数不要用可变对象;需要多返回值用元组解包;lambda 仅适合一行表达式;类属性修改建议用类名;通过实例赋值会遮蔽类属性;工厂方法优先用 @classmethod;继承层级复杂时用 __mro__ 确认调用顺序。这些都是原文代码能直接推导出的结论,也是面试和实际脚本开发中最容易出错的地方。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-22 13:29 , Processed in 0.026106 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部