杰瑞科技汇

Effective Python核心要点有哪些?

核心理念:拥抱 Python 的哲学

在深入具体技巧前,理解 Python 的设计哲学至关重要,这体现在《The Zen of Python`import this``) 中:

Effective Python核心要点有哪些?-图1
(图片来源网络,侵删)
  • 优美胜于丑陋: 代码要易于阅读和欣赏。
  • 明了胜于晦涩: 代码的意图应该清晰明了。
  • 简洁胜于复杂: 用最简单的方式解决问题。
  • 扁平胜于嵌套: 避免过深的代码嵌套。
  • 可读性很重要: 这是 Python 的核心信条。
  • 虽然实用性很重要,但不应损害可读性。
  • 错误不应被静默忽略: 明确的错误处理机制。
  • 除非你明确地想这样做。
  • 面对模棱两可的请求,不要猜测: 澄清需求。
  • 任何问题应该有一个(最好是只有一个)显而易见的解决方案。
  • 虽然那起初可能并不明显,除非你是个荷兰人。 (指 Guido van Rossum)
  • 现在总比没有好。
  • 虽然永远比现在晚。
  • 如果实现很难解释,那它是个坏点子。
  • 如果实现很容易解释,那它可能是个好点子。
  • 命名空间是个绝妙的想法,我们应该多加利用!

用 Pythonic 的方式思考

帮助你从其他语言(如 C++/Java)的思维转向 Python 的思维模式。

用列表推导和生成器表达式代替 mapfilter

列表推导和生成器表达式更 Pythonic,可读性更强。

  • 不推荐: mapfilter 需要配合 lambda 使用,代码冗长。

    # 使用 map
    squares = list(map(lambda x: x**2, range(10)))
    # 使用 filter
    even_squares = list(filter(lambda x: x % 2 == 0, squares))
  • 推荐: 列表推导式更直观。

    # 使用列表推导
    squares = [x**2 for x in range(10)]
    even_squares = [x for x in squares if x % 2 == 0]
  • 注意: 当数据量很大时,应使用生成器表达式(用圆括号 ),它按需生成数据,节省内存。

    # 生成器表达式
    sum_of_squares = sum(x**2 for x in range(1000000))

enumerate 替代 range(len())

enumerate 能同时获取索引和值,代码更清晰。

  • 不推荐:

    for i in range(len(flavors)):
        print(f"{i + 1}: {flavors[i]}")
  • 推荐:

    for i, flavor in enumerate(flavors, start=1): # start=1 可指定起始索引
        print(f"{i}: {flavor}")

zip 同时迭代多个序列

zip 可以将多个序列“拉链”在一起,并行迭代。

  • 不推荐:

    names = ['Alice', 'Bob', 'Charlie']
    ages = [30, 25, 35]
    for i in range(len(names)):
        print(names[i], ages[i])
  • 推荐:

    for name, age in zip(names, ages):
        print(name, age)
  • 注意: 在 Python 2 中,zip 会返回一个列表,消耗内存,在 Python 3 中,zip 返回的是一个迭代器,按需生成,非常高效。

itemgetteroperator.attrgetter 排序复杂序列

当需要根据对象的某个属性或字典的某个键进行排序时,使用 sortedkey 参数。

  • 不推荐: 使用 lambda,虽然简单但对于复杂属性访问可能效率稍低。

    class User:
        def __init__(self, user_id):
            self.user_id = user_id
    users = [User(23), User(3), User(99)]
    sorted_users = sorted(users, key=lambda u: u.user_id)
  • 推荐: 使用 operator.itemgetterattrgetter,速度更快,意图更明确。

    from operator import itemgetter, attrgetter
    # 对于字典列表
    rows = [
        {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
        {'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
    ]
    sorted_rows = sorted(rows, key=itemgetter('fname', 'lname')) # 可按多个键排序
    # 对于对象列表
    sorted_users = sorted(users, key=attrgetter('user_id'))

函数

函数是 Python 的核心构建块。

用位置参数进行函数操作,关键字参数进行函数扩展

  • 位置参数: 调用时必须按顺序提供,它们定义了函数的基本操作。
  • 关键字参数: 调用时通过参数名指定,它们提供了灵活性,并使代码更易读,它们通常用于设置默认值。
def compare(a, b, *, key=None, reverse=False): # * 之后的参数必须为关键字参数
    # key 参数用于指定比较的依据
    # reverse 参数用于指定是否逆序
    # ...
    pass
compare(a, b) # 正确
compare(a, b, key=str.lower) # 正确
compare(a, b, True) # 错误 TypeError

不要使用可变默认参数

可变对象(如列表、字典)作为默认参数时,会在函数定义时被创建一次,而不是每次调用时都创建,这会导致意想不到的副作用。

  • 不推荐:

    def append_to_bad_list(item, bad_list=[]):
        bad_list.append(item)
        return bad_list
    print(append_to_bad_list(1)) # [1]
    print(append_to_bad_list(2)) # [1, 2]  意外的副作用!
  • 推荐: 使用 None 作为默认值,在函数内部检查并初始化。

    def append_to_good_list(item, good_list=None):
        if good_list is None:
            good_list = []
        good_list.append(item)
        return good_list
    print(append_to_good_list(1)) # [1]
    print(append_to_good_list(2)) # [2]

*args**kwargs 处理可变参数

  • *args: 将位置参数收集到一个元组中。
  • **kwargs: 将关键字参数收集到一个字典中。

这可以让你编写更通用的函数,特别是包装器或装饰器。

def log(message, *values, **kwargs):
    if not values:
        print(message)
    else:
        values_str = ', '.join(str(v) for v in values)
        print(f"{message}: {values_str}")
    # 可以处理其他关键字参数
    if 'prefix' in kwargs:
        print(f"Prefix: {kwargs['prefix']}")
log("My numbers are", 1, 2, 3)
log("Hi there")
log("Error", code=500, severity="high")

类与继承

Python 的面向对象设计有其独特之处。

用简单的公开属性代替复杂的 gettersetter

Python 鼓励直接访问对象的属性,而不是通过 get()set() 方法,这被称为“我们都是成年人”的原则。

  • 不推荐(Java/C++ 风格):

    class OldResistor:
        def __init__(self, ohms):
            self._ohms = ohms
        def get_ohms(self):
            return self._ohms
        def set_ohms(self, ohms):
            self._ohms = ohms
  • 推荐(Pythonic 风格):

    class Resistor:
        def __init__(self, ohms):
            self.ohms = ohms  # 直接访问
            self.voltage = 0
            self.current = 0
    r1 = Resistor(50e3)
    r1.ohms = 10e3 # 直接赋值
  • 何时需要 @property 当属性的获取或设置需要执行额外逻辑时(如验证、计算、日志记录)。

    class VoltageResistor(Resistor):
        def __init__(self, ohms):
            super().__init__(ohms)
            self._voltage = 0
        @property
        def voltage(self):
            return self._voltage
        @voltage.setter
        def voltage(self, voltage):
            self._voltage = voltage
            self.current = self.voltage / self.ohms

__slots__ 优化内存占用

默认情况下,Python 的每个实例都有一个 __dict__ 字典来存储其属性,这很灵活但消耗内存。

__slots__ 告诉 Python 不要为实例创建 __dict__,而是预先定义好固定的属性列表,这可以:

  1. 大幅减少内存占用。
  2. 稍微加快属性访问速度。
  3. 防止用户动态添加新属性。

缺点:

  • 无法为实例添加 __slots__ 中未定义的属性。
  • 无法为类定义子类(除非子类也定义 __slots__)。
class Player:
    __slots__ = ['name', 'score'] # 只允许这两个属性
    def __init__(self, name, score):
        self.name = name
        self.score = score

重写 __eq__ 时也要重写 __hash__

__hash__ 方法用于将对象转换为哈希值,通常用于字典的键或集合的成员。 __eq__ 方法用于比较两个对象是否相等。

Python 的一个基本规则是:如果两个对象相等,它们的哈希值也必须相等。

  • 不推荐: 如果只重写了 __eq__ 而没有重写 __hash__,实例将无法被正确地放入集合或用作字典键。

    class Card:
        def __init__(self, rank, suit):
            self.rank = rank
            self.suit = suit
        def __eq__(self, other):
            return self.rank == other.rank and self.suit == other.suit
        # 未定义 __hash__,Python 会将其设为 None
    c1 = Card('7', 'hearts')
    c2 = Card('7', 'hearts')
    print(c1 == c2) # True
    print(hash(c1) == hash(c2)) # AttributeError: 'Card' object has no attribute '__hash__'
  • 推荐:

    class Card:
        # ... __init__ 和 __eq__ 同上 ...
        def __hash__(self):
            return hash((self.rank, self.suit)) # 对一个元组求哈希

元类及更多高级特性

比较深入,但在某些特定场景下非常有用。

@classmethod@staticmethod 区分类方法与静态方法

  • 实例方法: 第一个参数是 self,用于操作实例状态。
  • @classmethod: 第一个参数是 cls(类本身),用于操作类状态或创建实例的工厂方法。
  • @staticmethod: 不接收 selfcls 参数,它只是一个在类中定义的普通函数,与类没有绑定关系。
class MyClass:
    def instance_method(self):
        print(f"Called instance method of {self}")
    @classmethod
    def class_method(cls):
        print(f"Called class method of {cls}")
        # 常用作工厂方法
        # return cls()
    @staticmethod
    def static_method():
        print("Called static method")
obj = MyClass()
obj.instance_method()
MyClass.class_method()
MyClass.static_method()

__getattr__, __getattribute____setattr__ 实现属性访问的代理

这些是 Python 的“描述符协议”的一部分,用于精细控制属性的访问。

  • __getattr__(self, name): 当通过常规方式(如 obj.name)找不到属性时被调用。
  • __getattribute__(self, name): 每次通过常规方式访问属性时都会被调用(要小心无限递归)。
  • __setattr__(self, name, value): 每次给属性赋值时被调用(也要小心无限递归)。

一个经典例子是代理模式:

class Delegator:
    def __init__(self, delegate):
        self.delegate = delegate
    def __getattr__(self, name):
        # 将属性访问转发给被代理的对象
        return getattr(self.delegate, name)
    def __setattr__(self, name, value):
        # 将属性赋值转发给被代理的对象
        if name == 'delegate':
            super().__setattr__(name, value)
        else:
            setattr(self.delegate, name, value)
# 原始对象
original = {'a': 1, 'b': 2}
# 代理对象
proxy = Delegator(original)
print(proxy.a) # 输出 1
proxy.c = 3
print(original) # 输出 {'a': 1, 'b': 2, 'c': 3}

并发与并行

concurrent.futures 管理线程池和进程池

直接使用 threadingmultiprocessing 模块比较底层。concurrent.futures 提供了更高层次的抽象,更易于使用。

  • ThreadPoolExecutor: 适用于 I/O 密集型任务(如网络请求、文件读写)。
  • ProcessPoolExecutor: 适用于 CPU 密集型任务(如数值计算)。
import concurrent.futures
import time
def do_task(n):
    time.sleep(n)
    return f"Task {n} done"
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    # 提交任务,返回一个 Future 对象
    futures = [executor.submit(do_task, i) for i in (1, 2, 3)]
    # 可以等待所有任务完成
    concurrent.futures.wait(futures)
    # 获取结果
    for future in concurrent.futures.as_completed(futures):
        print(future.result())

asyncio 处理高并发 I/O

asyncio 是 Python 的异步 I/O 框架,它使用单线程和事件循环来实现高并发,非常适合处理大量网络连接。

核心概念:

  • 协程:async def 定义的函数。
  • 事件循环: asyncio 的核心,负责运行和调度协程。
  • await: 在协程中使用,用于暂停当前协程的执行,并等待另一个协程完成,同时事件循环可以去执行其他任务。
import asyncio
async def fetch_data(url):
    print(f"Fetching {url}...")
    await asyncio.sleep(2) # 模拟网络 I/O 延迟
    return f"Data from {url}"
async def main():
    urls = ['url1', 'url2', 'url3']
    tasks = [fetch_data(url) for url in urls]
    # 并发执行所有任务
    results = await asyncio.gather(*tasks)
    for result in results:
        print(result)
# 运行事件循环
asyncio.run(main())

元编程

__enter____exit__ 实现上下文管理器

上下文管理器(通常与 with 语句一起使用)可以确保资源的正确获取和释放,即使发生异常也不例外。

  • 手动实现:

    class ManagedFile:
        def __init__(self, filename):
            self.filename = filename
        def __enter__(self):
            print("Opening file...")
            self.file = open(self.filename, 'w')
            return self.file # 返回给 as 关键字
        def __exit__(self, exc_type, exc_val, exc_tb):
            if self.file:
                print("Closing file...")
                self.file.close()
            # 如果返回 True,则会抑制异常
            return False 
    with ManagedFile('hello.txt') as f:
        f.write('Hello, world!')
  • 推荐: 使用 contextlib.contextmanager 装饰器,它可以用生成器更简单地实现上下文管理器。

    from contextlib import contextmanager
    @contextmanager
    def managed_file(filename):
        print("Opening file...")
        f = open(filename, 'w')
        try:
            yield f # yield 的值会赋给 as 关键字
        finally:
            print("Closing file...")
            f.close()
    with managed_file('hello.txt') as f:
        f.write('Hello, world!')

《Effective Python》的核心思想是:

  1. 优先选择 Pythonic 的写法:列表推导、enumeratezip 等,它们更清晰、更高效。
  2. 警惕语言的“陷阱”:如可变默认参数、__eq____hash__ 的关系。
  3. 拥抱 Python 的动态特性:利用 *args, **kwargs, @property, 描述符等写出灵活的代码。
  4. 为正确性、健壮性和可维护性而设计:使用上下文管理器、考虑并发模型、编写清晰的文档字符串。
  5. 性能优化要适度:先保证代码清晰正确,再用性能分析工具找到瓶颈,然后进行针对性优化(如 __slots__、生成器表达式)。

这本书的价值在于它不仅告诉你“怎么做”,更解释了“为什么这么做”,帮助你真正理解 Python 的设计哲学和最佳实践。

分享:
扫描分享到社交APP
上一篇
下一篇