Python 编程技巧

在开发解决方案时,我们倾向于将复杂的实际问题提炼为更小、更易于管理的子问题,然后使用函数来解决这些问题。函数是冗余代码的克星,也是我们抵御代码复杂性的最强防线。

大多数函数在编写过程中,关键是其返回值。函数产生结果的方式会极大地影响用户调用函数时的体验。掌握设计优雅返回结果的函数的艺术是制作高质量函数的基础。

不要返回多种类型

Python 是如此的灵活,以至于我们可以很容易地做到在其他语言中很难做到的事情。例如:让函数同时返回不同类型的结果。就像下面这样:

def get_users(user_id=None):
    if user_id is not None:
        return User.get(user_id)
    else:
        return User.filter(is_active=True)


# Return single user
get_users(user_id=1)
# Return all users
get_users()

在上面的代码片段中,当我们需要获取单个用户时,我们传递一个 "user_id " 参数,否则,如果我们传递一个 None 值,它将返回所有活跃用户的列表。乍一看,这种设计似乎很合理。

但是,编写类似功能强大的函数并不是一件好事。这是因为优秀的函数必须具有 单一职责。所谓 “单一职责”,是指一个函数只做好一件事,而且目的明确。这样的函数将来也不太可能随着需求的变化而修改,同时也非常方便编写单元测试。

返回多种类型的函数违反了 "单一职责 "原则。一个好的函数应始终提供一个稳定的返回值,以尽量减少调用者的处理成本。就像上面的例子,我们应该编写两个独立的函数 get_active_users()get_user_by_id(user_id)

使用类型提示定义返回类型

使用类型提示定义返回类型和显式参数声明。这样,集成开发环境就能帮助您进行自动补全和类型检查,从而在编辑的时候就发现错误。

例如:

def say_hello(name: str) -> str:
    return "Hello, " + name

-> 语法表示 say_hello() 函数将返回一个字符串。

使用部分函数构造新函数

假设在这种情况下,您的代码中有一个带有很多参数的函数 A,它非常适用。另一个函数 B 调用 A 做一些工作,就像下面这样:

def add(x, y):
    return x + y


def sum(value):
    # Calling add
    return add(100, value)

在上述示例中,我们可以使用 functools 模块中的 partial() 函数来简化它。

import functools

sum = functools.partial(add, 100)
sum(200)  # Output is 300

partial(func,*args,**kwargs) 以传入的函数为基础,使用变量参数构造一个新函数。在合并当前调用参数和构造参数后,对新函数的所有调用都将委托给原始函数。

因此,在使用部分函数时,可以将上面的求和函数定义修改为单行表达式,这样会更加简洁和直接。

抛出异常而不是返回错误

有时,您可能需要编写同时返回结果和错误信息的函数:

def create_user(name):
    if len(name) > MAX_LENGTH_OF_NAME:
        return None, 'name of user is too long'
    if len(CURRENT_USERS) > MAX_USERS_QUOTA:
        return None, 'too many users'
    return User(name=name), ''def create_from_input():
    name = input()
    user, err_msg = create_user(name)
    if err_msg:
        print(f'create user failed: {err_msg}')
    else:
        print(f'user<{name}> created')

在上例中,create_user 函数的作用是创建一个新的用户对象。同时,为了在发生错误时向调用者提供错误详细信息,它利用了多返回值特性,将错误信息作为第二个结果返回。

但在 Python 中,这并不是解决此类问题的最佳方法。因为这种做法会增加调用者处理错误的成本,尤其是当许多函数都遵循这种规范,并且存在多层调用时。

在这种情况下,使用异常来处理错误过程是更习以为常的做法。因此,上述代码可以重写为:

class CreateUserError(Exception):
    """Exception for user creation failure"""
    pass

def create_user(name):
    """Create new user

    :raises: CreateUserError
    """
    if len(name) > MAX_LENGTH_OF_NAME:
        raise CreateUserError('name of user is too long')
    if len(CURRENT_USERS) > MAX_USERS_QUOTA:
        raise CreateUserError('Too many users')
    return User(name=name)


def create_for_input():
    name = input()
    try:
        user = create_user(name)
    except CreateUserError as e:
        print(f'create user failed: {e}')
    else:
        print(f'user<{name}> created')

在使用抛出异常而不是返回结果、错误信息后,整个错误处理过程乍一看变化不大,但实际上在一些细节上有很大不同:

  • 新版函数的返回值类型更加稳定,它将始终只返回用户类型或抛出异常。
  • 异常与返回值的不同之处在于,异常在被捕获之前会不断向调用栈的上层报告。因此,create_user 的一级调用者可以完全省略异常处理,将其留给上层处理。

尽量少返回 None

None 常被用来表示应该存在但缺少的东西,在 Python 中是独一无二的。由于 None 独特的虚无主义特质,它经常被用作函数返回值。

当我们使用 None 作为函数的返回值时,通常有以下三种情况。

  • 作为操作类函数的默认返回值:当操作类函数不需要任何返回值时,通常会返回 None。此外,对于没有任何返回语句的函数,None 也是默认返回值。例如,list.append()
  • 作为某个可能不存在的预期值:在 Python 标准库中,正则表达式模块下的函数 re 就属于这一类。例如,re.searchre.match
  • 作为代表错误结果的值:有时,当函数调用失败时,我们经常使用 None 作为默认返回值。如果是这种情况,请确保您的函数名称更有意义,例如 create_user_or_none()

限制递归的使用

当函数返回调用自身时,这就是递归。递归在某些情况下是非常有用的编程技巧,但 Python 对递归的支持非常有限。

Python 语言不支持尾部递归优化。此外,Python 对递归级别的最大数量也有严格限制。所以要尽可能少写递归。如果您想用递归来解决问题,首先要考虑是否可以用循环来轻松替代递归。如果答案是肯定的,那就用循环重写。如果绝对必须使用递归,请考虑以下几点:

  • 确保递归层小于 sys.getrecursionlimit()
  • 尽可能使用缓存,例如 functools.lru_cache

你可能感兴趣的:(python,程序人生)