python 描述符专项
-
这个文章不错
https://www.waynerv.com/posts/python-descriptor-in-detail/ -
Clean Code in Python中的描述符类学习
# 具有当前城市的旅行者在程序运行期间跟踪用户访问过的所有城市
class HistoryTracedAttribute:
def __init__(self, trace_attribute_name) :
self.trace_attribute_name = trace_attribute_name # 需要跟踪的属性 # cities_visited
self._name = None
def __set_name__(self, owner, name):
# print(owner, name) # class Traveller , current_city # 初始化才调用
self._name = name
def __get__(self, instance, owner):
if instance is None:
return self
# print('__get__',instance.__dict__,self._name) # {'name': 'the traveller name', 'cities_visited': ['a', 'b', 'd'], 'current_city': 'd'} current_city
return instance.__dict__[self._name]
def __set__(self, instance, value):
print('set start')
# print('__set__',instance,value) # <__main__.Traveller object at 0x00000289A33634C8> a # b # d
self._track_change_in_value_for_instance(instance, value)
instance.__dict__[self._name] = value # current_city = a # b # d
print('4')
print(instance.__dict__)
print('set end')
def _track_change_in_value_for_instance(self, instance, value):
self._set_default(instance) # 设置需要跟踪的属性的默认值 'cities_visited': []
if self._needs_to_track_change(instance, value):
instance.__dict__[self.trace_attribute_name].append(value) # 'cities_visited': [] 列表append
print('3')
print('_track_change_in_value_for_instance:',instance.__dict__)
def _needs_to_track_change(self, instance, value) -> bool: # 很巧妙,初始化的时候,字典对应的key current_city不存在:直接添加
try:
# print('_needs_to_track_change:', instance.__dict__)
current_value = instance.__dict__[self._name]
except KeyError:
print('2')
print('_needs_to_track_change:', instance.__dict__)
return True
return value != current_value
def _set_default(self, instance):
# print(self.trace_attribute_name) # cities_visited
instance.__dict__.setdefault(self.trace_attribute_name, []) # 设置默认值 'cities_visited': []
# print(instance.__dict__)
class Traveller: # 所有者类
# 调用 __set_name__ , 这里current_city是一个类的实例
# t.current_city = 'b' 后current_city就是字符串了
current_city = HistoryTracedAttribute("cities_visited") # 是类的属性,20210420
def __init__(self, name, current_city):
self.name = name
print('1')
# print(self.current_city) # 报错,__get__ 中 instance.__dict__[self._name],KeyError: 'current_city'
self.current_city = current_city # 调用 __set__ #注意这里就算不设置self.current_city,外面也是可以t.current_city访问的
print('5')
t = Traveller('the traveller name','a')
t.current_city = 'b'
t.current_city = 'd'
t.current_city = 'b' # ['a', 'b', 'd', 'b']
t.current_city = 'b' # 不增加
# HistoryTracedAttribute 类给Traveller对象添加的
print(t.cities_visited) # ['a', 'b', 'd'] # 不会调用 __get__
# print(t.__dict__)
# Traveller类自带的
print(t.name) # the traveller name # 不会调用 __get__
print(t.current_city) # d #访问的时候会调用 __get__ ,只有访问描述符才会调用
- robot中testsuite相关代码中,描述符类装饰器比较难以理解,简化源码方便理解如下
# 描述符类, 描述符是作为类的属性而不是实例属性存在的
# 将一个类中的方法装饰成类中的属性
class setter(object):
def __init__(self, method):
self.method = method
self.attr_name = '_setter__' + method.__name__
self.__doc__ = method.__doc__
print('setter: __init__:' ,method)
def __get__(self, instance, owner): # instance:TestSuite object ;owner:class TestSuite
if instance is None:
return self
print('setter: __get__:', instance, owner)
return getattr(instance, self.attr_name) # 返回的是TestSuite的attr_name
# try:
# print('setter: __get__:', instance, owner)
# return getattr(instance, self.attr_name)
# except AttributeError:
# raise AttributeError(self.method.__name__)
def __set__(self, instance, value):
if instance is None:
return
print('setter: __set__:', instance,value )
# 相当于TestSuite 执行 test(self, Keyword)
setattr(instance, self.attr_name, self.method(instance, value))
class SetterAwareType(type):
def __new__(cls, name, bases, dct):
slots = dct.get('__slots__')
if slots is not None:
for item in dct.values():
if isinstance(item, setter):
slots.append(item.attr_name)
return type.__new__(cls, name, bases, dct)
def with_metaclass(meta, *bases):
"""Create a base class with a metaclass."""
# This requires a bit of explanation: the basic idea is to make a
# dummy metaclass for one level of class instantiation that replaces
# itself with the actual metaclass.
class metaclass(type):
def __new__(cls, name, this_bases, d):
return meta(name, bases, d)
return type.__new__(metaclass, 'temporary_class', (), {})
class ModelObject(with_metaclass(SetterAwareType, object)):
__slots__ = [] # __slots__变量,来限制该class实例能添加的属性
def copy(self, **attributes):
pass
class Keyword(ModelObject):
@setter
def parent(self, parent):
"""Parent test suite, test case or keyword."""
if parent and parent is not self.parent:
self._sort_key = getattr(parent, '_child_sort_key', -1)
return parent
@setter
def tags(self, tags):
"""Keyword tags as a :class:`~.model.tags.Tags` object."""
return tags
class TestSuite(ModelObject):
def __init__(self, name='', doc='', metadata=None, source=None):
self.parent = None #: Parent suite. ``None`` with the root suite.
self._name = name
# test = setter(test) , setter是描述符类,描述符是作为类的属性而不是实例属性存在的
@setter
def test(self, Keyword):
print('TestSuite: test: 赋值的时候调用',Keyword)
return Keyword
# _setter__test = 'amize'
# 源码中都是如下的suites,tests,keywords等
# @setter
# def suites(self, suites):
# """Child suites as a :class:`~.TestSuites` object."""
# return TestSuites(self.__class__, self, suites)
#
# @setter
# def tests(self, tests):
# """Tests as a :class:`~.TestCases` object."""
# return TestCases(self.test_class, self, tests)
#
# @setter
# def keywords(self, keywords):
# """Suite setup and teardown as a :class:`~.Keywords` object."""
# return Keywords(self.keyword_class, self, keywords)
''' 以下不实例化也会打印
setter: __init__: <function Keyword.parent at 0x0000020B824D7E58>
setter: __init__: <function Keyword.tags at 0x0000020B824D7EE8>
setter: __init__: <function TestSuite.test at 0x0000020B82520048>'''
# 不执行下面的语句也会打印上面的
suite = TestSuite('name')
keyword = Keyword()
# @setter 保证suite.test必须赋值后才能运用,因为__get__ 返回的是__init__里面的值
# 这里会报错:'TestSuite' object has no attribute '_setter__test'
# print(suite.test)
# 同上,报错:'TestSuite' object has no attribute '_setter__test'
# print(suite._setter__test)
suite.test = keyword # 【1. setter: __set__:函数】 ,【2. TestSuite: test: 函数被调用】
# 已经赋值了 suite.test = keyword ,不能看class setter中的 __init__了,这里会报错:'Keyword' object has no attribute 'method'
# print(type(suite.test.method))
# _setter__test == suite.test ,区别是 print(suite.test)还要调用__get__
print(suite._setter__test)
print(suite.test) # 【1. setter: __get__ 调用】
print(suite.__dict__) # {'parent': None, '_name': 'name', '_setter__test': <__main__.Keyword object at 0x0000026458EF11C8>}
print(dir(suite))
- robot中testsuite相关代码中,描述符类装饰器比较难以理解,简化源码2方便理解如下
# 将一个类中的方法装饰成类中的属性
import copy
class setter(object):
def __init__(self, method):
self.method = method
self.attr_name = '_setter__' + method.__name__
self.__doc__ = method.__doc__
def __get__(self, instance, owner):
if instance is None:
return self
try:
return getattr(instance, self.attr_name)
except AttributeError:
raise AttributeError(self.method.__name__)
def __set__(self, instance, value):
if instance is None:
return
setattr(instance, self.attr_name, self.method(instance, value))
def with_metaclass(meta, *bases): # 虚拟元类,参数元类meta,返回元类metaclass
"""Create a base class with a metaclass."""
# This requires a bit of explanation: the basic idea is to make a
# dummy metaclass for one level of class instantiation that replaces
# itself with the actual metaclass.
class metaclass(type):
def __new__(cls, name, this_bases, d):
return meta(name, bases, d) # 常规的return type(name, bases, attr)
return type.__new__(metaclass, 'temporary_class', (), {}) # return __new__的情况 , 返回【类】,【参数:当前准备创建的类的对象,类的名字,类继承的父类集合,类的方法集合】
class SetterAwareType(type): # 元类
def __new__(cls, name, bases, dct):
slots = dct.get('__slots__')
if slots is not None:
for item in dct.values():
if isinstance(item, setter):
slots.append(item.attr_name)
return type.__new__(cls, name, bases, dct)
class ModelObject(with_metaclass(SetterAwareType, object)): # 还是元类
def copy(self, **attributes):
copied = copy.copy(self)
for name in attributes:
setattr(copied, name, attributes[name])
return copied
class Message(ModelObject):
"""A message created during the test execution.
Can be a log message triggered by a keyword, or a warning or an error
that occurred during parsing or test execution.
"""
@setter
def parent(self, parent):
if parent and parent is not getattr(self, 'parent', None):
self._sort_key = getattr(parent, '_child_sort_key', -1)
return parent