
有么有人看了并且看懂了 我发现一个不懂的地方 为什么通过__init__subclass 设置描述符 跟 通过元类 new 方法设置描述符生成的类 会在 set value 时 有区别
第一种会触发实例的__setattr__
第二种不会触发
最大的区别在 Field 类 setattr(instance, self.storage_name, value) 与 instance.dict[self.name] = value
这个是基于元类:
from collections.abc import Callable from typing import Any, NoReturn, get_type_hints # tag::CHECKED_FIELD[] class Field: def __init__(self, name: str, constructor: Callable) -> None: if not callable(constructor) or constructor is type(None): raise TypeError(f'{name!r} type hint must be callable') self.name = name self.storage_name = '_' + name # <1> self.cOnstructor= constructor def __get__(self, instance, owner=None): if instance is None: # <2> return self return getattr(instance, self.storage_name) # <3> def __set__(self, instance: Any, value: Any) -> None: if value is ...: value = self.constructor() else: try: value = self.constructor(value) except (TypeError, ValueError) as e: type_name = self.constructor.__name__ msg = f'{value!r} is not compatible with {self.name}:{type_name}' raise TypeError(msg) from e setattr(instance, self.storage_name, value) # <4> # end::CHECKED_FIELD[] # tag::CHECKED_META[] class CheckedMeta(type): def __new__(meta_cls, cls_name, bases, cls_dict): # <1> print(cls_dict.get('__slots__')) if '__slots__' not in cls_dict: # <2> print ("\n\n\nin __new__\n\n\n") slots = [] type_hints = cls_dict.get('__annotations__', {}) # <3> for name, constructor in type_hints.items(): # <4> field = Field(name, constructor) # <5> cls_dict[name] = field # <6> slots.append(field.storage_name) # <7> cls_dict['__slots__'] = slots # <8> return super().__new__( meta_cls, cls_name, bases, cls_dict) # <9> # end::CHECKED_META[] # tag::CHECKED_CLASS[] class Checked(metaclass=CheckedMeta): __slots__ = () # skip CheckedMeta.__new__ processing @classmethod def _fields(cls) -> dict[str, type]: return get_type_hints(cls) def __init__(self, **kwargs: Any) -> None: print(super().__class__.__name__) for name in self._fields(): value = kwargs.pop(name, ...) setattr(self, name, value) if kwargs: self.__flag_unknown_attrs(*kwargs) def __flag_unknown_attrs(self, *names: str) -> NoReturn: plural = 's' if len(names) > 1 else '' extra = ', '.join(f'{name!r}' for name in names) cls_name = repr(self.__class__.__name__) raise AttributeError(f'{cls_name} object has no attribute{plural} {extra}') def _asdict(self) -> dict[str, Any]: return { name: getattr(self, name) for name, attr in self.__class__.__dict__.items() if isinstance(attr, Field) } def __repr__(self) -> str: kwargs = ', '.join( f'{key}={value!r}' for key, value in self._asdict().items() ) return f'{self.__class__.__name__}({kwargs})' class Movie(Checked): title: str year: int box_office: float movie = Movie(title='The Godfather', year=1972, box_office=137) print(movie) print(movie.title) print(type(CheckedMeta),type(Checked),type(Movie)) # end::MOVIE_DEMO[] 下面是基于__init_subclass
from collections.abc import Callable # <1> from typing import Any, NoReturn, get_type_hints class Field: def __init__(self, name: str, constructor: Callable) -> None: # <2> if not callable(constructor) or constructor is type(None): # <3> raise TypeError(f'{name!r} type hint must be callable') self.name = name self.cOnstructor= constructor def __set__(self, instance: Any, value: Any) -> None: if value is ...: # <4> value = self.constructor() else: try: value = self.constructor(value) # <5> except (TypeError, ValueError) as e: # <6> type_name = self.constructor.__name__ msg = f'{value!r} is not compatible with {self.name}:{type_name}' raise TypeError(msg) from e instance.__dict__[self.name] = value # <7> # end::CHECKED_FIELD[] # tag::CHECKED_TOP[] class Checked: @classmethod def _fields(cls) -> dict[str, type]: # <1> return get_type_hints(cls) def __init_subclass__(subclass) -> None: # <2> super().__init_subclass__() # <3> for name, constructor in subclass._fields().items(): # <4> setattr(subclass, name, Field(name, constructor)) # <5> def __init__(self, **kwargs: Any) -> None: for name in self._fields(): # <6> value = kwargs.pop(name, ...) # <7> setattr(self, name, value) # <8> if kwargs: # <9> self.__flag_unknown_attrs(*kwargs) # <10> # end::CHECKED_TOP[] # tag::CHECKED_BOTTOM[] def __setattr__(self, name: str, value: Any) -> None: # <1> if name in self._fields(): # <2> cls = self.__class__ descriptor = getattr(cls, name) descriptor.__set__(self, value) # <3> else: # <4> self.__flag_unknown_attrs(name) def __flag_unknown_attrs(self, *names: str) -> NoReturn: # <5> plural = 's' if len(names) > 1 else '' extra = ', '.join(f'{name!r}' for name in names) cls_name = repr(self.__class__.__name__) raise AttributeError(f'{cls_name} object has no attribute{plural} {extra}') def _asdict(self) -> dict[str, Any]: # <6> return { name: getattr(self, name) for name, attr in self.__class__.__dict__.items() if isinstance(attr, Field) } def __repr__(self) -> str: # <7> kwargs = ', '.join( f'{key}={value!r}' for key, value in self._asdict().items() ) return f'{self.__class__.__name__}({kwargs})' class Movie(Checked): title: str year: int box_office: float movie = Movie(title='The Godfather', year=1972, box_office=137) print(movie.title) print(movie) try: # remove the "type: ignore" comment to see Mypy error movie.year = 'MCMLXXII' # type: ignore except TypeError as e: print(e) 1 zhouyin OP 问题描述不准确 CheckedMeta 如果有__setattr__方法 给描述符赋值时 也会触发该方法 现在的问题是 为什么 1.继承自 CheckedMeta 的类 也就是通过元类 __new__ 方法设置描述符生成的类 没有__setattr__方法 给描述符赋值时 会自动调用描述符的__set__方法 2.通过__init_subclass 方法设置描述符生成的类 没有__setattr__方法 给描述符赋值时 不会自动调用描述符的__set__方法 |
2 zhouyin OP 我好像找到了原因 1.___init_subclass__ 通过 setattr 给实例增加描述符属性 2.继承自自定义元类的类 通过元类的__new__方法 的第四个参数 class_dict 给实例增加描述符属性 具体的原因肯定在更底层代码 不把用户自定义描述符加到 class_dict 赋值时就不会触发描述符的__set__方法 |