Python高质量函数编写指南

简介: Python高质量函数编写指南

The Ultimate Guide to Writing Functions

1.视频 https://www.youtube.com/watch?v=yatgY4NpZXE

2.代码 https://github.com/ArjanCodes/2022-funcguide

Python高质量函数编写指南

1. 一次做好一件事

from dataclasses import dataclass
from datetime import datetime


@dataclass
class Customer:
    name: str
    phone: str
    cc_number: str
    cc_exp_month: int
    cc_exp_year: int
    cc_valid: bool = False

# validate_card函数做了太多事情
def validate_card(customer: Customer) -> bool:
    def digits_of(number: str) -> list[int]:
        return [int(d) for d in number]

    digits = digits_of(customer.cc_number)
    odd_digits = digits[-1::-2]
    even_digits = digits[-2::-2]
    checksum = 0
    checksum += sum(odd_digits)
    for digit in even_digits:
        checksum += sum(digits_of(str(digit * 2)))

    customer.cc_valid = (
        checksum % 10 == 0
        and datetime(customer.cc_exp_year, customer.cc_exp_month, 1) > datetime.now()
    )
    return customer.cc_valid


def main() -> None:
    alice = Customer(
        name="Alice",
        phone="2341",
        cc_number="1249190007575069",
        cc_exp_month=1,
        cc_exp_year=2024,
    )
    is_valid = validate_card(alice)
    print(f"Is Alice's card valid? {is_valid}")
    print(alice)


if __name__ == "__main__":
    main()

我们发现validate_card函数做了两件事:验证数字和有效、验证时间有效。

我们把验证数字和拆分出来一个函数luhn_checksum, 并在validate_card中调用。

修改后:

from dataclasses import dataclass
from datetime import datetime

# 验证和 函数
def luhn_checksum(card_number: str) -> bool:
    def digits_of(number: str) -> list[int]:
        return [int(d) for d in number]

    digits = digits_of(card_number)
    odd_digits = digits[-1::-2]
    even_digits = digits[-2::-2]
    checksum = 0
    checksum += sum(odd_digits)
    for digit in even_digits:
        checksum += sum(digits_of(str(digit * 2)))
    return checksum % 10 == 0


@dataclass
class Customer:
    name: str
    phone: str
    ...


def validate_card(customer: Customer) -> bool:
    customer.cc_valid = (
        luhn_checksum(customer.cc_number)
        and datetime(customer.cc_exp_year, customer.cc_exp_month, 1) > datetime.now()
    )
    return customer.cc_valid


2. 分离命令和查询(command and query)

validate_card 中同时进行了查询赋值两个操作,这样不好。

我们将查询和赋值拆分成两个步骤。

validate_card只返回卡是否有效,而赋值操作alice.cc_valid = validate_card(alice) 移动到了主函数中。

from dataclasses import dataclass
from datetime import datetime


def luhn_checksum(card_number: str) -> bool:
    def digits_of(number: str) -> list[int]:
        return [int(d) for d in number]

    digits = digits_of(card_number)
    odd_digits = digits[-1::-2]
    even_digits = digits[-2::-2]
    checksum = 0
    checksum += sum(odd_digits)
    for digit in even_digits:
        checksum += sum(digits_of(str(digit * 2)))
    return checksum % 10 == 0


@dataclass
class Customer:
    name: str
    phone: str
    cc_number: str
    cc_exp_month: int
    cc_exp_year: int
    cc_valid: bool = False

# 查询
def validate_card(customer: Customer) -> bool:
    return (
        luhn_checksum(customer.cc_number)
        and datetime(customer.cc_exp_year, customer.cc_exp_month, 1) > datetime.now()
    )


def main() -> None:
    alice = Customer(
        name="Alice",
        phone="2341",
        cc_number="1249190007575069",
        cc_exp_month=1,
        cc_exp_year=2024,
    )
    # 赋值
    alice.cc_valid = validate_card(alice) 
    print(f"Is Alice's card valid? {alice.cc_valid}")
    print(alice)


if __name__ == "__main__":
    main()

3. 只请求你需要的

函数validate_card实际上只需要3个参数(而不需要整个Customer对象)。

因此只请求3个参数:def validate_card(*, number: str, exp_month: int, exp_year: int) -> bool:

``

from dataclasses import dataclass
from datetime import datetime


def luhn_checksum(card_number: str) -> bool:
    def digits_of(number: str) -> list[int]:
        return [int(d) for d in number]

    digits = digits_of(card_number)
    odd_digits = digits[-1::-2]
    even_digits = digits[-2::-2]
    checksum = 0
    checksum += sum(odd_digits)
    for digit in even_digits:
        checksum += sum(digits_of(str(digit * 2)))
    return checksum % 10 == 0


@dataclass
class Customer:
    name: str
    phone: str
    cc_number: str
    cc_exp_month: int
    cc_exp_year: int
    cc_valid: bool = False

# 只请求你需要的参数
def validate_card(*, number: str, exp_month: int, exp_year: int) -> bool:
    return luhn_checksum(number) and datetime(exp_year, exp_month, 1) > datetime.now()


def main() -> None:
    alice = Customer(
        name="Alice",
        phone="2341",
        cc_number="1249190007575069",
        cc_exp_month=1,
        cc_exp_year=2024,
    )
    alice.cc_valid = validate_card(
        number=alice.cc_number,
        exp_month=alice.cc_exp_month,
        exp_year=alice.cc_exp_year,
    )
    print(f"Is Alice's card valid? {alice.cc_valid}")
    print(alice)


if __name__ == "__main__":
    main()

4. 保持最小参数量

参数量很多时,调用时传参会比较麻烦。另一方面,函数需要很多参数,则暗示该函数可能做了很多事情。

下面我们抽象出Card 类, 减少了Customervalidae_card的参数量。

from dataclasses import dataclass
from datetime import datetime
from typing import Protocol


def luhn_checksum(card_number: str) -> bool:
    def digits_of(number: str) -> list[int]:
        return [int(d) for d in number]

    digits = digits_of(card_number)
    odd_digits = digits[-1::-2]
    even_digits = digits[-2::-2]
    checksum = 0
    checksum += sum(odd_digits)
    for digit in even_digits:
        checksum += sum(digits_of(str(digit * 2)))
    return checksum % 10 == 0


@dataclass
class Card:
    number: str
    exp_month: int
    exp_year: int
    valid: bool = False


@dataclass
class Customer:
    name: str
    phone: str
    card: Card
    card_valid: bool = False


class CardInfo(Protocol):
    @property
    def number(self) -> str:
        ...

    @property
    def exp_month(self) -> int:
        ...

    @property
    def exp_year(self) -> int:
        ...


def validate_card(card: CardInfo) -> bool:
    return (
        luhn_checksum(card.number)
        and datetime(card.exp_year, card.exp_month, 1) > datetime.now()
    )


def main() -> None:
    card = Card(number="1249190007575069", exp_month=1, exp_year=2024)
    alice = Customer(name="Alice", phone="2341", card=card)  # 现在传入card,而不是3个参数
    card.valid = validate_card(card) # 传入card
    print(f"Is Alice's card valid? {card.valid}")
    print(alice)


if __name__ == "__main__":
    main()

5. 不要在同一个地方创建并使用对象

不要再函数内创建对象并使用,更好的方式是在外面创建对象并作为参数传递给函数。

import logging


class StripePaymentHandler:
    def handle_payment(self, amount: int) -> None:
        logging.info(f"Charging ${amount/100:.2f} using Stripe")


PRICES = {
    "burger": 10_00,
    "fries": 5_00,
    "drink": 2_00,
    "salad": 15_00,
}

# !!
def order_food(items: list[str]) -> None:
    total = sum(PRICES[item] for item in items)
    logging.info(f"Order total is ${total/100:.2f}.")
    payment_handler = StripePaymentHandler() # ... 创建对象
    payment_handler.handle_payment(total)  # 使用对象
    logging.info("Order completed.")


def main() -> None:
    logging.basicConfig(level=logging.INFO)
    order_food(["burger", "fries", "drink"])


if __name__ == "__main__":
    main()


修改后:

import logging
from typing import Protocol


class StripePaymentHandler:
    def handle_payment(self, amount: int) -> None:
        logging.info(f"Charging ${amount/100:.2f} using Stripe")


PRICES = {
    "burger": 10_00,
    "fries": 5_00,
    "drink": 2_00,
    "salad": 15_00,
}


class PaymentHandler(Protocol):
    def handle_payment(self, amount: int) -> None:
        ...

# !!  现在通过参数传入对象
def order_food(items: list[str], payment_handler: PaymentHandler) -> None:
    total = sum(PRICES[item] for item in items)
    logging.info(f"Order total is ${total/100:.2f}.")
    payment_handler.handle_payment(total) # 
    logging.info("Order completed.")


def main() -> None:
    logging.basicConfig(level=logging.INFO)
    order_food(["burger", "salad", "drink"], StripePaymentHandler())


if __name__ == "__main__":
    main()


6. 不要用flag参数

flag参数意味着函数处理两种情况,函数会变得复杂。建议将两者情况拆分成单独的函数。

from dataclasses import dataclass
from enum import StrEnum, auto

FIXED_VACATION_DAYS_PAYOUT = 5


class Role(StrEnum):
    PRESIDENT = auto()
    VICEPRESIDENT = auto()
    MANAGER = auto()
    LEAD = auto()
    ENGINEER = auto()
    INTERN = auto()


@dataclass
class Employee:
    name: str
    role: Role
    vacation_days: int = 25

    def take_a_holiday(self, payout: bool, nr_days: int = 1) -> None:
        if payout:
            if self.vacation_days < FIXED_VACATION_DAYS_PAYOUT:
                raise ValueError(
                    f"You don't have enough holidays left over for a payout.\
                        Remaining holidays: {self.vacation_days}."
                )
            self.vacation_days -= FIXED_VACATION_DAYS_PAYOUT
            print(f"Paying out a holiday. Holidays left: {self.vacation_days}")
        else:
            if self.vacation_days < nr_days:
                raise ValueError(
                    "You don't have any holidays left. Now back to work, you!"
                )
            self.vacation_days -= nr_days
            print("Have fun on your holiday. Don't forget to check your emails!")


def main() -> None:
    employee = Employee(name="John Doe", role=Role.ENGINEER)
    employee.take_a_holiday(True)


if __name__ == "__main__":
    main()

修改后:

from dataclasses import dataclass
from enum import StrEnum, auto

FIXED_VACATION_DAYS_PAYOUT = 5


class Role(StrEnum):
    PRESIDENT = auto()
    VICEPRESIDENT = auto()
    MANAGER = auto()
    LEAD = auto()
    ENGINEER = auto()
    INTERN = auto()


@dataclass
class Employee:
    name: str
    role: Role
    vacation_days: int = 25

    def payout_holiday(self) -> None:
        if self.vacation_days < FIXED_VACATION_DAYS_PAYOUT:
            raise ValueError(
                f"You don't have enough holidays left over for a payout.\
                    Remaining holidays: {self.vacation_days}."
            )
        self.vacation_days -= FIXED_VACATION_DAYS_PAYOUT
        print(f"Paying out a holiday. Holidays left: {self.vacation_days}")

    def take_holiday(self, nr_days: int = 1) -> None:
        if self.vacation_days < nr_days:
            raise ValueError("You don't have any holidays left. Now back to work, you!")
        self.vacation_days -= nr_days
        print("Have fun on your holiday. Don't forget to check your emails!")


def main() -> None:
    employee = Employee(name="John Doe", role=Role.ENGINEER)
    employee.payout_holiday()


if __name__ == "__main__":
    main()

7. 函数也是对象

函数也是对象,因此可以作为参数传递,作为函数返回值。

import logging
from functools import partial
from typing import Callable


def handle_payment_stripe(amount: int) -> None:
    logging.info(f"Charging ${amount/100:.2f} using Stripe")


PRICES = {
    "burger": 10_00,
    "fries": 5_00,
    "drink": 2_00,
    "salad": 15_00,
}


HandlePaymentFn = Callable[[int], None]

# 函数作为参数
def order_food(items: list[str], payment_handler: HandlePaymentFn) -> None:
    total = sum(PRICES[item] for item in items)
    logging.info(f"Order total is ${total/100:.2f}.")
    payment_handler(total)
    logging.info("Order completed.")


order_food_stripe = partial(order_food, payment_handler=handle_payment_stripe)


def main() -> None:
    logging.basicConfig(level=logging.INFO)
    # order_food(["burger", "salad", "drink"], handle_payment_stripe)
    order_food_stripe(["burger", "salad", "drink"])


if __name__ == "__main__":
    main()


相关文章
|
15天前
|
缓存 监控 程序员
Python中的装饰器是一种特殊类型的声明,它允许程序员在不修改原有函数或类代码的基础上,通过在函数定义前添加额外的逻辑来增强或修改其行为。
【6月更文挑战第30天】Python装饰器是无侵入性地增强函数行为的工具,它们是接收函数并返回新函数的可调用对象。通过`@decorator`语法,可以在不修改原函数代码的情况下,添加如日志、性能监控等功能。装饰器促进代码复用、模块化,并保持源代码整洁。例如,`timer_decorator`能测量函数运行时间,展示其灵活性。
18 0
|
3天前
|
索引 Python
python操作符或函数与数据类型不兼容
【7月更文挑战第11天】
9 1
|
6天前
|
开发者 Python
Python函数参数定义中的这两个分隔符,还有人不知道吗?
python 函数的参数定义想必大家应该是非常熟悉的,有两种: • 位置参数(positional argument):根据函数在参数列表中的位置传递给函数的参数。 • 关键词参数(keyword argument):通过指定参数名称及其对应值传参的参数。
|
10天前
|
Python
python解包字典到函数参数
【7月更文挑战第5天】
12 2
|
1天前
|
缓存 测试技术 Python
Python中的装饰器:优雅地增强函数功能
在Python编程中,装饰器是一种强大的工具,它能够在不改变函数本身的情况下,动态地增强其功能。本文将深入探讨装饰器的工作原理、常见用法以及如何利用装饰器提高代码的可重用性和可维护性。
|
1天前
|
资源调度 计算机视觉 Python
`scipy.ndimage`是SciPy库中的一个子模块,它提供了许多用于处理n维数组(通常是图像)的函数。
`scipy.ndimage`是SciPy库中的一个子模块,它提供了许多用于处理n维数组(通常是图像)的函数。
7 0
|
1天前
|
Python
`scipy.signal`模块是SciPy库中的一个子模块,它提供了信号处理、滤波、频谱分析等功能。这个模块包含了许多用于信号处理的函数和类,其中`butter()`和`filtfilt()`是两个常用的函数。
`scipy.signal`模块是SciPy库中的一个子模块,它提供了信号处理、滤波、频谱分析等功能。这个模块包含了许多用于信号处理的函数和类,其中`butter()`和`filtfilt()`是两个常用的函数。
10 0
|
1天前
|
Python
`matplotlib`是Python中一个非常流行的绘图库,它提供了丰富的绘图接口,包括二维和三维图形的绘制。`Axes3D`是`matplotlib`中用于创建三维坐标轴的对象,而`plot_surface`则是用于在三维空间中绘制表面的函数。
`matplotlib`是Python中一个非常流行的绘图库,它提供了丰富的绘图接口,包括二维和三维图形的绘制。`Axes3D`是`matplotlib`中用于创建三维坐标轴的对象,而`plot_surface`则是用于在三维空间中绘制表面的函数。
10 0
|
1天前
|
SQL Java C++
Python代码示例简单的print()函数使用
Python代码示例简单的print()函数使用
4 0
|
1天前
|
存储 Python
`input()` 函数是 Python 中的一个内置函数,用于从用户那里获取输入。
`input()` 函数是 Python 中的一个内置函数,用于从用户那里获取输入。
4 0