第一种会触发实例的__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 #
self.constructor = constructor
def __get__(self, instance, owner=None):
if instance is None: #
return self
return getattr(instance, self.storage_name) #
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) #
# end::CHECKED_FIELD[]
# tag::CHECKED_META[]
class CheckedMeta(type):
def __new__(meta_cls, cls_name, bases, cls_dict): #
print(cls_dict.get('__slots__'))
if '__slots__' not in cls_dict: #
print ("\n\n\nin __new__\n\n\n")
slots = []
type_hints = cls_dict.get('__annotations__', {}) #
for name, constructor in type_hints.items(): #
field = Field(name, constructor) #
cls_dict[name] = field #
slots.append(field.storage_name) #
cls_dict['__slots__'] = slots #
return super().__new__(
meta_cls, cls_name, bases, cls_dict) #
# 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 #
from typing import Any, NoReturn, get_type_hints
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.constructor = constructor
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
instance.__dict__[self.name] = value #
# end::CHECKED_FIELD[]
# tag::CHECKED_TOP[]
class Checked:
@classmethod
def _fields(cls) -> dict[str, type]: #
return get_type_hints(cls)
def __init_subclass__(subclass) -> None: #
super().__init_subclass__() #
for name, constructor in subclass._fields().items(): #
setattr(subclass, name, Field(name, constructor)) #
def __init__(self, **kwargs: Any) -> None:
for name in self._fields(): #
value = kwargs.pop(name, ...) #
setattr(self, name, value) #
if kwargs: #
self.__flag_unknown_attrs(*kwargs) #
# end::CHECKED_TOP[]
# tag::CHECKED_BOTTOM[]
def __setattr__(self, name: str, value: Any) -> None: #
if name in self._fields(): #
cls = self.__class__
descriptor = getattr(cls, name)
descriptor.__set__(self, value) #
else: #
self.__flag_unknown_attrs(name)
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.title)
print(movie)
try:
# remove the "type: ignore" comment to see Mypy error
movie.year = 'MCMLXXII' # type: ignore
except TypeError as e:
print(e)