python高阶特性
文章目录
类的相关方法
实例方法
实例方法就是绑定到实例的普通函数,其底层通过 描述符协议 把函数变成“方法对象”,从而自动注入 self(通常是一个实例的一个对象)。但正是这个“绑定”动作使得每次调用对象时单独开辟空间,让其能够正常满足各个对象的魔改操作了。
篇幅有限,我只列出相关代码,后续解释敬请期待。。。提示一下:有相关注释,大家可以先自行理解哟
动态增删实例方法
import types
class Person:
def __init__(self, name):
self.name = name
def greet(self, msg="Hello"):
return f"{msg}, I'm {self.name}"
p = Person("Alice")
p.greet = types.MethodType(greet, p)
print(p.greet()) # Hello, I'm Alice
del p.greet # 随时删掉
方法链
class Query:
def __init__(self, sql=""):
self.sql = sql
def select(self, *cols):
self.sql = f"SELECT {','.join(cols)}"
return self
def where(self, cond):
self.sql += f" WHERE {cond}"
return self
def build(self):
return self.sql + ";"
q = Query().select("id", "name").where("age > 18").build()
print(q) # SELECT id,name WHERE age > 18;
零拷贝数据绑定
class Cat:
def __init__(self, name): self.name = name
def speak(self): return f"{self.name} meows"
class Dog:
def __init__(self, name): self.name = name
dog = Dog("Buddy")
dog.speak = types.MethodType(Cat.speak, dog) # 借用 Cat 的 speak
print(dog.speak()) # Buddy meows
惰性计算和自动缓存
class DataLoader:
def __init__(self, path):
self._path = path
self._df = None
@property
def df(self):
if self._df is None:
print("loading csv...")
import pandas as pd
self._df = pd.read_csv(self._path)
return self._df
loader = DataLoader("big.csv")
loader.df # 第一次会读文件
loader.df # 第二次直接返回缓存对象
自动注册DSL
registry = {}
def command(name):
def decorator(func):
registry[name] = func
return func
return decorator
class Shell:
@command("greet")
def do_greet(self, user):
return f"Hi {user}"
shell = Shell()
print(registry["greet"](shell, "Alice")) # Hi Alice
猴子补丁
import time, functools
def timing(original):
@functools.wraps(original)
def _wrapper(self, *a, **kw):
t0 = time.perf_counter()
res = original(self, *a, **kw)
print(f"{original.__name__} took {time.perf_counter()-t0:.4f}s")
return res
return _wrapper
class Service:
def heavy(self):
time.sleep(0.1)
s = Service()
s.heavy = types.MethodType(timing(Service.heavy), s)
s.heavy() # heavy took 0.1003s
静态方法
直接给你们个概念:静态方法的最大价值是“把与类强相关、却不需要类/实例状态的工具函数”优雅地收进类的命名空间,从而提升代码的可读性、可发现性、可扩展性**,并天然支持无实例调用、继承隐藏**、接口归一等高级技巧,通常我们在语义上属于类,但是并不关心实例状态,优先使用静态方法,再选择使用模块
下面我们一次来解释一下无实例调用、继承隐藏、接口归一:
无实例调用
命名空间收纳技巧(将工具函数类封装成“静态工具箱”),代码说明:
class Color:
def __init__(self, r, g, b): ...
@staticmethod
def hex_to_rgb(hex_code: str) -> tuple[int, int, int]:
hex_code = hex_code.lstrip('#')
return tuple(int(hex_code[i:i+2], 16) for i in (0, 2, 4))
# 使用
Color.hex_to_rgb("#1E90FF")
继承隐藏
子类可“同名覆盖”静态方法做策略切换,代码说明:
class Payment:
@staticmethod
def gateway_fee(amount: float) -> float:
return amount * 0.01 # 默认 1%
class PayPal(Payment):
@staticmethod
def gateway_fee(amount: float) -> float:
return 0.30 + amount * 0.029 # PayPal 费率
print(Payment.gateway_fee(100)) # 1.0
print(PayPal.gateway_fee(100)) # 3.2
接口归一
用于多构造函数创建(可以理解为一个静态工厂),代码如下:
class Point:
def __init__(self, x, y): ...
@staticmethod
def from_tuple(t): # 无需 cls,纯转换
return Point(*t)
@classmethod
def origin(cls): # 需要 cls,可被子类复用
return cls(0, 0)
总的一句话就是,你可以这样理解它,静态函数让代码“用类组织,却不必实例化”,是 Python 里做策略、校验、转换、注册等高阶语义时最轻量也最优雅的手段之一,大家可以尝试着写一个注册表来体验一下。
类方法
类方法我们一般用来用作工厂函数的创建,相比实例构造函数需要传递严格的参数,调用者需要先解析出严格的参数,然后传递到构造函数中,耦合性高,并且类对象没有封装“如何创建对象”的细节,你可以这样看:
class People:
def __init__(self,name,age):
self.name=name
self.age=age
data="www 31"
name,age=data.split(",")
p1=People(name,int(age))
我们需要在使用split()函数时返回的是一个元组,其type为str,因此要做一次强转,来使得保证其类型为整型,下面我们来解释婴喜爱类方法是如何更优秀的构建这个类似构造方法的示例:
class People:
def __init__(self,name,age):
self.name=name
self.age=age
@classmethod
def from_String(cls,ree):
name,age=ree.split(",")
return cls(name,int(age))
p2=People.from_string("ddad,31")
这里要补充一点就是cls(name,int(age))中的cls==People,因此将这种封装方式来提高效率是非常行之有效的,并且这个生命周期是当类有被调用即存在内存空间,后续也同样遵循垃圾回收机制,这是一个非常重要的工厂函数开发技巧。
总的来说类方法是“挂在类上的工厂函数,它把“对象该怎么出生”封装回类内部,让调用者用语义化、可扩展、继承友好的方式创建实例,而不是把所有创建逻辑硬塞在 __init__ 里。
assert断言
黄阶
直接上核心语法,然后再讲解
语法基本结构assert_stmt ::= "assert" epression1 ["," expression2]
(中括号内表示可选内容,若是不选则只报assertionerror错误)
我们使用assert断言来检测无法恢复纠正的bug问题,即当发生问题(expression1为假)时则报assertionerror错误,不发生问题则直接跳过。其实这是一种程序内部自检,声明代码不可能出现的条件,如若出现这种条件则相应存在对应的bug。
其诞生是源于问题不能正确预料才出现这种断言方法,说句实话,要是知道我们知道问题了就可以用于纠正和重试了。再说使用断言我们就可以更加方便的找到问题所在了,总结一句话就是python断言语句是一种调试辅助功能,不是用来处理运行时发生错误的机制。
玄阶
我们给出在python编译解释器的相关执行代码:
if __debug__:
if not expression1:
raise AssertionError(expression2)
__debug__是一个全局变量,内置一个布尔标记,一般请况下为真,若是进行优化,则变为假;另外我们还可以这样理解expression2传递一个可选的错误信息,此调试其实也将回溯中的assertionerror一起现实,用来进一步简化和调试。
地阶
在突破python断言语法的情况下,其实也会出现安全风险和bug问题,并且特别容易出现许多并无啥作用的断言,因此我将从下面的几个例子来帮助大家理解相关的陷阱
断言验证数据风险
在CPython中的PYTHONOPTIMIZE环境变量修改和在终端命令行出现-O和-OO标识都会出现全局禁用断言的情况,因此这也是问题出现的一个原因,下面仔细为大家讲解一下:
使用断言语句检查管理员权限会造成任何服务都可以进行逆向操作,来修改相关操作,这是一个非常严重的问题,原因在于我们使用全局禁言会让该操作变为空操作,因此权限会对所有人开放,会引发权限泄露问题。
下一个就是使相应的函数失效,若是使用相关的检测函数,那么这个检测机制就没有任何效果了,大量的数据输入获取会造成十分严重的问题,说句实话这个问题比前一个问题还大,所以我们应该尽量的避免此情况。
要避免这个问题,当然就是少用这个assert不久行了吗,全部改成if-elif-else来替换,对,就是这样。。。
无效断言的无效使用
我们在进行判断是否执行,其实是通过布尔值来验证是否执行后面的报错,但是大多数人都不能正确写出这个expression1,下面我就来为大家举个例子:
assert (
1==2,
"注意"
)
大家以为这个是不是错的条件,其实这是对的,因为所有python中元组对象里面有值就是对的,当然不能全是假哟,大多数开发的时候不注意都会犯下这个错误,大家请着重留心一下。
天阶
大多数人都不能正确使用断言这个操作,但是这个确实是一个非常厉害的调试工具,大家的最难的任务就是使用它合理运用到项目中,大家一定要注意,学会容易,但是合理且高效的运用才是最难得的。
上下文管理器
基于对象构建
class mange_mm():
def __init__(self,name):
self.name=name
def __enter__(self):
self.file=open(self.name,"w")
return self.file
def __exit__(self,exc_type,exc_val,exc_tb):
if self.file:
sele.file.close()
解释一下:这个是将这个文件获取封装起来,类似使用open()函数,不过,我们可以自己调控相关的魔改操作,一般在__enter__()时调用开始时执行此函数,结束时调用__exit__()函数执行操作,调用起来也很简单,下面直接給出相关的示例:
with mange_mm("示例.txt") as f:
pass
这就是简单的示例,明白这个理论对后续学习蛮重要的,要着重理解一下
基于生成器构建
contextlib的相关函数,它能够为with资源管理器定义一个基于生成器的工厂函数,我们就来重写上面这个方法,为大家提供一个新的方法
from contextlib import contextmanger
@coontextmanger
def mange_mm():
try:
f=open(name,"w")
yield f
finally:
f.close()
就是这么使用的,大家可以先参考以我的代码,然后在创新修改哟。。。
yield使用
一句话就是说yield是python生成器的核心关键字,通常把一个普通函数变为一个可以通过yield来控制暂停的迭代器,简单来说就是先返回且存档,下次接着后面的代码继续跑。核心来说就是,在函数里用 yield 返回值,函数立刻暂停并“冻结”当前状态并且返回一个他要返回的值;下次再调用(next/send)时,从上次停住的地方继续往下跑。
我们现在对比一下它和return的区别,大家可以悉心对比一下:
| 特性 | return | yield |
|---|---|---|
| 返回后状态 | 函数结束,局部变量全丢 | 函数暂停,局部变量冻结 |
| 再次调用 | 重新开始 | 从上次暂停处继续 |
| 返回值类型 | 单一值 | 生成器对象(可迭代) |
还提供一些常见的接口,方便大家随时查看:
| 方式 | 作用 |
|---|---|
next(gen) | 继续到下一个 yield,返回其值 |
gen.send(value) | 把 value 送到 上一次 yield 表达式 处,并继续运行 |
gen.throw(exc) | 在上一次 yield 处抛出异常 |
gen.close() | 提前终止生成器 |
逗号的高阶特性
python描述符协议
描述符协议只有 3 个魔法钩子,却支撑了 Python 中@property、@staticmethod、@classmethod、函数对象、槽位(__slots__)等 所有“属性魔法” 的底层机制。描述符协议把“属性读写”变成一次可插拔的函数调用;通过实现 __get__/__set__/__delete__,你可以零侵入地劫持属性访问
下面列出一个脑图,大家可以递归的了解一下这个核心
obj.attr
│
├─找 MRO 顺序中第一个出现该名字的对象 d(可为 data descriptor / non-data descriptor / instance value / 类变量 / 父类值)
├─如果 d 定义了 __get__ → 调用 d.__get__(obj, type(obj)) 得到最终结果
│ ├─data descriptor(同时定义 __set__ 或 __delete__)优先级最高
│ └─non-data descriptor(只定义 __get__)次之
├─否则看实例字典 obj.__dict__['attr'] 是否存在
│
├─再看类字典(及其父类)中的普通变量
│
└─都没有 → AttributeError
下面大家可以通过这个图来更细致的理解相关接口:
| 钩子 | 触发时机 | 签名 |
|---|---|---|
__get__(self, obj, type) | 读属性时 | 返回计算后的值 |
__set__(self, obj, value) | 写属性时 | 无返回值 |
__delete__(self, obj) | 删属性时 | 无返回值 |
由于这个难度有点难,我只列举@property来帮助大家理解,后续大家可以自行查询资料,了解情况
class MyProperty:
def __init__(self, fget=None, fset=None, fdel=None):
self.fget, self.fset, self.fdel = fget, fset, fdel
def __get__(self, obj, objtype=None):
if obj is None: # 类级别访问
return self
if self.fget is None:
raise AttributeError("unreadable")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete")
self.fdel(obj)
# 使用
class Celsius:
def __init__(self, temp=0):
self._t = temp
@MyProperty
def temperature(self):
return self._t
@temperature.setter
def temperature(self, value):
if value < -273.15:
raise ValueError("below abs zero")
self._t = value


被折叠的 条评论
为什么被折叠?



