14 个 Python 高级功能
Python 是世界上应用最广泛的编程语言之一。然而,由于它易于 “实现某些功能”,因此也是最不被重视的编程语言之一。
如果你在 Google 或其他搜索引擎上搜索 Python 的十大高级技巧,你会发现大量的博客或 LinkedIn 文章都在介绍生成器或元组等琐碎(但仍然有用)的东西。
然而,作为一个在过去 12 年中一直在编写 Python 的人,我遇到过很多非常有趣、被低估、独特或(有些人可能会说)“非 Pythonic ”的技巧,它们能真正提升 Python 的性能。
这就是为什么我决定将这些功能中的前 14 个与示例和其他资源一起汇编起来,如果您想更深入地了解其中任何一个的话。
目录
- 1. 打字超载
- 2. 仅关键字参数和仅位置参数
- 3. 未来注释
- 4. 泛型
- 5. 协议
- 6. 上下文管理器
- 7. 结构模式匹配
- 8. Python 插槽
- 9. Python 挑刺
- 10. 高级 f-string 字符串格式化
- 11. 缓存 / lru_cache
- 12. Python 期货
- 13. 代理属性
- 14. 元类
1. 类型重载
@overload
是 Python typing
模块中的一个装饰器,它允许你为同一个函数定义多个签名。每个重载都会告诉类型检查器,当传入特定参数时,应该使用什么类型。
例如,下面的代码规定,如果mode=split
,则只能返回 list[str]
;如果mode=upper
,则只能返回 str
。 Literal
类型也强制模式必须是 split
或 upper
之一)。
from typing import Literal, overload
@overload
def transform(data: str, mode: Literal["split"]) -> list[str]:
...
@overload
def transform(data: str, mode: Literal["upper"]) -> str:
...
def transform(data: str, mode: Literal["split", "upper"]) -> list[str] | str:
if mode == "split":
return data.split()
else:
return data.upper()
split_words = transform("hello world", "split") # Return type is list[str]
split_words[0] # Type checker is happy
upper_words = transform("hello world", "upper") # Return type is str
upper_words.lower() # Type checker is happy
upper_words.append("!") # Cannot access attribute "append" for "str"
重载的作用不仅仅是根据参数改变返回类型!在另一个例子中,我们使用类型重载来确保传递 id
或 username
中的一个,但绝不同时传递两个。
@overload
def get_user(id: int = ..., username: None = None) -> User:
...
@overload
def get_user(id: None = None, username: str = ...) -> User:
...
def get_user(id: int | None = None, username: str | None = None) -> User:
...
get_user(id=1) # Works!
get_user(username="John") # Works!
get_user(id=1, username="John") # No overloads for "get_user" match the provided arguments
...
是一个特殊值,常用于重载,表示参数是可选的,但仍需要一个值。
✨ 快速奖励技巧: 您可能已经看到,Python 还支持字符串字面量。它们有助于断言只有特定的字符串值才能传递给参数,从而为您提供更多的类型安全性。把它们想象成 Enums 的轻量级形式!
def set_color(color: Literal["red", "blue", "green"]) -> None:
...
set_color("red")
set_color("blue")
set_color("green")
set_color("fuchsia") # Argument of type "Literal['fuchsia']" cannot be assigned to parameter "color"
2. 纯关键字参数和纯位置参数
默认情况下,必填参数和可选参数都可以使用位置语法和关键字语法分配。但是,如果您不希望出现这种情况怎么办?只使用关键字的参数和只使用位置参数的参数可以让您控制。
def foo(a, b, /, c, d, *, e, f):
# ^ ^
# Ever seen these before?
...
*
(星号)标记为只包含关键字的参数。*
后面的参数必须作为关键字参数传递。
# KW+POS | KW ONLY
# vv | vv
def foo(a, *, b):
...
# == ALLOWED ==
foo(a=1, b=2) # All keyword
foo(1, b=2) # Half positional, half keyword
# == NOT ALLOWED ==
foo(1, 2) # Cannot use positional for keyword-only parameter
# ^
/
(正斜线)标记只用于位置传递的参数。在 /
之前的参数必须按位置传递,不能用作关键字参数。
# POS ONLY | KW POS
# vv | vv
def bar(a, /, b):
...
# == ALLOWED ==
bar(1, 2) # All positional
bar(1, b=2) # Half positional, half keyword
# == NOT ALLOWED ==
bar(a=1, b=2) # Cannot use keyword for positional-only parameter
# ^
仅关键字参数和仅位置参数尤其有助于 API 开发人员确定参数的使用和传递方式。
3.未来注解
关于 Python 类型的快速历史课:
这与其说是 “Python 特性”,不如说是 Python 类型系统的历史课,以及如果你在生产代码中遇到
from __future__ import annotations
,它的作用是什么。
Python 的类型系统最初只是一个黑客程序。函数注解语法最早是在 Python 3.0 的 PEP 3107 中引入的,纯粹是一种装饰函数的额外方法,没有实际的类型检查功能。
后来,在 Python 3.5 中,通过 PEP 484 添加了适当的类型注解规范,但它们被设计为在绑定/定义时进行评估。这在简单的情况下效果很好,但有一类问题却越来越令人头疼:正向引用。
这意味着前向引用(在类型定义之前使用该类型)需要回退到字符串字面量,使得代码不够优雅,而且更容易出错。
# This won't work
class Foo:
def action(self) -> Foo:
# The `-> Foo` return annotation is evaluated immediately during definition,
# but the class `Foo` is not yet fully defined at that point,
# causing a NameError during type checking.
...
# This is the workaround -> Using string types
class Bar:
def action(self) -> "Bar":
# Workaround with string literals, but ugly and error-prone
...
作为 PEP(Python 增强提案)引入的 PEP 563: 注释的延迟评估》旨在通过改变类型注释的评估时间来解决这个问题。PEP 563 不再在定义时评估注释,而是在后台对类型进行 “字符串化”,并将评估推迟到实际需要时,通常是在静态分析期间。这样就可以在不明确定义字符串字面量的情况下实现更简洁的前向引用,并减少了类型注解的运行时开销。
from __future__ import annotations
class Foo:
def bar(self) -> Foo: # Works now!
...
那么问题出在哪里呢?
对于类型检查程序来说,这种变化在很大程度上是透明的。但由于 PEP 563 是通过在幕后将所有类型都视为字符串来实现这一点的,因此任何依赖于在运行时访问返回类型的东西(如 ORM、序列化库、验证器、依赖注入器等)都会有与新设置的兼容性问题。
这就是为什么即使在最初提议提出的十年后,现代 Python (截至本文写作时为 3.13)仍然依赖于 Python 3.5 中引入的相同的黑客拼凑类型系统。
# ===== Regular Python Typing =====
def foobar() -> int:
return 1
ret_type = foobar.__annotations__.get("return")
ret_type
# Returns: <class 'int'>
new_int = ret_type()
# ===== With Postponed Evaluation =====
from __future__ import annotations
def foobar() -> int:
return 1
ret_type = foobar.__annotations__.get("return")
ret_type
# "int" (str)
new_int = ret_type() # TypeError: 'str' object is not callable
最近,PEP 649 提出了一种通过延迟或 “懒惰 “评估来处理 Python 函数和类注解的新方法。这种方法不像传统方法那样在函数或类定义时对注解进行评估,而是将注解的计算延迟到实际访问时进行。
这是通过将注解表达式编译成一个单独的函数来实现的,该函数存储在一个特殊的 __annotate__
属性中。当第一次访问 __annotations__
属性时,该函数将被调用来计算和缓存注释,使它们在后续访问中随时可用。
# Example code from the PEP 649 proposal
class function:
# __annotations__ on a function object is already a
# "data descriptor" in Python, we're just changing
# what it does
@property
def __annotations__(self):
return self.__annotate__()
# ...
def annotate_foo():
return {'x': int, 'y': MyType, 'return': float}
def foo(x = 3, y = "abc"):
...
foo.__annotate__ = annotate_foo
class MyType:
...
foo_y_annotation = foo.__annotations__['y']
这种延迟评估策略可以解决前向引用和循环依赖等问题,因为注释只在需要时才进行评估。此外,它还通过避免立即计算可能用不到的注解来提高性能,并维护完整的语义信息,支持内省和运行时类型检查工具。
✨ 额外事实:自 Python 3.11 起,Python 现在支持 “Self “类型 (PEP 673),它允许对返回自身类实例的方法进行适当的类型化,从而解决了自指返回类型的这个特殊例子。
from typing import Self
class Foo:
def bar(self) -> Self:
...
4. 泛型
您知道 Python 有泛型吗?事实上,从 Python 3.12 开始,Generics 引入了一种更新、更时尚、更性感的语法。
class KVStore[K: str | int, V]:
def __init__(self) -> None:
self.store: dict[K, V] = {}
def get(self, key: K) -> V:
return self.store[key]
def set(self, key: K, value: V) -> None:
self.store[key] = value
kv = KVStore[str, int]()
kv.set("one", 1)
kv.set("two", 2)
kv.set("three", 3)
Python 3.5 最初通过 TypeVar
语法引入了泛型。然而,Python 3.12 的 PEP 695 对类型注解进行了改进,为泛型、类型别名等提供了本地语法。
# OLD SYNTAX - Python 3.5 to 3.11
from typing import Generic, TypeVar
UnBounded = TypeVar("UnBounded")
Bounded = TypeVar("Bounded", bound=int)
Constrained = TypeVar("Constrained", int, float)
class Foo(Generic[UnBounded, Bounded, Constrained]):
def __init__(self, x: UnBounded, y: Bounded, z: Constrained) -> None:
self.x = x
self.y = y
self.z = z
# NEW SYNTAX - Python 3.12+
class Foo[UnBounded, Bounded: int, Constrained: int | float]:
def __init__(self, x: UnBounded, y: Bounded, z: Constrained) -> None:
self.x = x
self.y = y
self.z = z
这一变更还引入了功能更强大的可变泛型。这意味着你可以为复杂的数据结构和操作设置任意数量的类型参数。
class Tuple[*Ts]:
def __init__(self, *args: *Ts) -> None:
self.values = args
# Works with any number of types!
pair = Tuple[str, int]("hello", 42)
triple = Tuple[str, int, bool]("world", 100, True)
最后,作为 3.12 类型更改的一部分,Python 还为类型别名引入了新的简洁语法!
# OLD SYNTAX - Python 3.5 to 3.9
from typing import NewType
Vector = NewType("Vector", list[float])
# OLD-ish SYNTAX - Python 3.10 to 3.11
from typing import TypeAlias
Vector: TypeAlias = list[float]
# NEW SYNTAX - Python 3.12+
type Vector = list[float]
5.协议
Python 的主要特性之一(同时也是主要抱怨)是它对 Duck Typing 的支持。俗话说
“如果它走路像鸭子,游泳像鸭子,叫起来像鸭子,那么它很可能就是一只鸭子。
然而,这就提出了一个问题: 如何进行鸭子打字?
class Duck:
def quack(self): print('Quack!')
class Person:
def quack(self): print("I'm quacking!")
class Dog:
def bark(self): print('Woof!')
def run_quack(obj):
obj.quack()
run_quack(Duck()) # Works!
run_quack(Person()) # Works!
run_quack(Dog()) # Fails with AttributeError
这就是协议的作用。协议(也称为结构子类型)是 Python 中的类型类,它定义了类可以遵循的结构或行为,而无需使用接口或继承。
from typing import Protocol
class Quackable(Protocol):
def quack(self) -> None:
... # The ellipsis indicates this is just a method signature
class Duck:
def quack(self): print('Quack!')
class Dog:
def bark(self): print('Woof!')
def run_quack(obj: Quackable):
obj.quack()
run_quack(Duck()) # Works!
run_quack(Dog()) # Fails during TYPE CHECKING (not runtime)
从本质上讲,协议检查的是对象能做什么,而不是它是什么。协议简单地说,只要一个对象实现了某些方法或行为,它就符合条件,而不管它的实际类型或继承关系如何。
✨ 快速补充提示:如果想让 isinstance()
检查与协议一起工作,请添加 @runtime_checkable
装饰器!
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None:
...
6.上下文管理器
上下文管理器是定义方法的对象: __enter__()
和__exit__()
。__enter__()
方法在进入 with 代码块时运行,而 __exit__()
方法在离开 with
代码块时运行(即使出现异常)。
Contextlib
将所有模板代码封装在一个易于使用的装饰器中,从而简化了这一过程。
# OLD SYNTAX - Traditional OOP-style context manager
class retry:
def __enter__(self):
print("Entering Context")
def __exit__(self, exc_type, exc_val, exc_tb):
print("Exiting Context")
# NEW SYNTAX - New contextlib-based context manager
import contextlib
@contextlib.contextmanager
def retry():
print("Entering Context")
yield
print("Exiting Context")
要创建自己的上下文管理器,请使用 @contextlib.contextmanager
装饰器编写一个函数。在 yield
之前添加设置代码,在 yield
之后添加清理代码。yield
中的任何变量都将作为附加上下文传入。就是这样。
yield
语句会指示上下文管理器暂停你的函数,并让 with
块中的内容运行。
import contextlib
@contextlib.contextmanager
def context():
# Setup code here
setup()
yield (...) # Any variables you want to be passed to the with block
# Teardown code here
takedown()
总之,这是一种在 Python 中创建和使用上下文管理器的更简洁、更易读的方法。
7.结构模式匹配
在 Python 3.10 中引入的结构模式匹配为 Python 开发人员提供了传统条件逻辑的强大替代方案。最基本的语法是这样的
match value:
case pattern1:
# code if value matches pattern1
case pattern2:
# code if value matches pattern2
case _:
# wildcard case (default)
真正的威力来自于重组!匹配模式可以分解复杂的数据结构,只需一步即可提取数值。
# Destructuring and matching tuples
match point:
case (0, 0):
return "Origin"
case (0, y):
return f"Y-axis at {y}"
case (x, 0):
return f"X-axis at {x}"
case (x, y):
return f"Point at ({x}, {y})"
# Using OR pattern (|) to match multiple patterns
match day:
case ("Monday"
| "Tuesday"
| "Wednesday"
| "Thursday"
| "Friday"):
return "Weekday"
case "Saturday" | "Sunday":
return "Weekend"
# Guard clauses with inline 'if' statements
match temperature:
case temp if temp < 0:
return "Freezing"
case temp if temp < 20:
return "Cold"
case temp if temp < 30:
return "Warm"
case _:
return "Hot"
# Capture entire collections using asterisk (*)
match numbers:
case [f]:
return f"First: {f}"
case [f, l]:
return f"First: {f}, Last: {l}"
case [f, *m, l]:
return f"First: {f}, Middle: {m}, Last: {l}"
case []:
return "Empty list"
您还可以将 match-case 与其他 Python 特性(如Walrus 运算符)结合起来,创建更强大的模式。
# Check if a packet is valid or not
packet: list[int] = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]
match packet:
case [c1, c2, *data, footer] if ( # Deconstruct packet into header, data, and footer
(checksum := c1 + c2) == sum(data) and # Check that the checksum is correct
len(data) == footer # Check that the data length is correct
):
print(f"Packet received: {data} (Checksum: {checksum})")
case [c1, c2, *data]: # Failure case where structure is correct but checksum is wrong
print(f"Packet received: {data} (Checksum Failed)")
case [_, *__]: # Failure case where packet is too short
print("Invalid packet length")
case []: # Failure case where packet is empty
print("Empty packet")
case _: # Failure case where packet is invalid
print("Invalid packet")
8. Python Slots
Slots
是一种可以加快任何 Python 类的创建和访问速度的方法。
提要:它们为类定义了一组固定的属性,在运行时优化并加快了访问速度。
class Person:
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
在 Python 的引擎盖下,Python 类将实例属性存储在一个名为 __dict__
的内部字典中,这意味着每次要访问一个值时都需要进行哈希表查找。
与此相反,__slots__
使用了类似数组的结构,可以在真正的 O(1) 时间内查找属性,这给 Python 带来了一个小的整体速度提升。
# Without __slots__
class FooBar:
def __init__(self):
self.a = 1
self.b = 2
self.c = 3
f = FooBar()
print(f.__dict__) # {'a': 1, 'b': 2, 'c': 3}
# With __slots__
class FooBar:
__slots__ = ('a', 'b', 'c')
def __init__(self):
self.a = 1
self.b = 2
self.c = 3
f = FooBar()
print(f.__dict__) # AttributeError
print(f.__slots__) # ('a', 'b', 'c')
关于 __slots__
是否值得使用仍有争论,因为它使类的定义变得复杂,而性能上的好处却微乎其微,甚至根本没有。不过,如果需要的话,它还是一个有用的工具。
9. Python Nitpicks
这本身并不是 Python 的 “功能 “或 “技巧”,而是一些快速语法技巧,用于真正清理您的 Python 代码库。
作为一个看过很多 Python 代码的人,我想说的是。
9.1 For-else 语句
如果您需要检查 for 循环是否在没有中断的情况下完成,for-else 语句是一种无需使用临时变量就能实现这一目的的好方法。
# ===== Don't write this =====
found_server = False # Keep track of whether we found a server
for server in servers:
if server.check_availability():
primary_server = server
found_server = True # Set the flag to True
break
if not found_server:
# Use the backup server if no server was found
primary_server = backup_server
# Continue execution with whatever server we found
deploy_application(primary_server)
# ===== Write this instead =====
for server in servers:
if server.check_availability():
primary_server = server
break
else:
# Use the backup server if no server was found
primary_server = backup_server
# Continue execution with whatever server we found
deploy_application(primary_server)
9.2 Walrus 操作符
如果您需要在一个表达式中定义和评估一个变量,Walrus 操作符(Python 3.8 中的 PEP 572 新特性)是实现这一目的的快速方法。
Walrus 操作符对于在检查一个值是
not None
后立即使用该值非常有用!
# ===== Don't write this =====
response = get_user_input()
if response:
print('You pressed:', response)
else:
print('You pressed nothing')
# ===== Write this instead =====
if response := get_user_input():
print('You pressed:', response)
else:
print('You pressed nothing')
9.3 短路评估
短路评估是获取表达式列表中 “下一个可用 “或 “下一个真值 “的快捷方式。事实证明,你可以简单地链式or
语句!
# ===== Don't write this =====
username, full_name, first_name = get_user_info()
if username is not None:
display_name = username
elif full_name is not None:
display_name = full_name
elif first_name is not None:
display_name = first_name
else:
display_name = "Anonymous"
# ===== Write this instead =====
username, full_name, first_name = get_user_info()
display_name = username or full_name or first_name or "Anonymous"
9.4 运算符链
最后,Python 允许您将比较运算符链在一起,以缩短整数范围的比较,使它们比等价的布尔表达式更易读。
# ===== Don't write this =====
if 0 < x and x < 10:
print("x is between 0 and 10")
# ===== Write this instead =====
if 0 < x < 10: # Instead of if 0 < x and x < 10
print("x is between 0 and 10")
10. 高级 f-string 字符串格式化
Python 的 f-string 现在已经不是什么秘密了。在 Python 3.6 的 PEP 498 中引入的 f-string 是将变量、对象和表达式插入字符串的更好、更干净、更快、更安全的方法。
但你知道 f-strings 不只是插入变量那么简单吗?有一种隐藏的格式化语法被称为 “格式化迷你语言”(Format Mini-Language ),它允许你对字符串格式化进行更多的控制。
print(f"{' [ Run Status ] ':=^50}")
print(f"[{time:%H:%M:%S}] Training Run {run_id=} status: {progress:.1%}")
print(f"Summary: {total_samples:,} samples processed")
print(f"Accuracy: {accuracy:.4f} | Loss: {loss:#.3g}")
print(f"Memory: {memory / 1e9:+.2f} GB")
Output:
=================== [ Run Status ] ===================
[11:16:37] Training Run run_id=42 status: 87.4%
Summary: 12,345,678 samples processed
Accuracy: 0.9876 | Loss: 0.0123
Memory: +2.75 GB
你可以启用调试表达式、应用数字格式化(类似于 str.format
)、添加字符串填充、格式化日期时间对象等!所有这些都可以通过 f-string 格式指定器来实现。
常规 f-strings
print(f"Hello {item}!")
Hello World!
Debug 表达式
print(f"{name=}, {age=}")
name='Claude', age=3
数字格式化
print(f"Pi: {pi:.2f}")
print(f"Avogadro: {avogadro:.2e}")
print(f"Big Number: {big_num:,}")
print(f"Hex: {num:#0x}")
print(f"Number: {num:09}")
Pi: 3.14
Avogadro: 6.02e+23
Big Number: 1,000,000
Hex: 0x1a4
Number: 000000420
字符串填充
print(f"Left: |{word:<10}|")
print(f"Right: |{word:>10}|")
print(f"Center: |{word:^10}|")
print(f"Center *: |{word:*^10}|")
Left: |Python |
Right: | Python|
Center: | Python |
Center *: |**Python**|
日期格式化
print(f"Date: {now:%Y-%m-%d}")
print(f"Time: {now:%H:%M:%S}")
Date: 2025-03-10
Time: 14:30:59
百分比格式化
print(f"Progress: {progress:.1%}")
Progress: 75.0%
11. Cache / lru_cache
您可以使用内置的 @cache
装饰器来显著加快递归函数和昂贵计算的速度!(它在 Python 3.9 中取代了 @lru_cache
!)。
from functools import cache
@cache
def fib(n):
return n if n < 2 else fib(n-1) + fib(n-2)
从 Python 3.2 开始,作为 functools
模块的一部分,引入了 @lru_cache
,以实现快速、简洁的函数记忆化。从 Python 3.9 开始,添加了 @cache
,以更少的代码实现同样的效果。如果您想明确控制缓存大小,lru_cache
仍然存在。
FIB_CACHE = {}
# With Manual Caching :(
def fib(n):
if n in FIB_CACHE:
return FIB_CACHE[n]
if n <= 2:
return 1
FIB_CACHE[n] = fib(n - 1) + fib(n - 2)
return FIB_CACHE[n]
from functools import lru_cache
# Same code with lru_cache :)
@lru_cache(maxsize=None)
def fib(n):
return n if n < 2 else fib(n-1) + fib(n-2)
from functools import cache
# Same code with new Python 3.9's cache :D
@cache
def fib(n):
return n if n < 2 else fib(n-1) + fib(n-2)
12. Python Futures
您知道 Python 有类似 Promise 的本机并发控制吗?
from concurrent.futures import Future
# Manually create a Future Object
future = Future()
# Set its result whenever you want
future.set_result("Hello from the future!")
# Get the result
print(future.result()) # "Hello from the future!"
Python 的 concurrent.futures
模块可以让您直接控制异步操作,就像 JS 的 Promises 一样。例如,它们可以让您附加回调,在结果就绪时运行(就像 JS 的 .then()
)。
from concurrent.futures import Future
future = Future()
# Add callbacks BEFORE or AFTER completion!
future.add_done_callback(lambda f: print(f"Got: {f.result()}"))
future.set_result("Async result")
# Prints: "Got: Async result"
future.add_done_callback(lambda f: print(f"After: {f.result()}"))
# Prints: "After: Async result"
Python Futures 还提供了处理异常、设置超时或完全停止任务的原语。
from concurrent.futures import Future
import time, threading
# Create and manage a future manually
future = Future()
# Background task function
def background_task():
time.sleep(2)
future.set_result("Done!")
thread = threading.Thread(target=background_task)
thread.daemon = True
thread.start()
# Try all control operations
print(f"Cancelled: {future.cancel()}") # Likely False if started
try:
# Wait at most 0.5 seconds
result = future.result(timeout=0.5)
except TimeoutError:
print("Timed out!")
# Create failed future
err_future = Future()
err_future.set_exception(ValueError("Failed"))
print(f"Has error: {bool(err_future.exception())}")
就像现代 JS 一样,asyncio
模块也有自己的 Future,可以与 Python 的 async/await
语法无缝协作:
import asyncio
async def main():
future = asyncio.Future()
# Set result after delay
asyncio.create_task(set_after_delay(future))
# Await just like a JS Promise!
result = await future
print(result) # "Worth the wait!"
async def set_after_delay(future):
await asyncio.sleep(1)
future.set_result("Worth the wait!")
asyncio.run(main())
最后,对于 CPU 或 I/O 绑定任务,Python 的 ThreadPoolExecutor
可以自动为您创建和管理futures 。
from concurrent.futures import ThreadPoolExecutor
import time
def slow_task():
time.sleep(1)
return "Done!"
with ThreadPoolExecutor() as executor:
# Returns a Future immediately
future = executor.submit(slow_task)
# Do other work while waiting...
print("Working...")
# Get result when needed
print(future.result())
13. 代理属性
你知道可以让类属性既作为方法又作为属性吗?这不是 Python 的内置特性,而是巧妙使用 Python 的 dunder(魔法)方法和描述符所能实现的演示。
(请注意,这只是一个示例实现,不应在生产中使用)
from typing import Callable, Generic, TypeVar, ParamSpec, Self
P = ParamSpec("P")
R = TypeVar("R")
T = TypeVar("T")
class ProxyProperty(Generic[P, R]):
func: Callable[P, R]
instance: object
def __init__(self, func: Callable[P, R]) -> None:
self.func = func
def __get__(self, instance: object, _=None) -> Self:
self.instance = instance
return self
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
return self.func(self.instance, *args, **kwargs)
def __repr__(self) -> str:
return self.func(self.instance)
def proxy_property(func: Callable[P, R]) -> ProxyProperty[P, R]:
return ProxyProperty(func)
class Container:
@proxy_property
def value(self, val: int = 5) -> str:
return f"The value is: {val}"
# Example usage
c = Container()
print(c.value) # Returns: The value is: 5
print(c.value(7)) # Returns: The value is: 7
在引擎盖下是如何工作的呢?这要归功于 Python 的描述符协议:
__get__
方法将ProxyProperty
对象转换为描述符。- 访问
c.value
时,Python 调用__get__
返回self
(描述符实例)。 __repr__
方法处理属性访问(返回缺省值)。__call__
方法处理带参数的方法调用。
这样就创建了一个两用属性,既可以直接读取,也可以像函数一样调用!
这个类的好处是,它允许你创建直观的 API,其中的属性可能需要配置,或者属性应该有合理的默认值,但仍然允许自定义。
如果您想了解代理属性的合适的生产就绪实现,请点击此处查看 Codegen 的 ProxyProperty
实现:codegen/src/codegen/sdk/_proxy.py
14. Metaclasses
最后,介绍 Python 最强大但最神秘的功能之一: 元类 Metaclasses
class MyMetaclass(type):
def __new__(cls, name, bases, namespace):
# Magic happens here
return super().__new__(cls, name, bases, namespace)
class MyClass(metaclass=MyMetaclass):
pass
obj = MyClass()
Python 中的类不仅仅是对象的蓝图。它们也是对象!每个对象都需要一个创建它的类。那么是什么创建了类对象呢?元类。
默认情况下,Python 使用type
元类来创建所有类。例如,这两个类是等价的:
# Create a MyClass object
class MyClass:
...
obj = MyClass()
# Also creates a MyClass object
obj2 = type("MyClass", (), {})
为了解释这些参数的含义,下面是一个例子,它创建了一个带有属性 x
和方法 say_hi
的类,该类也是 off object
的子类。
# type(
# name,
# bases,
# attributes
# )
CustomClass = type(
'CustomClass',
(object,),
{'x': 5, 'say_hi': lambda self: 'Hello!'}
)
obj = CustomClass()
print(obj.x) # 5
print(obj.say_hi()) # Hello!
实质上,元类可以让你在创建类时自定义和修改这些参数。例如,这里有一个元类,可将类的每个整数属性加倍:
class DoubleAttrMeta(type):
def __new__(cls, name, bases, namespace):
new_namespace = {}
for key, val in namespace.items():
if isinstance(val, int):
val *= 2
new_namespace[key] = val
return super().__new__(cls, name, bases, new_namespace)
class MyClass(metaclass=DoubleAttrMeta):
x = 5
y = 10
print(MyClass.x) # 10
print(MyClass.y) # 20
下面是另一个元类的例子,它将创建的每个类都注册到注册表中。
# ===== Metaclass Solution =====
class RegisterMeta(type):
registry = []
def __new__(mcs, name, bases, attrs):
cls = super().__new__(mcs, name, bases, attrs)
mcs.registry.append(cls)
return cls
问题是,decorators 不使用黑魔法也能达到同样的目的(而且往往更干净)。
# ===== Decorator Solution =====
def register(cls):
registry.append(cls)
return cls
@register
class MyClass:
pass
这就暴露了metaclasses 的最大问题:
几乎 100% 的情况下,您都不需要使用元类。
在日常开发中,99% 的代码都不会遇到元类有用的用例。而在这 1%的代码中,95%的情况都可以通过普通的装饰器、Dunder 方法或简单的继承来解决。
这就是为什么 Python 有一句名言:”元类是更深奥的魔法:
元类是更深层次的魔法,99% 的用户都不应该担心。如果你想知道自己是否需要元类,那你就不需要。- Tim Peters
但如果您是那 1%,有一个只有元类才能解决的足够独特的问题,那么它们就是一个强大的工具,让您可以修补 Python 对象系统的内部。
至于元类在现实世界中的一些例子:
- Python 的 “ABC “实现使用元类来实现抽象类。
- Python 的 “Enum “实现用它来创建枚举类型。
- 许多第三方库,如 Django、SQLAlchemy、Pydantic 和 Pytest,都将元类用于各种用途。
Fin
就这样,伙计们!我在 Python 职业生涯中遇到的最有趣、最被低估的 14 个 Python 特性。
如果您已经读到这里,请给我留言,告诉我哪些是您以前见过的,哪些是您没见过的!我很乐意听到您的意见。
你也许感兴趣的:
- Python 的新 t-strings
- Python 异步编程的 9 个级别
- 您不应该再使用的 11 个过时 Python 模块
- Python 中 help() 函数的各种特性
- Python 奇特的自引用
- 揭秘 Python 的 10 个隐藏技巧
- 如果您使用 Python… 你现在就需要了解这 3 个工具!
- 【外评】Python 为何如此糟糕…
- 【外评】用 Python 解释 Rust 背后的思想或理念
- Python 版本之间的主要变化摘要
你对本文的反应是: