有么有人看了并且看懂了 我发现一个不懂的地方 为什么通过__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[]


```python
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)
举报· 103 次点击
登录 注册 站外分享
快来抢沙发
0 条回复  
返回顶部