什么是面对向语言
面向对象编程语言(Object-Oriented Programming Language,简称OOP语言)是一种基于"对象"概念的编程范式。这种编程风格强调使用对象来模拟现实世界中的事物,并通过对象之间的交互来实现软件功能。面向对象编程语言的主要特点包括:
1。抽象:通过抽象,可以将复杂的现实世界问题简化为易于理解和处理的模型。在面向对象编程中,抽象是通过类(class)来实现的,类是对现实世界中事物的抽象描述。
2.封装:封装是指将数据和数据操作方法捆绑在一起,形成一个独立的单元,即对象。封装可以隐藏对象的内部实现细节,只对外提供必要的接口,从而降低模块间的耦合度。
3.继承:继承是一种允许新创建的类(子类)继承现有类(父类)的属性和方法的机制。通过继承,可以实现代码的重用,提高开发效率。
4.多态:多态是指允许不同类的对象对同一消息做出响应的能力。多态性使得程序具有更好的灵活性和可扩展性。
常见的面向对象编程语言有:Java、C++、Python、C#、Ruby等。这些语言都支持上述面向对象编程的基本特性,并在此基础上发展出了各自独特的特性和库。
什么是魔术方法
在 Python 中,魔术方法(Magic Methods)是一种特殊的方法,它们的名称以双下划线 __ 开头和结尾(例如 init 、str),也被称为 "双下划线方法"(Dunder Methods)。
魔术方法和普通函数一样,都是用 def 关键字定义的方法,但它们的核心区别在于调用方式和设计目的。 普通方法:需要显式调用,比如 obj.method() 魔术方法:通常不需要手动调用,而是在特定场景下由 Python 解释器自动触发
python
class Person:
def __init__(self, name): # 魔术方法
self.name = name
def greet(self): # 普通方法
return f"Hello, {self.name}!"
p = Person("Alice") # 自动触发 __init__(无需写 p.__init__())
print(p.greet()) # 必须显式调用 greet()
参数传递: 传入了一个实参 "Alice"(字符串),Python 解释器会自动在参数列表前添加实例本身作为第一个参数(即 self),因此实际调用 init 时的参数是:self = p(新创建的实例),name = "Alice"。
self 的特殊作用: 所有实例方法(包括魔术方法)的第一个参数必须是 self(名称约定,不是关键字),self 代表当前实例对象本身,在调用方法时由 Python 自动传入,通过 self.属性名 可以访问或修改实例的属性。
构建一副扑克牌
python
import collections # 导入collections模块,用于创建命名元组
# 使用namedtuple创建一个名为Card的命名元组类型,包含rank(点数)和suit(花色)两个字段
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck: # 定义一个FrenchDeck类,用于表示一副法国扑克牌
# 定义牌的点数列表,包括2-10以及J、Q、K、A
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
# 定义牌的花色列表,包括黑桃、红心、方块和梅花
suits = ['♠️', '♥️', '♦️', '♣️']
def __init__(self): # 初始化方法,创建一副完整的扑克牌
# 使用列表推导式创建所有可能的牌组合,先按花色排序,再按点数排序
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]
def __len__(self): # 实现len()函数,返回牌的数量
return len(self._cards) # 返回_cards列表的长度
def __getitem__(self, position):
return self._cards[position] # 返回指定位置的牌
定义单张牌的结构(Card 命名元组) Card = collections.namedtuple('Card', ['rank', 'suit']) 这行代码创建了一种 "单张牌" 的类型,每张牌有两个属性:rank:点数,suit:花色
定义整副牌的规则(FrenchDeck 类) ranks 列表:定义所有可能的点数(2-10、J、Q、K、A) suits 列表:定义所有花色(4 种)
生成完整的牌组(init 方法) self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks] 这行代码用嵌套循环生成了所有可能的牌: 先遍历每个花色 对每个花色,遍历所有点数(2、3、...、A) 每组合一个(点数 + 花色),就创建一张 Card 实例 最终生成 13 个点数 × 4 种花色 = 52 张牌,存在 _cards 列表中。
支持 "查数量" 和 "取牌" 操作 len 方法:让我们可以用 len(deck) 直接知道牌的总数(52 张) getitem 方法:让我们可以用 deck[0] 取第 1 张牌,deck[-1] 取最后 1 张牌
场景 1:想知道一副牌有多少张 不用魔术方法:得写一个普通方法,比如get_length(),每次用都要显式调用:
python
class FrenchDeck:
def get_length(self): # 普通方法
return len(self._cards)
deck = FrenchDeck()
print(deck.get_length()) # 必须写方法名,还得加括号
用魔术方法__len__:直接用 Python 原生的len()函数,不用记类的自定义方法名:
python
print(len(deck)) # 所有能算"长度"的东西(列表、字符串、字典、你的牌组)都用len(),统一又好记
场景 2:想取第 10 张牌 不用魔术方法:得写get_card(position)这样的普通方法:
python
def get_card(self, pos):
return self._cards[pos]
print(deck.get_card(10)) # 每次取牌都要写方法名
用魔术方法__getitem__:直接用 deck[10] 就行,不用记方法名:
python
print(deck[10]) # 索引从0开始,所以第10张牌是索引为10的牌
getitem 方法之所以能让这摞牌(FrenchDeck 实例)变得可迭代(可以用 for 循环遍历),核心原因是 Python 会自动利用 getitem 实现迭代逻辑,不需要我们手动编写额外的迭代方法。
具体原理: 用 for card in deck: 遍历一副牌时,Python 实际上在做一件很简单的事:从位置 0 开始,不断调用 getitem (position) 取元素,直到取到最后一个位置。 步骤分解如下: 第一次循环:调用 deck.getitem (0),拿到第 0 张牌 第二次循环:调用 deck.getitem(1),拿到第 1 张牌 继续下去:直到 position 超过牌的总数(51),此时会触发 IndexError 异常,Python 会自动捕获这个异常并结束循环
为什么 getitem 能 "兼职" 迭代功能? Python 的迭代机制有一个 "偷懒" 的设计:如果一个对象没有实现专门的迭代方法(iter ),但实现了 getitem ,就会默认用 getitem 来迭代,从索引 0 开始依次获取元素。 这就像:如果一个容器没有 "专门的迭代工具",但有 "按序号取东西" 的功能,那我们就可以从第 0 个开始一个个拿,直到拿完为止。
#doctest: +ELLIPSIS 是 Python 中 doctest 模块的一个选项标记,用于在文档测试(doctest)中启用省略号匹配模式。 它的作用是:让测试时可以用 ...(省略号)匹配字符串中任意长度的任意字符(包括空字符),灵活处理那些 "部分内容不固定" 的输出结果。 举个例子理解 假设你有一个函数返回当前时间,输出中包含不固定的时间值:
python
import datetime
def get_current_time():
"""
返回当前时间的字符串表示。
>>> get_current_time() # doctest: +ELLIPSIS
'2023-10-09 15:...'
"""
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
这里的 ... 会匹配任意分钟和秒数(比如 15:30:45 或 15:01:59 都能匹配 15:...),避免因时间动态变化导致测试失败。 为什么需要它? 当函数输出中包含动态变化的内容(如时间、内存地址、随机数等),直接写固定的预期结果会导致测试频繁失败。 +ELLIPSIS 允许我们用 ... 模糊匹配这些不固定的部分,只校验关键的固定内容,让文档测试更灵活。
在 Python 中,迭代的 "隐式" 主要体现在:我们很少直接写出 "迭代的具体步骤",而是通过简洁的语法(如 for 循环)触发迭代,背后的细节由 Python 自动处理。 举个直观的例子对比: 假设我们要遍历一副牌(deck)中的所有牌: 显式迭代(手动控制步骤): 如果没有迭代机制,我们需要手动写循环逻辑,明确控制 "从哪开始、到哪结束、每次怎么取":
python
position = 0
while position < len(deck):
card = deck[position] # 手动获取当前位置的牌
print(card)
position += 1 # 手动更新位置
这里的每一步(初始化位置、判断是否结束、取元素、更新位置)都需要我们显式写出,非常繁琐。 隐式迭代(Python 自动处理): 有了迭代机制后,我们只需要写:
python
for card in deck:
print(card)
这里没有出现 "位置""索引""循环条件" 等细节 ------Python 自动帮我们完成了 "从 0 开始取元素、依次递增位置、直到结束" 的全过程。 我们看不到迭代的具体步骤,却能直接使用迭代的结果,这就是 "隐式" 的核心含义。 更深层的原因:迭代器协议的 "自动生效" Python 中,一个对象能被迭代(如 for 循环、in 运算符等),是因为它遵循了 "迭代器协议"------ 但我们通常不需要手动实现完整的协议,很多时候像 FrenchDeck 那样只定义 getitem 就够了。 Python 会自动检测对象是否支持迭代: 如果有 iter 方法(迭代器协议的核心),就用它生成迭代器; 如果没有,就尝试用 getitem 按索引迭代(像牌组的例子); 整个过程不需要我们调用任何迭代相关的方法(如 next),Python 内部自动完成。
排序
完整代码
python
import collections # 导入collections模块,用于创建命名元组
# 使用namedtuple创建一个名为Card的命名元组类型,包含rank(点数)和suit(花色)两个字段
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck: # 定义一个FrenchDeck类,用于表示一副法国扑克牌
# 定义牌的点数列表,包括2-10以及J、Q、K、A
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
# 定义牌的花色列表,包括黑桃、红心、方块和梅花
suits = ['♠️', '♥️', '♦️', '♣️']
def __init__(self): # 初始化方法,创建一副完整的扑克牌
# 使用列表推导式创建所有可能的牌组合,先按花色排序,再按点数排序
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]
def __len__(self): # 实现len()函数,返回牌的数量
return len(self._cards) # 返回_cards列表的长度
def __getitem__(self, position):
return self._cards[position] # 返回指定位置的牌
#排序
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0) # 定义花色的优先级,黑桃最高,梅花最低
def spades_high(card): # 定义一个函数,用于按花色优先级和点数排序
rank_value = FrenchDeck.ranks.index(card.rank) # 获取牌的点数在ranks列表中的索引
return rank_value * len(suit_values) + suit_values[card.suit] # 返回按花色优先级和点数排序的结果
# 修正花色与权重的对应关系(用符号作为键)
suit_values = {'♠️': 3, '♥️': 2, '♦️': 1, '♣️': 0}
deck = FrenchDeck()
print("排序后前5张:", sorted(deck, key=spades_high)[:5])