V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
zhouyin
V2EX  ›  Python

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

  •  
  •   zhouyin · 23 小时 35 分钟前 · 426 次点击

    有么有人看了并且看懂了 我发现一个不懂的地方 为什么通过__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 条附言  ·  22 小时 32 分钟前
    问题描述不准确

    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
       22 小时 32 分钟前
    问题描述不准确

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

    现在的问题是

    为什么

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

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