Vielleicht hören Sie oft das Konzept des „Deskriptors“, aber da die meisten Programmierer es selten verwenden, verstehen Sie sein Prinzip möglicherweise nicht genau. In der Spalte „Python-Video-Tutorial“ wird es ausführlich vorgestellt.
Empfohlen (kostenlos). ):
Python-Video-TutorialAber wenn Sie Ihre Karriere auf die nächste Stufe bringen und sich mit der Verwendung von Python besser auskennen möchten, sollten Sie meiner Meinung nach trotzdem
Obwohl wir während des Entwicklungsprozesses keine Deskriptoren direkt verwendet haben, werden sie auf der untersten Ebene sehr häufig verwendet. Zum Beispiel Folgendes: 描述符
的这个概念有一个清晰的了解,这对于你以后的发展有着巨大的帮助,也有利于你将来更深层次的python设计的理解。
尽管在开发的过程中,我们没有直接的使用过描述符,但是它在底层的运用却是十分频繁的存在。例如下面的这些:
function
、bound method
、unbound method
property
、staticmethod
、classmethod
什么是描述符?
在我们了解什么是描述符前,我们可以先找一个例子来看一下
class A: x = 10print(A.x) # 10
这个例子很简单,我们先在类A
中定义一个类属性x
,然后得出它的值。
除了这种直接定义类属性的方法外,我们还可以这样去定义一个类属性:
class Ten: def __get__(self, obj, objtype=None): return 10class A: x = Ten() # 属性换成了一个类print(A.x) # 10
我们可以发现,这回的类属性x
不是一个具体的值了,而是一个类Ten
,通过这个Ten
定义了一个__get__
方法,返回具体的值。
因此可得出:在python中,我们可以把一个类的属性,托管给一个类,而这样的属性就是一个描述符
简而言之,描述符
是一个绑定行为
属性
而这又有着什么意思呢?
回想,我们在开发时,一般情况下,会将行为
叫做什么?行为
即一个方法。
所以我们也可以将描述符
理解为:对象的属性并非一个具体的值,而是交给了一个方法去定义。
可以想像一下,如果我们用一个方法去定义一个属性,这么做有什么好处?
有了方法,我们就可以在方法内实现自己的逻辑,最简单的,我们可以根据不同的条件,在方法内给属性赋予不同的值,就像下面这样:
class Age: def __get__(self, obj, objtype=None): if obj.name == 'zhangsan': return 20 elif obj.name == 'lisi': return 25 else: return ValueError("unknow")class Person: age = Age() def __init__(self, name): self.name = name p1 = Person('zhangsan')print(p1.age) # 20p2 = Person('lisi')print(p2.age) # 25p3 = Person('wangwu')print(p3.age) # unknow
这个例子中,age
类属性被另一个类托管了,在这个类的 __get__
中,它会根据 Person
类的属性 name
,决定 age
是什么值。
通过这样一个例子,我们可以看到,通过描述符的使用,我们可以轻易地改变一个类属性的定义方式。
描述符协议
了解了描述符的定义,现在我们把重点放到托管属性的类上。
其实,一个类属性想要托管给一个类,这个类内部实现的方法不能是随便定义的,它必须遵守「描述符协议」,也就是要实现以下几个方法:
__get__(self, obj, type=None) -> value
__set__(self, obj, value) -> None
__delete__(self, obj) -> None
只要是实现了以上几个方法的其中一个,那么这个类属性就可以称作描述符。
另外,描述符又可以分为「数据描述符」和「非数据描述符」:
__get___
,叫做非数据描述符__get__
之外,还定义了 __set__
或 __delete__
,叫做数据描述符它们两者有什么区别,我会在下面详述。
现在我们来看一个包含 __get__
和 __set__
方法的描述符例子:
# coding: utf8class Age: def __init__(self, value=20): self.value = value def __get__(self, obj, type=None): print('call __get__: obj: %s type: %s' % (obj, type)) return self.value def __set__(self, obj, value): if value <= 0: raise ValueError("age must be greater than 0") print('call __set__: obj: %s value: %s' % (obj, value)) self.value = valueclass Person: age = Age() def __init__(self, name): self.name = name p1 = Person('zhangsan')print(p1.age)# call __get__: obj: <__main__.Person object at 0x1055509e8> type: <class '__main__.Person'># 20print(Person.age)# call __get__: obj: None type: <class '__main__.Person'># 20p1.age = 25# call __set__: obj: <__main__.Person object at 0x1055509e8> value: 25print(p1.age)# call __get__: obj: <__main__.Person object at 0x1055509e8> type: <class '__main__.Person'># 25p1.age = -1# ValueError: age must be greater than 0
在这例子中,类属性 age
是一个描述符,它的值取决于 Age
类。
从输出结果来看,当我们获取或修改 age
属性时,调用了 Age
的 __get__
和 __set__
Funktion
, gebundene Methode
, ungebundene Methode
staticmethod
, classmethod
def getattr_hook(obj, name): try: return obj.__getattribute__(name) except AttributeError: if not hasattr(type(obj), '__getattr__'): raise return type(obj).__getattr__(obj, name)
A
>x und rufen Sie dann seinen Wert ab. # 获取一个对象的属性 def __getattribute__(obj, name): null = object() # 对象的类型 也就是实例的类 objtype = type(obj) # 从这个类中获取指定属性 cls_var = getattr(objtype, name, null) # 如果这个类实现了描述符协议 descr_get = getattr(type(cls_var), '__get__', null) if descr_get is not null: if (hasattr(type(cls_var), '__set__') or hasattr(type(cls_var), '__delete__')): # 优先从数据描述符中获取属性 return descr_get(cls_var, obj, objtype) # 从实例中获取属性 if hasattr(obj, '__dict__') and name in vars(obj): return vars(obj)[name] # 从非数据描述符获取属性 if descr_get is not null: return descr_get(cls_var, obj, objtype) # 从类中获取属性 if cls_var is not null: return cls_var # 抛出 AttributeError 会触发调用 __getattr__ raise AttributeError(name)
x
dieses Mal kein spezifischer Wert ist Stattdessen handelt es sich um eine Klasse Ten
. Diese Ten
definiert eine __get__
-Methode, um einen bestimmten Wert zurückzugeben. 🎜🎜Daher kann man schlussfolgern: In Python 🎜Wir können die Attribute einer Klasse zu einer Klasse hosten, und ein solches Attribut ist ein Deskriptor
🎜Deskriptor
ist ein Bindungsverhalten
-Attribut🎜🎜Und was bedeutet das? Verhalten
, wenn wir entwickeln? Behavior
ist eine Methode. 🎜🎜Wir können descriptor
also auch so verstehen: 🎜Das Attribut des Objekts ist kein spezifischer Wert, sondern wird durch eine Methode definiert. 🎜🎜🎜Sie können sich vorstellen, welche Vorteile es hat, wenn wir eine Methode zum Definieren eines Attributs verwenden? 🎜🎜Mit Methoden können wir unsere eigene Logik innerhalb der Methode implementieren. Im einfachsten Fall können wir Attributen innerhalb der Methode unterschiedliche Werte zuweisen, wie folgt: 🎜type(a).__dict__['b'].__get__(a, type(a))复制代码
__get__
auf dem Attribut namePerson
. /code>, bestimmt den Wert von age
. 🎜🎜Anhand eines solchen Beispiels können wir sehen, dass wir durch die Verwendung von Deskriptoren die Art und Weise, wie ein Klassenattribut definiert wird, leicht ändern können. 🎜🎜🎜Deskriptorprotokoll🎜🎜🎜Um die Definition von Deskriptoren zu verstehen, konzentrieren wir uns nun auf die Klasse der verwalteten Eigenschaften. 🎜🎜Wenn ein Klassenattribut tatsächlich in einer Klasse gehostet werden soll, können die in dieser Klasse implementierten Methoden nicht zufällig definiert werden. Es muss dem „Deskriptorprotokoll“ entsprechen, dh die folgenden Methoden müssen implementiert werden: 🎜__get__(self, obj, type=None) -> value
__set__(self, obj, value) -> > li>__delete__(self, obj) -> None
__get___
definiert, der als Nicht-Daten-Deskriptor bezeichnet wird li >__get__
wird auch __set__
oder __delete__
definiert, ein sogenannter Datendeskriptor__get__
und __set__
enthält: 🎜a.__dict__['b'].__get__(None, a)复制代码
age
Ein Deskriptor, dessen Wert von der Klasse Age
abhängt. 🎜🎜Der Ausgabe nach zu urteilen: Wenn wir das Attribut age
erhalten oder ändern, werden __get__
und __set__ von <code>Age
aufgerufen Code> Methode: 🎜p1.age
时,__get__
被调用,参数 obj
是 Person
实例,type
是 type(Person)
Person.age
时,__get__
被调用,参数 obj
是 None
,type
是 type(Person)
p1.age = 25
时,__set__
被调用,参数 obj
是 Person
实例,value
是25p1.age = -1
时,__set__
没有通过校验,抛出 ValueError
其中,调用 __set__
传入的参数,我们比较容易理解,但是对于 __get__
方法,通过类或实例调用,传入的参数是不同的,这是为什么?
这就需要我们了解一下描述符的工作原理。
描述符的工作原理
要解释描述符的工作原理,首先我们需要先从属性的访问说起。
在开发时,不知道你有没有想过这样一个问题:通常我们写这样的代码 a.b
,其背后到底发生了什么?
这里的 a
和 b
可能存在以下情况:
a
可能是一个类,也可能是一个实例,我们这里统称为对象b
可能是一个属性,也可能是一个方法,方法其实也可以看做是类的属性其实,无论是以上哪种情况,在 Python 中,都有一个统一的调用逻辑:
__getattribute__
尝试获得结果__getattr__
用代码表示就是下面这样:
def getattr_hook(obj, name): try: return obj.__getattribute__(name) except AttributeError: if not hasattr(type(obj), '__getattr__'): raise return type(obj).__getattr__(obj, name)
我们这里需要重点关注一下 __getattribute__
,因为它是所有属性查找的入口,它内部实现的属性查找顺序是这样的:
__get__
__dict__
中查找__dict__
中查找不到,再看它是否是一个非数据描述符__get__
AttributeError
异常写成代码就是下面这样:
# 获取一个对象的属性 def __getattribute__(obj, name): null = object() # 对象的类型 也就是实例的类 objtype = type(obj) # 从这个类中获取指定属性 cls_var = getattr(objtype, name, null) # 如果这个类实现了描述符协议 descr_get = getattr(type(cls_var), '__get__', null) if descr_get is not null: if (hasattr(type(cls_var), '__set__') or hasattr(type(cls_var), '__delete__')): # 优先从数据描述符中获取属性 return descr_get(cls_var, obj, objtype) # 从实例中获取属性 if hasattr(obj, '__dict__') and name in vars(obj): return vars(obj)[name] # 从非数据描述符获取属性 if descr_get is not null: return descr_get(cls_var, obj, objtype) # 从类中获取属性 if cls_var is not null: return cls_var # 抛出 AttributeError 会触发调用 __getattr__ raise AttributeError(name)
如果不好理解,你最好写一个程序测试一下,观察各种情况下的属性的查找顺序。
到这里我们可以看到,在一个对象中查找一个属性,都是先从 __getattribute__
开始的。
在 __getattribute__
中,它会检查这个类属性是否是一个描述符,如果是一个描述符,那么就会调用它的 __get__
方法。但具体的调用细节和传入的参数是下面这样的:
a
是一个实例,调用细节为:type(a).__dict__['b'].__get__(a, type(a))复制代码
a
是一个类,调用细节为:a.__dict__['b'].__get__(None, a)复制代码
所以我们就能看到上面例子输出的结果。
数据描述符和非数据描述符
了解了描述符的工作原理,我们继续来看数据描述符和非数据描述符的区别。
从定义上来看,它们的区别是:
__get___
,叫做非数据描述符__get__
之外,还定义了 __set__
或 __delete__
,叫做数据描述符此外,我们从上面描述符调用的顺序可以看到,在对象中查找属性时,数据描述符要优先于非数据描述符调用。
在之前的例子中,我们定义了 __get__
和 __set__
,所以那些类属性都是数据描述符。
我们再来看一个非数据描述符的例子:
class A: def __init__(self): self.foo = 'abc' def foo(self): return 'xyz'print(A().foo) # 输出什么? 复制代码
这段代码,我们定义了一个相同名字的属性和方法 foo
,如果现在执行 A().foo
,你觉得会输出什么结果?
答案是 abc
。
为什么打印的是实例属性 foo
的值,而不是方法 foo
呢?
这就和非数据描述符有关系了。
我们执行 dir(A.foo)
,观察结果:
print(dir(A.foo))# [... '__get__', '__getattribute__', ...]复制代码
看到了吗?A
的 foo
方法其实实现了 __get__
,我们在上面的分析已经得知:只定义 __get__
方法的对象,它其实是一个非数据描述符,也就是说,我们在类中定义的方法,其实本身就是一个非数据描述符。
所以,在一个类中,如果存在相同名字的属性和方法,按照上面所讲的 __getattribute__
中查找属性的顺序,这个属性就会优先从实例中获取,如果实例中不存在,才会从非数据描述符中获取,所以在这里优先查找的是实例属性 foo
的值。
到这里我们可以总结一下关于描述符的相关知识点:
__getattribute__
是查找一个属性(方法)的入口__getattribute__
定义了一个属性(方法)的查找顺序:数据描述符、实例属性、非数据描述符、类属性__getattribute__
方法,会阻止描述符的调用__get__
描述符的使用场景
了解了描述符的工作原理,那描述符一般用在哪些业务场景中呢?
在这里我用描述符实现了一个属性校验器,你可以参考这个例子,在类似的场景中去使用它。
首先我们定义一个校验基类 Validator
,在 __set__
方法中先调用 validate
方法校验属性是否符合要求,然后再对属性进行赋值。
class Validator: def __init__(self): self.data = {} def __get__(self, obj, objtype=None): return self.data[obj] def __set__(self, obj, value): # 校验通过后再赋值 self.validate(value) self.data[obj] = value def validate(self, value): pass 复制代码
接下来,我们定义两个校验类,继承 Validator
,然后实现自己的校验逻辑。
class Number(Validator): def __init__(self, minvalue=None, maxvalue=None): super(Number, self).__init__() self.minvalue = minvalue self.maxvalue = maxvalue def validate(self, value): if not isinstance(value, (int, float)): raise TypeError(f'Expected {value!r} to be an int or float') if self.minvalue is not None and value < self.minvalue: raise ValueError( f'Expected {value!r} to be at least {self.minvalue!r}' ) if self.maxvalue is not None and value > self.maxvalue: raise ValueError( f'Expected {value!r} to be no more than {self.maxvalue!r}' )class String(Validator): def __init__(self, minsize=None, maxsize=None): super(String, self).__init__() self.minsize = minsize self.maxsize = maxsize def validate(self, value): if not isinstance(value, str): raise TypeError(f'Expected {value!r} to be an str') if self.minsize is not None and len(value) < self.minsize: raise ValueError( f'Expected {value!r} to be no smaller than {self.minsize!r}' ) if self.maxsize is not None and len(value) > self.maxsize: raise ValueError( f'Expected {value!r} to be no bigger than {self.maxsize!r}' )复制代码
最后,我们使用这个校验类:
class Person: # 定义属性的校验规则 内部用描述符实现 name = String(minsize=3, maxsize=10) age = Number(minvalue=1, maxvalue=120) def __init__(self, name, age): self.name = name self.age = age # 属性符合规则 p1 = Person('zhangsan', 20)print(p1.name, p1.age)# 属性不符合规则 p2 = person('a', 20)# ValueError: Expected 'a' to be no smaller than 3p3 = Person('zhangsan', -1)# ValueError: Expected -1 to be at least 1复制代码
现在,当我们对 Person
实例进行初始化时,就可以校验这些属性是否符合预定义的规则了。
function与method
我们再来看一下,在开发时经常看到的 function
、unbound method
、bound method
它们之间到底有什么区别?
来看下面这段代码:
class A: def foo(self): return 'xyz'print(A.__dict__['foo']) # <function foo at 0x10a790d70>print(A.foo) # <unbound method A.foo>print(A().foo) # <bound method A.foo of <__main__.A object at 0x10a793050>>复制代码
从结果我们可以看出它们的区别:
function
准确来说就是一个函数,并且它实现了 __get__
方法,因此每一个 function
都是一个非数据描述符,而在类中会把 function
放到 __dict__
中存储function
被实例调用时,它是一个 bound method
function
被类调用时, 它是一个 unbound method
function
是一个非数据描述符,我们之前已经讲到了。
而 bound method
和 unbound method
的区别就在于调用方的类型是什么,如果是一个实例,那么这个 function
就是一个 bound method
,否则它是一个 unbound method
。
property/staticmethod/classmethod
我们再来看 property
、staticmethod
、classmethod
。
这些装饰器的实现,默认是 C 来实现的。
其实,我们也可以直接利用 Python 描述符的特性来实现这些装饰器,
property
的 Python 版实现:
class property: def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: return self.fget if self.fget is None: raise AttributeError(), "unreadable attribute" return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError, "can't set attribute" return self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError, "can't delete attribute" return self.fdel(obj) def getter(self, fget): return type(self)(fget, self.fset, self.fdel, self.__doc__) def setter(self, fset): return type(self)(self.fget, fset, self.fdel, self.__doc__) def deleter(self, fdel): return type(self)(self.fget, self.fset, fdel, self.__doc__)复制代码
staticmethod
的 Python 版实现:
class staticmethod: def __init__(self, func): self.func = func def __get__(self, obj, objtype=None): return self.func 复制代码
classmethod
的 Python 版实现:
class classmethod: def __init__(self, func): self.func = func def __get__(self, obj, klass=None): if klass is None: klass = type(obj) def newfunc(*args): return self.func(klass, *args) return newfunc 复制代码
除此之外,你还可以实现其他功能强大的装饰器。
由此可见,通过描述符我们可以实现强大而灵活的属性管理功能,对于一些要求属性控制比较复杂的场景,我们可以选择用描述符来实现。
总结
这篇文章我们主要讲了 Python 描述符的工作原理。
首先,我们从一个简单的例子了解到,一个类属性是可以托管给另外一个类的,这个类如果实现了描述符协议方法,那么这个类属性就是一个描述符。此外,描述符又可以分为数据描述符和非数据描述符。
之后我们又分析了获取一个属性的过程,一切的入口都在 __getattribute__
中,这个方法定义了寻找属性的顺序,其中实例属性优先于数据描述符调用,数据描述符要优先于非数据描述符调用。
Darüber hinaus haben wir auch gelernt, dass eine Methode tatsächlich ein Nicht-Datendeskriptor ist. Wenn wir Instanzattribute und Methoden mit demselben Namen in der Klasse definieren, entsprechend der Attributsuchreihenfolge in __getattribute__
, wird der Auf das Instanzattribut wird zuerst zugegriffen. __getattribute__
中的属性查找顺序,实例属性优先访问。
最后我们分析了 function
和 method
的区别,以及使用 Python 描述符也可以实现 property
、staticmethod
、classmethod
function
und method
analysiert und außerdem Python-Deskriptoren verwendet, um property
und staticmethod
zu implementieren , classmethod
Dekorator. Python-Deskriptoren bieten leistungsstarke Attributzugriffskontrollfunktionen, die wir in Szenarien verwenden können, die eine komplexe Kontrolle von Attributen erfordern. Dieses Werk unterliegt der „CC-Lizenz“, und beim Nachdruck müssen der Autor und der Link zu diesem Artikel angegeben werden🎜Das obige ist der detaillierte Inhalt vonEinführung in die Bedeutung von Python-Deskriptoren. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!