首页 注册 登录
V2EX = way to explore V2EX 是一个关于分享和探索的地方
现在注册 已注册用户请 登录
V2EX Python

fluent Python 第 2 版第 24 章 __init_subclass 与 meta class 的__setattr__区别

zhouyin · 2024 年 12 月 26 日 · 1672 次点击
这是一个创建于 380 天前的主题,其中的信息可能已经有所发展或是发生改变。

有么有人看了并且看懂了 我发现一个不懂的地方 为什么通过__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 条附言 · 2024 年 12 月 26 日
问题描述不准确

CheckedMeta 如果有__setattr__方法 给描述符赋值时 也会触发该方法

现在的问题是

为什么

1.继承自 CheckedMeta 的类 也就是通过元类 __new__ 方法设置描述符生成的类 没有__setattr__方法 给描述符赋值时 会自动调用描述符的__set__方法

2.通过__init_subclass 方法设置描述符生成的类 没有__setattr__方法 给描述符赋值时 不会自动调用描述符的__set__方法
2 条回复 2024年12月26日 13:19:23 +08:00
zhouyin
1
zhouyin
OP
2024 年 12 月 26 日
问题描述不准确

CheckedMeta 如果有__setattr__方法 给描述符赋值时 也会触发该方法

现在的问题是

为什么

1.继承自 CheckedMeta 的类 也就是通过元类 __new__ 方法设置描述符生成的类 没有__setattr__方法 给描述符赋值时 会自动调用描述符的__set__方法

2.通过__init_subclass 方法设置描述符生成的类 没有__setattr__方法 给描述符赋值时 不会自动调用描述符的__set__方法
zhouyin
2
zhouyin
OP
2024 年 12 月 26 日
我好像找到了原因
1.___init_subclass__ 通过 setattr 给实例增加描述符属性
2.继承自自定义元类的类 通过元类的__new__方法 的第四个参数 class_dict 给实例增加描述符属性 具体的原因肯定在更底层代码 不把用户自定义描述符加到 class_dict 赋值时就不会触发描述符的__set__方法
关于 · 帮助文档 · 自助推广系统 · 博客 · API · FAQ · Solana · 1589 人在线 最高记录 6679 · Select Language 创意工作者们的社区 World is powered by solitude VERSION: 3.9.8.5 · 25ms · UTC 16:23 · PVG 00:23 · LAX 08:23 · JFK 11:23
♥ Do have faith in what you're doing.

AltStyle によって変換されたページ (->オリジナル) /