Vanson's Eternal Blog

Python中如何编写出优雅代码的技巧(上篇)

Elegant code in python.jpg
Published on
/19 mins read/---

写出优雅代码能提升可读性、可维护性和开发效率,减少错误,便于协作和扩展,是高质量软件开发的关键。

f-string格式化字符串

基本用法

在字符串前加上f或F,然后在字符串中用大括号包裹变量或表达式。

 
name = "Alice"
age = 30
print(f"My name is {name} and I am {age} years old.")
 

表达式

 
a = 5
b = 10
print(f"The sum of {a} and {b} is {a + b}.")
 

格式化数字

例如指定小数点后的位数、添加千位分隔符等。

 
pi = 3.141592653589793
print(f"Pi is approximately {pi:.2f}.")  # 保留两位小数
print(f"Pi is approximately {pi:.5f}.")  # 保留五位小数
 

格式化日期

可以与datetime模块结合,格式化日期和时间。

 
from datetime import datetime
 
now = datetime.now()
print(f"The current time is {now:%Y-%m-%d %H:%M:%S}.")
 

格式化字典

可以直接访问字典中的键值。

 
person = {"name": "Alice", "age": 30}
print(f"{person['name']} is {person['age']} years old.")
 

格式化对象属性

可以直接访问对象的属性。

 
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
 
person = Person("Alice", 30)
print(f"{person.name} is {person.age} years old.")
 
 

格式化列表和元组

直接访问列表和元组中的元素。

 
numbers = [1, 2, 3, 4, 5]
print(f"The first number is {numbers[0]} and the last number is {numbers[-1]}.")
 

格式化字符串中的占位符

f-string支持在字符串中使用占位符,占位符可以用变量名或索引。

 
name = "Alice"
age = 30
print(f"{name=}, {age=}.")
 
 

格式化字符串中的嵌套表达式

f-string支持嵌套表达式,可以在大括号中嵌套多个表达式。

 
a = 5
b = 10
print(f"The product of {a} and {b} is {(a * b):.2f}.")
 

格式化字符串中的循环

f-string可以与循环结合,动态生成字符串。

 
numbers = [1, 2, 3, 4, 5]
print(f"Numbers: {', '.join(str(n) for n in numbers)}")
 

格式化字符串中的多行

f-string可以用于多行字符串,通过三引号 """'''定义。

 
name = "Alice"
age = 30
city = "New York"
print(f"""
My name is {name}.
I am {age} years old.
I live in {city}.
""")
 

格式化字符串中的转义

在f-string中,如果需要在字符串中包含大括号{},需要使用双大括号{{}}来转义。

 
print(f"{{name}} is a placeholder for the variable name.")
 

交换两个变量的值

多重赋值语法,无需临时变量即可交换两个变量的值。

a, b = b, a

 
# 初始值
a = 1
b = 2
c = 3
 
print(f"交换前: a={a}, b={b}, c={c}")
 
# 交换值
a, b, c = b, c, a
 
print(f"交换后: a={a}, b={b}, c={c}")
 

我们的目标是将 a 的值变为 2,b 的值变为 3,c 的值变为 1。

逻辑顺序:为了实现这个目标,我们需要按照以下顺序进行赋值:

  • a 应该接收 b 的值。
  • b 应该接收 c 的值。
  • c 应该接收 a 的值。

让字符串重复

快速生成多个相同的字符串,例如打印 5 个 "Hello",你可能会这样写:

result = "Hello " * 5

简化条件判断

# 青铜段位
num = 10
if num % 2 == 0:
    result = "偶数"
else:
    result = "奇数"
 
 
# 钻石段位
result = "偶数" if num % 2 == 0 else "奇数"
 

列表拼接成字符串

# 假设你有一组邮箱地址的列表,现在你想把它们拼接成一条 , 分隔的字符串:
 
emails = ["a@example.com", "b@example.com", "c@example.com"]
result = ", ".join(emails)
print(result)
 

字典取值

操作 Python 字典时,如果你尝试访问一个不存在的键,Python 会抛出 KeyError,但你可以使用 .get() 方法安全获取值:

 
data = {"name": "Alice", "age": 25}
age = data.get("age", "默认值")  # 如果 'age' 存在,则返回其值,否则返回 '默认值'
print(age)  # 输出 25
 

统计列表中元素出现的次数

擅长于用 collections 集合类。Java里边也多是用集合类操作的。

假设你有一个列表,想知道其中某个元素出现的次数,最简单的方法是使用 collections.Counter

from collections import Counter
 
letters = ["a", "b", "c", "a", "b", "a"]
counter = Counter(letters)
 
print(counter)  # 输出 {'a': 3, 'b': 2, 'c': 1}
print(counter.most_common(1))  # 获取出现次数最多的元素 [('a', 3)]
 

列表去重

通过将列表转换为集合(set)来去除重复元素,再将集合转换回列表。set是一个无序集合,自动去除重复元素,因此这行代码可以快速实现列表去重。

lst = list(set(lst))

计算列表中数字的平均值

sum()函数计算列表中所有数字的总和,再用len()函数获取列表的长度,从而计算平均值。这种方法简洁高效。

average = sum(lst) / len(lst)
 

倒序排列字符串

通过[::-1]将字符串s倒序排列。切片操作是Python中处理字符串和列表的常用技巧,简洁且高效。

 
reversed_str = s[::-1]
 

获取列表中的最大和最小值

利用max()和min()函数,分别获取列表lst中的最大值和最小值。Python的这些内建函数使得这类操作变得极其简便。

 
max_val, min_val = max(lst), min(lst)
 

判断一个数是否为质数

使用了all()函数结合列表推导式,快速判断一个数n是否为质数。它通过检查n是否能被2到sqrt(n)之间的任何数整除来判断。

 
is_prime = all(n % i for i in range(2, int(n**0.5) + 1))
 

生成一个包含1到10的数字列表

利用range()函数生成一个从1到10的数字序列,再用list()将其转换为列表。这种方法非常适合生成序列数据。

 
numbers = list(range(1, 11))
 

找出字符串中的所有数字

利用列表推导式结合isdigit()方法,从字符串s中提取所有的数字,并将它们作为整数返回。

 
digits = [int(i) for i in s if i.isdigit()]
 

检查一个字符串是否是回文

通过字符串切片,判断字符串s是否与其倒序版本相同,从而判断是否为回文字符串。这种方法简洁且高效。

切片操作的基本语法是 s[start:stop:step],其中:

  • start 是切片的起始索引(包含)。
  • stop 是切片的结束索引(不包含)。
  • step 是步长,表示每次取值的间隔。
 
is_palindrome = s == s[::-1]
 

获取文件的扩展名

os.path.splitext()

os.path.splitext()函数将路径分割成两部分:文件名和扩展名。

返回值是一个元组,其中第一个元素是文件名(不包括扩展名),第二个元素是扩展名。

 
import os
 
# 示例文件名
filename = "example.txt"
 
# 使用os.path.splitext()获取扩展名
_, ext = os.path.splitext(filename)
 
print(f"文件扩展名是: {ext}")
 

pathlib()

pathlib模块也提供了更现代的方式来处理路径。

pathlib模块中的Path对象有一个suffix属性,可以直接获取文件的扩展名。

 
 

给 enumerate() 设置起始值

我们在遍历列表时会使用 enumerate(),默认情况下,它从 0 开始计数:

# 想让索引从 1 开始,可以这样做:
 
for index, name in enumerate(names, start=1):
    print(index, name)
 

合并两个字典

dict1 = {"a": 1, "b": 2}
dict2 = {"c": 3, "d": 4}
 
# 方法一
merged = {**dict1, **dict2}
 
# 方法二:Python 3.9 以后,你可以用 | 运算符更简洁地合并字典
merged = dict1 | dict2
 

让大数字更易读

如果你需要在代码中写入一个大数字,

比如 1000000,可能会因为数零太多而感到困惑。Python 允许你使用 下划线 _ 作为分隔符,让数字更易读:

big_number = 1_000_000  # 这个和 1000000 是一样的
print(big_number)  # 输出 1000000
 
# 使用格式化字符串来让输出更直观:
print(f"{big_number:_}")  # 输出 1_000_000
 

让类像函数一样调用

创建可调用的类,这样类的实例可以像函数一样使用。这是通过 __call__ 方法实现的:

 
class Multiplier:
    def __init__(self, factor):
        self.factor = factor
 
    def __call__(self, value):
        return self.factor * value
 
double = Multiplier(2)  # 创建一个倍数器
print(double(10))  # 输出 20
 

方法链式调用

一个类有多个方法,你需要连续调用它们。这时,我们可以返回 self,从而支持链式调用:

 
class Person:
    def__init__(self, name, age):
        self.name = name
        self.age = age
 
    defset_name(self, new_name):
        self.name = new_name
        returnself# 关键:返回 self 以支持链式调用
 
    defset_age(self, new_age):
        self.age = new_age
        returnself# 关键:返回 self
 
bob = Person("Bob", 25)
bob.set_name("Alice").set_age(30)  # 一行代码完成两次修改
 
print(bob.name, bob.age)  # 输出:Alice 30
 

让列表输出更美观

# 有一个列表,想要以更直观的方式打印出来,你可以使用 * 运算符结合 print():
 
foods = ["苹果", "香蕉", "橘子"]
print(*foods, sep=", ")  # 输出:苹果, 香蕉, 橘子
 

让对象打印更可读

如果直接打印一个对象,默认会返回类似 <__main__.Person object at 0x7ff6d8e1f9d0> 这样的信息,根本没法看懂。

我们可以定义 __repr__ 方法,让它返回更有意义的字符串:

 
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
 
    def __repr__(self):
        return f"Person(name={self.name}, age={self.age})"
 
bob = Person("Bob", 25)
print(bob)  # 输出:Person(name=Bob, age=25)
 

一行代码获取列表的首尾元素

假设你有一个列表 people = ["Bob", "Alice", "Charlie", "David"],想要获取第一个和最后一个元素,你可以这样做:

 
# *middle 会捕获中间所有的元素,而 first 和 last 则分别获取开头和结尾的元素。
first, *middle, last = people
print(first, last)  # 输出:Bob David
 

数字四舍五入

 
# 保留小数位数
print(round(3.14159, 2))  # 输出:3.14
 
# -2 表示保留到百位,类似于“银行家舍入法”,让数据更加易读。
print(round(12345, -2))  # 输出:12300
 

按自定义规则找出最值

Python 内置的 max() 和 min() 支持自定义规则。

# 例如,找出字母 a 出现最多的名字
names = ["Timothy", "Bob", "James", "Zebra", "Amanda", "Anna", "Luigi"]
most_a_name = max(names, key=lambda x: x.lower().count("a"))
print(most_a_name)  # 输出:Amanda
 
# 还可以按字符串长度查找最短和最长的名字:
shortest_name = min(names, key=len)
longest_name = max(names, key=len)
print(shortest_name, longest_name)  # 输出:Bob Timothy
 

解包操作:优雅地处理序列

解包(unpacking)是一个非常强大的特性。它让我们能够用一行代码就完成多个变量的赋值操作。

 
# 基础解包
x, y = 1, 2  
# x=1, y=2
 
# 交换变量值(无需临时变量)
a, b = b, a  
 
# 解包列表/元组
first, *rest = [1, 2, 3, 4, 5]  # first=1, rest=[2,3,4,5]
*begin, last = [1, 2, 3, 4, 5]  # begin=[1,2,3,4], last=5
 
# 忽略某些值
name, _, age = ['Alice', 'Smith', 25]  # 忽略中间值
 

上下文管理器

使用上下文管理器可以确保资源使用更加安全可靠。

 
# 自定义一个简单的上下文管理器
class Timer:
    def __enter__self):
        self.start = time.time()
        return self
    
    def __exit__self*args):
        self.end = time.time()
        print(f"代码块执行时间:{self.end - self.start}秒")
 
with Timer():
    # 模拟耗时操作
    time.sleep(1
 

*args**kwargs

*args

用于接收任意数量的位置参数(非关键字参数)。这些参数会被收集到一个元组中。 语法:在函数定义中,*args 放在参数列表的最后,用于接收所有未被其他参数捕获的位置参数。 用途:当你不确定函数会接收多少个位置参数时,可以使用 *args

def print_args(*args):
    for arg in args:
        print(arg)
 
print_args(1, 2, 3, "hello", [1, 2, 3])
 
###### 输出 ######
# 1
# 2
# 3
# hello
# [1, 2, 3]

**kwargs

用于接收任意数量的关键字参数。这些参数会被收集到一个字典中。 语法:在函数定义中,**kwargs 放在参数列表的最后,用于接收所有未被其他参数捕获的关键字参数。 用途:当你不确定函数会接收多少个关键字参数时,可以使用 **kwargs

 
def print_kwargs(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")
 
print_kwargs(name="小明", age=18, city="北京")
 
###### 输出 ######
# 位置参数: (1, 2, 3)
# 关键字参数: {'name': '小明', 'age': 18}
 

顺序:*args 必须放在 **kwargs 之前。 解包:你可以在调用函数时使用 *** 来解包元组和字典,将它们作为参数传递。

 
args = (1, 2, 3)
kwargs = {"name": "小明", "age": 18}
 
print_info(*args, **kwargs)
 

赋值表达式

海象运算符(:=)是Python 3.8引入的一个新特性,也被称为“赋值表达式”(assignment expression)。

它允许你在表达式中进行赋值操作,这在条件判断中特别有用,可以减少代码的重复和提高可读性。

# 不使用海象运算符
text = "Hello, Python!"
length = len(text)
if length > 10:
    print(f"文本长度为{length},超过10个字符")
 
# 使用海象运算符
if (length := len(text)) > 10:
    print(f"文本长度为{length},超过10个字符")
 

all 和 any

all(iterable):检查可迭代对象中的所有元素是否都为真。如果所有元素都为真,则返回 True;否则返回 False。

result = all([True, 1, 'non-empty string']) # 返回 True

检查可迭代对象中是否存在至少一个为真的元素。如果存在至少一个为真的元素,则返回 True;否则返回 False。

result = any([False, 0, None, 'exists']) # 返回 True

结合生成器表达式优化性能

在处理大型数据集时,直接使用列表作为参数可能会导致性能问题,因为所有元素需要先被存入内存。

all 和 any 支持生成器表达式,可以以惰性评估的方式逐步计算,从而大幅提升性能。

# 文件行检查
def all_lines_match(file_path, condition):
    with open(file_path, 'r') as file:
        return all(condition(line) for line in file)
 
# 示例条件:行长度必须大于10
result = all_lines_match('large_file.txt', lambda line: len(line) > 10)
print(result)  # 如果所有行都满足条件,返回 True
 

与自定义类结合,实现灵活逻辑

# 权限验证系统
class User:
    def __init__(self, permissions):
        self.permissions = permissions
 
    def has_permission(self, permission):
        return permission in self.permissions
 
def can_user_execute(user, required_permissions):
    return all(user.has_permission(perm) for perm in required_permissions)
 
# 示例用法
user = User(['read', 'write', 'delete'])
permissions_needed = ['read', 'write']
print(can_user_execute(user, permissions_needed))  # True
 

结合逻辑表达式实现动态决策

# 智能筛选系统
def filter_products(products, filters):
    return [
        product for product in products
        if all(condition(product) for condition in filters)
    ]
 
# 示例数据
products = [
    {'name': 'Laptop', 'price': 1000, 'stock': 10},
    {'name': 'Smartphone', 'price': 500, 'stock': 0},
    {'name': 'Tablet', 'price': 300, 'stock': 5},
]
 
# 筛选条件
filters = [
    lambda p: p['price'] < 800,   # 价格小于800
    lambda p: p['stock'] > 0      # 有库存
]
 
result = filter_products(products, filters)
print(result)  # [{'name': 'Tablet', 'price': 300, 'stock': 5}]
 

短路逻辑,避免不必要的计算

all 和 any 都具备短路逻辑特性。当 all 遇到 False 或 any 遇到 True 时,会立即停止后续的评估。

# 深度数据校验
 
data = {'user': {'profile': {'age': 30}}}
 
def is_valid(data):
    return all([
        'user' in data,
        'profile' in data.get('user', {}),
        data['user']['profile'].get('age') > 18
    ])
 
print(is_valid(data))  # True
 

动态生成复杂条件

# 动态表单验证
def validate_form(data, rules):
    return all(rule(data.get(field, None)) for field, rule in rules.items())
 
# 示例规则
rules = {
    'username': lambda x: isinstance(x, str) and len(x) > 3,
    'age': lambda x: isinstance(x, int) and x > 18,
    'email': lambda x: '@' in x if isinstance(x, str) else False
}
 
form_data = {'username': 'Alice', 'age': 25, 'email': 'alice@example.com'}
print(validate_form(form_data, rules))  # True