Python 基础:zip() 的妙用与 @property 方法属性化

本文涵盖两个 Python 高频基础知识点:zip() 内置函数的各类实用技巧,以及 @property 装饰器如何将方法"属性化"。从基础语法到实战封装,一篇搞懂。


一、zip() 基础用法

1.1 什么是 zip

zip() 是 Python 内置函数,将多个可迭代对象"拉链式"打包,逐元素配对成元组。

names = ["Alice", "Bob", "Charlie"]
scores = [90, 85, 78]

result = zip(names, scores)
print(list(result))
# [('Alice', 90), ('Bob', 85), ('Charlie', 78)]

可以想象成拉链——两边对齐咬合:

names:  Alice    Bob    Charlie
          ↕       ↕       ↕
scores:   90      85      78
        ───────────────────────
zip:  (Alice,90) (Bob,85) (Charlie,78)

1.2 长度不一致时

zip()最短的可迭代对象为准,多余元素自动丢弃:

a = [1, 2, 3, 4, 5]
b = ["a", "b", "c"]

print(list(zip(a, b)))
# [(1, 'a'), (2, 'b'), (3, 'c')]  ← 4 和 5 被丢弃

如果想保留所有元素,用 itertools.zip_longest

from itertools import zip_longest

a = [1, 2, 3, 4, 5]
b = ["a", "b", "c"]

print(list(zip_longest(a, b, fillvalue="???")))
# [(1, 'a'), (2, 'b'), (3, 'c'), (4, '???'), (5, '???')]

1.3 zip 三个或更多序列

names = ["Alice", "Bob", "Charlie"]
scores = [90, 85, 78]
ages = [20, 21, 22]

for name, score, age in zip(names, scores, ages):
    print(f"{name}: 分数={score}, 年龄={age}")

# Alice: 分数=90, 年龄=20
# Bob: 分数=85, 年龄=21
# Charlie: 分数=78, 年龄=22

二、zip() 的实战技巧

2.1 同时遍历多个列表

最常见的用途——替代索引访问:

# ❌ 不推荐:用索引访问
names = ["Alice", "Bob", "Charlie"]
scores = [90, 85, 78]
for i in range(len(names)):
    print(f"{names[i]}: {scores[i]}")

# ✅ 推荐:用 zip
for name, score in zip(names, scores):
    print(f"{name}: {score}")

2.2 构建字典

keys = ["name", "age", "city"]
values = ["Alice", 20, "Beijing"]

d = dict(zip(keys, values))
print(d)
# {'name': 'Alice', 'age': 20, 'city': 'Beijing'}

2.3 矩阵转置

经典面试题——二维矩阵行列互换:

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]

transposed = list(zip(*matrix))
print(transposed)
# [(1, 4, 7), (2, 5, 8), (3, 6, 9)]

原理:*matrix 将矩阵解包成三个独立的行,zip 再将它们按列重新组合:

解包前:  [1,2,3] [4,5,6] [7,8,9]
            ↓       ↓       ↓
zip后:   (1,4,7) (2,5,8) (3,6,9)

如果需要结果是 list of list:

transposed = [list(row) for row in zip(*matrix)]
print(transposed)
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]

2.4 解压(unzip)

zip 的逆操作——把配对的元组拆回两个列表:

pairs = [("Alice", 90), ("Bob", 85), ("Charlie", 78)]

names, scores = zip(*pairs)
print(names)   # ('Alice', 'Bob', 'Charlie')
print(scores)  # (90, 85, 78)

原理:*pairs 解包成三个元组,zip 重新组合成两组。

2.5 并行排序

按一个列表的顺序对另一个列表排序:

names = ["Alice", "Bob", "Charlie"]
scores = [90, 85, 78]

# 按分数降序排列,名字跟着走
sorted_pairs = sorted(zip(scores, names), reverse=True)
sorted_names = [name for _, name in sorted_pairs]

print(sorted_names)  # ['Alice', 'Bob', 'Charlie']

2.6 交替合并字符串

a = "abc"
b = "123"

merged = "".join(x + y for x, y in zip(a, b))
print(merged)  # a1b2c3

2.7 批量分组

将一个列表按固定大小分组:

def chunk(lst, size):
    """将列表按 size 个一组切分"""
    return list(zip(*[iter(lst)] * size))

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(chunk(data, 3))
# [(1, 2, 3), (4, 5, 6), (7, 8, 9)]  ← 10 被丢弃

# 不丢弃余数版本
from itertools import zip_longest
def chunk_safe(lst, size, fill=None):
    args = [iter(lst)] * size
    return [tuple(x for x in t if x is not fill) for t in zip_longest(*args, fillvalue=fill)]

print(chunk_safe(data, 3))
# [(1, 2, 3), (4, 5, 6), (7, 8, 9), (10,)]

三、zip() 速查表

用法代码结果
基本打包list(zip([1,2], ['a','b']))[(1,'a'), (2,'b')]
构建字典dict(zip(['a','b'], [1,2])){'a':1, 'b':2}
矩阵转置list(zip(*[[1,2],[3,4]]))[(1,3), (2,4)]
解压zip(*[(1,'a'), (2,'b')])(1,2), ('a','b')
长度对齐zip_longest([1,2,3], ['a'])[(1,'a'), (2,None), (3,None)]
并行遍历for a,b in zip(L1, L2)同时取两个列表的元素

四、@property:方法属性化

4.1 什么是方法属性化

@property 装饰器可以将一个方法变成属性来访问,调用时不需要加括号 ()

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        """面积:用属性方式访问,不需要 area()"""
        import math
        return math.pi * self.radius ** 2


c = Circle(5)

# 像属性一样访问,不加括号
print(c.area)  # 78.53981633974483

对比没有 @property 的写法:

# 没有 @property
print(c.area())  # 需要加括号,像个方法调用

# 有 @property
print(c.area)    # 不加括号,像个属性

4.2 为什么用 @property

场景不用 @property用 @property
访问方式obj.method()obj.method
语义这是一个动作这是一个属性
修改内部实现改了属性要改所有调用处内部计算逻辑变了,调用处不用改
参数可以带参数不带参数(纯计算/取值)

4.3 完整的三件套:getter / setter / deleter

class Temperature:
    """温度类,内部用摄氏度存储,对外暴露华氏度"""

    def __init__(self, fahrenheit=32):
        self._celsius = (fahrenheit - 32) * 5 / 9

    @property
    def celsius(self):
        """获取摄氏度"""
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        """设置摄氏度,自动校验范围"""
        if value < -273.15:
            raise ValueError("温度不能低于绝对零度 (-273.15°C)")
        self._celsius = value

    @celsius.deleter
    def celsius(self):
        """删除温度(重置为默认值)"""
        print("温度已重置")
        self._celsius = 0


# 使用
t = Temperature(fahrenheit=100)

# getter —— 像属性一样读
print(f"当前温度: {t.celsius:.1f}°C")  # 当前温度: 37.8°C

# setter —— 像属性一样写
t.celsius = 50
print(f"修改后: {t.celsius:.1f}°C")     # 修改后: 50.0°C

# setter 带校验
try:
    t.celsius = -300  # 低于绝对零度
except ValueError as e:
    print(f"设置失败: {e}")  # 设置失败: 温度不能低于绝对零度

# deleter
del t.celsius  # 温度已重置
print(f"删除后: {t.celsius:.1f}°C")  # 删除后: 0.0°C

4.4 实战:只读计算属性

最常见的用法——把计算逻辑封装成只读属性:

class Student:
    def __init__(self, name, scores):
        self.name = name
        self.scores = scores  # list[float]

    @property
    def average(self):
        """平均分(只读计算属性)"""
        return sum(self.scores) / len(self.scores) if self.scores else 0

    @property
    def highest(self):
        """最高分(只读)"""
        return max(self.scores) if self.scores else 0

    @property
    def grade(self):
        """等级(只读)"""
        avg = self.average
        if avg >= 90:
            return "A"
        elif avg >= 80:
            return "B"
        elif avg >= 70:
            return "C"
        elif avg >= 60:
            return "D"
        return "F"


s = Student("Alice", [88, 92, 79, 95, 86])

# 像属性一样访问计算结果
print(f"学生: {s.name}")
print(f"平均分: {s.average:.1f}")   # 88.0
print(f"最高分: {s.highest}")        # 95
print(f"等级: {s.grade}")           # B

# 只读属性不能赋值
try:
    s.average = 90  # TypeError: can't set attribute
except AttributeError as e:
    print(f"无法设置: {e}")

4.5 实战:带校验的属性

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance

    @property
    def balance(self):
        """余额(对外只读)"""
        return self._balance

    def deposit(self, amount):
        """存款"""
        if amount <= 0:
            raise ValueError("存款金额必须大于 0")
        self._balance += amount
        print(f"  存入 {amount},余额 {self._balance}")

    def withdraw(self, amount):
        """取款"""
        if amount <= 0:
            raise ValueError("取款金额必须大于 0")
        if amount > self._balance:
            raise ValueError(f"余额不足!当前余额 {self._balance}")
        self._balance -= amount
        print(f"  取出 {amount},余额 {self._balance}")


acc = BankAccount("Alice", 1000)

# 余额是只读属性,不能直接赋值
print(f"当前余额: {acc.balance}")  # 1000

acc.deposit(500)    # 存入 500,余额 1500
acc.withdraw(200)   # 取出 200,余额 1300

# 直接修改余额 → 被拦截
try:
    acc.balance = 99999  # AttributeError: can't set attribute
except AttributeError as e:
    print(f"无法直接修改余额: {e}")

4.6 property 的等价写法

@property 本质上是 property() 类的语法糖:

# 语法糖写法(推荐)
class MyClass1:
    def __init__(self):
        self._x = 0

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value


# 等价的 property() 写法(了解即可)
class MyClass2:
    def __init__(self):
        self._x = 0

    def _get_x(self):
        return self._x

    def _set_x(self, value):
        self._x = value

    x = property(_get_x, _set_x)


# 两种写法效果完全一样
a = MyClass1()
a.x = 10
print(a.x)  # 10

b = MyClass2()
b.x = 20
print(b.x)  # 20

property 四个参数:

property(fget=None, fset=None, fdel=None, doc=None)
参数对应装饰器作用
fget@property读取时调用
fset@xxx.setter赋值时调用
fdel@xxx.deleterdel 时调用
doc文档字符串help() 中显示

五、zip + @property 综合实战

把两个知识点结合起来——封装一个数据统计类:

class DataAnalyzer:
    """数据分析工具:用 zip 做并行统计,用 @property 暴露结果"""

    def __init__(self, labels: list[str], values: list[float]):
        if len(labels) != len(values):
            raise ValueError("labels 和 values 长度必须一致")
        self._data = list(zip(labels, values))

    @property
    def count(self):
        """数据条数"""
        return len(self._data)

    @property
    def total(self):
        """总和"""
        return sum(v for _, v in self._data)

    @property
    def average(self):
        """平均值"""
        return self.total / self.count if self.count else 0

    @property
    def max_item(self):
        """最大值项 (label, value)"""
        if not self._data:
            return None
        # zip(*self._data) 解压成 labels 和 values 两组
        labels, values = zip(*self._data)
        max_idx = values.index(max(values))
        return (labels[max_idx], values[max_idx])

    @property
    def ranking(self):
        """按值降序排列"""
        return sorted(self._data, key=lambda x: x[1], reverse=True)

    def summary(self):
        """打印汇总报告"""
        print(f"数据条数: {self.count}")
        print(f"总和: {self.total:.2f}")
        print(f"平均值: {self.average:.2f}")
        print(f"最大值: {self.max_item[0]} = {self.max_item[1]}")
        print("排名:")
        for i, (label, value) in enumerate(self.ranking, 1):
            print(f"  {i}. {label}: {value}")


# 使用
analyzer = DataAnalyzer(
    labels=["Python", "Java", "Go", "Rust", "JavaScript"],
    values=[85.5, 72.3, 68.0, 91.2, 78.5],
)

analyzer.summary()

输出:

数据条数: 5
总和: 395.5
平均值: 79.1
最大值: Rust = 91.2
排名:
  1. Rust: 91.2
  2. Python: 85.5
  3. JavaScript: 78.5
  4. Java: 72.3
  5. Go: 68.0

六、小结

zip() 要点

技巧代码模式
并行遍历for a, b in zip(L1, L2)
构建字典dict(zip(keys, values))
矩阵转置zip(*matrix)
解压zip(*pairs)
批量分组zip(*[iter(lst)] * n)

@property 要点

要点说明
基本用途方法变属性,访问不加 ()
只读属性只写 @property,不写 @xxx.setter
带校验赋值@xxx.setter 中做校验
三件套@property + @xxx.setter + @xxx.deleter
本质property(fget, fset, fdel) 的语法糖

如果本文对你有帮助,欢迎点赞收藏!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值