描述器使用指南

您所在的位置:网站首页 python的vars函数 描述器使用指南

描述器使用指南

2023-06-11 08:31| 来源: 网络整理| 查看: 265

纯 Python 等价实现¶

描述器协议很简单,但它提供了令人兴奋的可能性。有几个用例非常通用,以至于它们已预先打包到内置工具中。属性、绑定方法、静态方法、类方法和 __slots__ 均基于描述器协议。

属性¶

调用 property() 是构建数据描述器的简洁方式,该数据描述器在访问属性时触发函数调用。它的签名是:

property(fget=None, fset=None, fdel=None, doc=None) -> property

该文档显示了定义托管属性 x 的典型用法:

class C: def getx(self): return self.__x def setx(self, value): self.__x = value def delx(self): del self.__x x = property(getx, setx, delx, "I'm the 'x' property.")

要了解 property() 如何根据描述器协议实现,这里是一个纯 Python 的等价实现:

class Property: "Emulate PyProperty_Type() in Objects/descrobject.c" def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel if doc is None and fget is not None: doc = fget.__doc__ self.__doc__ = doc self._name = '' def __set_name__(self, owner, name): self._name = name def __get__(self, obj, objtype=None): if obj is None: return self if self.fget is None: raise AttributeError(f"property '{self._name}' has no getter") return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError(f"property '{self._name}' has no setter") self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError(f"property '{self._name}' has no deleter") self.fdel(obj) def getter(self, fget): prop = type(self)(fget, self.fset, self.fdel, self.__doc__) prop._name = self._name return prop def setter(self, fset): prop = type(self)(self.fget, fset, self.fdel, self.__doc__) prop._name = self._name return prop def deleter(self, fdel): prop = type(self)(self.fget, self.fset, fdel, self.__doc__) prop._name = self._name return prop

这个内置的 property() 每当用户访问属性时生效,随后的变化需要一个方法的参与。

例如,一个电子表格类可以通过 Cell('b10').value 授予对单元格值的访问权限。对程序的后续改进要求每次访问都要重新计算单元格;但是,程序员不希望影响直接访问该属性的现有客户端代码。解决方案是将对 value 属性的访问包装在属性数据描述器中:

class Cell: ... @property def value(self): "Recalculate the cell before returning value" self.recalc() return self._value

在此示例中,内置的 property() 或我们实现的的 Property() 均适用。

函数和方法¶

Python 的面向对象功能是在基于函数的环境构建的。通过使用非数据描述器,这两方面完成了无缝融合。

在调用时,存储在类词典中的函数将被转换为方法。方法与常规函数的不同之处仅在于对象实例被置于其他参数之前。方法与常规函数的不同之处仅在于第一个参数是为对象实例保留的。按照惯例,实例引用称为 self ,但也可以称为 this 或任何其他变量名称。

可以使用 types.MethodType 手动创建方法,其行为基本等价于:

class MethodType: "Emulate PyMethod_Type in Objects/classobject.c" def __init__(self, func, obj): self.__func__ = func self.__self__ = obj def __call__(self, *args, **kwargs): func = self.__func__ obj = self.__self__ return func(obj, *args, **kwargs)

为了支持自动创建方法,函数包含 __get__() 方法以便在属性访问时绑定其为方法。这意味着函数其是非数据描述器,它在通过实例进行点查找时返回绑定方法,其运作方式如下:

class Function: ... def __get__(self, obj, objtype=None): "Simulate func_descr_get() in Objects/funcobject.c" if obj is None: return self return MethodType(self, obj)

在解释器中运行以下类,这显示了函数描述器的实际工作方式:

class D: def f(self, x): return x

该函数具有 qualified name 属性以支持自省:

>>> D.f.__qualname__ 'D.f'

通过类字典访问函数不会调用 __get__()。相反,它只返回基础函数对象:

>>> D.__dict__['f']

来自类的点运算符访问会调用 __get__(),直接返回底层的函数。

>>> D.f

有趣的行为发生在从实例进行点访问期间。点运算符查找调用 __get__(),返回绑定的方法对象:

>>> d = D() >>> d.f

绑定方法在内部存储了底层函数和绑定的实例:

>>> d.f.__func__ >>> d.f.__self__

如果你曾好奇常规方法中的 self 或类方法中的 cls 是从什么地方来的,就是这里了!

方法的种类¶

非数据描述器为把函数绑定为方法的通常模式提供了一种简单的机制。

概括地说,函数对象具有 __get__() 方法,以便在作为属性访问时可以将其转换为方法。非数据描述器将 obj.f(*args) 的调用会被转换为 f(obj, *args) 。调用 klass.f(*args)` 因而变成 f(*args) 。

下表总结了绑定及其两个最有用的变体:

转换形式

通过对象调用

通过类调用

function -- 函数

f(obj, *args)

f(*args)

静态方法

f(*args)

f(*args)

类方法

f(type(obj), *args)

f(cls, *args)

静态方法¶

静态方法返回底层函数,不做任何更改。调用 c.f 或 C.f 等效于通过 object.__getattribute__(c, "f") 或 object.__getattribute__(C, "f") 查找。这样该函数就可以从对象或类中进行相同的访问。

适合作为静态方法的是那些不引用 self 变量的方法。

例如,一个统计用的包可能包含一个实验数据的容器类。该容器类提供了用于计算数据的平均值,均值,中位数和其他描述性统计信息的常规方法。但是,可能有在概念上相关但不依赖于数据的函数。例如, erf(x) 是在统计中的便捷转换,但并不直接依赖于特定的数据集。可以从对象或类中调用它: s.erf(1.5) --> .9332 或 Sample.erf(1.5) --> .9332。

由于静态方法返回的底层函数没有任何变化,因此示例调用也是意料之中:

class E: @staticmethod def f(x): return x * 10 >>> E.f(3) 30 >>> E().f(3) 30

使用非数据描述器,纯 Python 版本的 staticmethod() 如下所示:

import functools class StaticMethod: "Emulate PyStaticMethod_Type() in Objects/funcobject.c" def __init__(self, f): self.f = f functools.update_wrapper(self, f) def __get__(self, obj, objtype=None): return self.f def __call__(self, *args, **kwds): return self.f(*args, **kwds)

The functools.update_wrapper() call adds a __wrapped__ attribute that refers to the underlying function. Also it carries forward the attributes necessary to make the wrapper look like the wrapped function: __name__, __qualname__, __doc__, and __annotations__.

类方法¶

与静态方法不同,类方法在调用函数之前将类引用放在参数列表的最前。无论调用方是对象还是类,此格式相同:

class F: @classmethod def f(cls, x): return cls.__name__, x >>> F.f(3) ('F', 3) >>> F().f(3) ('F', 3)

当方法仅需要具有类引用并且确实依赖于存储在特定实例中的数据时,此行为就很有用。类方法的一种用途是创建备用类构造函数。例如,类方法 dict.fromkeys() 从键列表创建一个新字典。纯 Python 的等价实现是:

class Dict(dict): @classmethod def fromkeys(cls, iterable, value=None): "Emulate dict_fromkeys() in Objects/dictobject.c" d = cls() for key in iterable: d[key] = value return d

现在可以这样构造一个新的唯一键字典:

>>> d = Dict.fromkeys('abracadabra') >>> type(d) is Dict True >>> d {'a': None, 'b': None, 'r': None, 'c': None, 'd': None}

使用非数据描述器协议,纯 Python 版本的 classmethod() 如下:

import functools class ClassMethod: "Emulate PyClassMethod_Type() in Objects/funcobject.c" def __init__(self, f): self.f = f functools.update_wrapper(self, f) def __get__(self, obj, cls=None): if cls is None: cls = type(obj) if hasattr(type(self.f), '__get__'): # This code path was added in Python 3.9 # and was deprecated in Python 3.11. return self.f.__get__(cls, cls) return MethodType(self.f, cls)

The code path for hasattr(type(self.f), '__get__') was added in Python 3.9 and makes it possible for classmethod() to support chained decorators. For example, a classmethod and property could be chained together. In Python 3.11, this functionality was deprecated.

class G: @classmethod @property def __doc__(cls): return f'A doc for {cls.__name__!r}' >>> G.__doc__ "A doc for 'G'"

The functools.update_wrapper() call in ClassMethod adds a __wrapped__ attribute that refers to the underlying function. Also it carries forward the attributes necessary to make the wrapper look like the wrapped function: __name__, __qualname__, __doc__, and __annotations__.

成员对象和 __slots__¶

当一个类定义了 __slots__,它会用一个固定长度的 slot 值数组来替换实例字典。 从用户的视角看,效果是这样的:

1. Provides immediate detection of bugs due to misspelled attribute assignments. Only attribute names specified in __slots__ are allowed:

class Vehicle: __slots__ = ('id_number', 'make', 'model') >>> auto = Vehicle() >>> auto.id_nubmer = 'VYE483814LQEX' Traceback (most recent call last): ... AttributeError: 'Vehicle' object has no attribute 'id_nubmer'

2. Helps create immutable objects where descriptors manage access to private attributes stored in __slots__:

class Immutable: __slots__ = ('_dept', '_name') # Replace the instance dictionary def __init__(self, dept, name): self._dept = dept # Store to private attribute self._name = name # Store to private attribute @property # Read-only descriptor def dept(self): return self._dept @property def name(self): # Read-only descriptor return self._name >>> mark = Immutable('Botany', 'Mark Watney') >>> mark.dept 'Botany' >>> mark.dept = 'Space Pirate' Traceback (most recent call last): ... AttributeError: property 'dept' of 'Immutable' object has no setter >>> mark.location = 'Mars' Traceback (most recent call last): ... AttributeError: 'Immutable' object has no attribute 'location'

3. Saves memory. On a 64-bit Linux build, an instance with two attributes takes 48 bytes with __slots__ and 152 bytes without. This flyweight design pattern likely only matters when a large number of instances are going to be created.

4. Improves speed. Reading instance variables is 35% faster with __slots__ (as measured with Python 3.10 on an Apple M1 processor).

5. Blocks tools like functools.cached_property() which require an instance dictionary to function correctly:

from functools import cached_property class CP: __slots__ = () # Eliminates the instance dict @cached_property # Requires an instance dict def pi(self): return 4 * sum((-1.0)**n / (2.0*n + 1.0) for n in reversed(range(100_000))) >>> CP().pi Traceback (most recent call last): ... TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.

要创建一个一模一样的纯 Python 版的 __slots__ 是不可能的,因为它需要直接访问 C 结构体并控制对象内存分配。 但是,我们可以构建一个非常相似的模拟版,其中作为 slot 的实际 C 结构体由一个私有的 _slotvalues 列表来模拟。 对该私有结构体的读写操作将由成员描述器来管理:

null = object() class Member: def __init__(self, name, clsname, offset): 'Emulate PyMemberDef in Include/structmember.h' # Also see descr_new() in Objects/descrobject.c self.name = name self.clsname = clsname self.offset = offset def __get__(self, obj, objtype=None): 'Emulate member_get() in Objects/descrobject.c' # Also see PyMember_GetOne() in Python/structmember.c if obj is None: return self value = obj._slotvalues[self.offset] if value is null: raise AttributeError(self.name) return value def __set__(self, obj, value): 'Emulate member_set() in Objects/descrobject.c' obj._slotvalues[self.offset] = value def __delete__(self, obj): 'Emulate member_delete() in Objects/descrobject.c' value = obj._slotvalues[self.offset] if value is null: raise AttributeError(self.name) obj._slotvalues[self.offset] = null def __repr__(self): 'Emulate member_repr() in Objects/descrobject.c' return f''

type.__new__() 方法负责将成员对象添加到类变量:

class Type(type): 'Simulate how the type metaclass adds member objects for slots' def __new__(mcls, clsname, bases, mapping, **kwargs): 'Emulate type_new() in Objects/typeobject.c' # type_new() calls PyTypeReady() which calls add_methods() slot_names = mapping.get('slot_names', []) for offset, name in enumerate(slot_names): mapping[name] = Member(name, clsname, offset) return type.__new__(mcls, clsname, bases, mapping, **kwargs)

object.__new__() 方法负责创建具有 slot 而非实例字典的实例。 以下是一个纯 Python 的粗略模拟版:

class Object: 'Simulate how object.__new__() allocates memory for __slots__' def __new__(cls, *args, **kwargs): 'Emulate object_new() in Objects/typeobject.c' inst = super().__new__(cls) if hasattr(cls, 'slot_names'): empty_slots = [null] * len(cls.slot_names) object.__setattr__(inst, '_slotvalues', empty_slots) return inst def __setattr__(self, name, value): 'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c' cls = type(self) if hasattr(cls, 'slot_names') and name not in cls.slot_names: raise AttributeError( f'{cls.__name__!r} object has no attribute {name!r}' ) super().__setattr__(name, value) def __delattr__(self, name): 'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c' cls = type(self) if hasattr(cls, 'slot_names') and name not in cls.slot_names: raise AttributeError( f'{cls.__name__!r} object has no attribute {name!r}' ) super().__delattr__(name)

要在真实的类中使用这个模拟版,只需从 Object 继承并将 metaclass 设为 Type:

class H(Object, metaclass=Type): 'Instance variables stored in slots' slot_names = ['x', 'y'] def __init__(self, x, y): self.x = x self.y = y

这时,metaclass 已经为 x 和 y 加载了成员对象:

>>> from pprint import pp >>> pp(dict(vars(H))) {'__module__': '__main__', '__doc__': 'Instance variables stored in slots', 'slot_names': ['x', 'y'], '__init__': , 'x': , 'y': }

当实例被创建时,它们将拥有一个用于存放属性的 slot_values 列表:

>>> h = H(10, 20) >>> vars(h) {'_slotvalues': [10, 20]} >>> h.x = 55 >>> vars(h) {'_slotvalues': [55, 20]}

错误拼写或未赋值的属性将引发一个异常:

>>> h.xz Traceback (most recent call last): ... AttributeError: 'H' object has no attribute 'xz'


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3