Python元类有什么用?

piah890a  于 2023-01-08  发布在  Python
关注(0)|答案(9)|浏览(198)

对于不能以其他方式处理的元类,可以做些什么呢?
Alex Martelli说过有些任务没有元类是无法完成的,我想知道是哪些?

whlutmcx

whlutmcx1#

如果你想让类对象(而不是类对象的示例)具有“特殊的定制行为”,元类是必不可少的,因为对象的行为依赖于对象的类型上的特殊方法,而类对象的类型恰恰是元类的同义词。
例如,如果您希望类对象X的“print X”发出“Time is now 8:46 am”(8:46 am,或者更一般地说,当前时间),这就必须意味着type(x)(AKA X的元类)有一个特殊的定制__str__方法--类似地(使用各种适用的特殊方法),如果您希望为X + Y(其中X和Y都是类对象)或X[23](其中X也是类对象)等表达式给予意义。
大多数其他定制任务现在(在Python 2.6或更高版本中)更容易用类装饰器来实现,它可以在class语句结束后立即修改类对象,但还有一些情况是不可行的,因为修改必须在很早的时候进行,如果它们要产生任何效果的话(例如,设置或修改__slots__)。
在Python 3中,元类获得了一点额外的用处:元类现在可以选择性地指定要在class语句主体执行期间填充的Map对象(默认情况下,它是一个普通的dict)。这允许保留和使用类主体中名称绑定的 * 顺序 (而普通的dict会失去顺序),当类必须具有某种特定顺序的“字段”时(例如,将1:1Map到C struct、CSV文件或DB表中的行等),这有时是很好的--在Python 2. 中,这必须被冗余地指定(通常使用一个额外的class属性,该属性是一个序列,因此保持顺序),而Python 3元类的这个特性允许删除冗余。

uqjltbpv

uqjltbpv2#

为您的编程增加额外的灵活性:
但是根据这个Metaclass programming in Python,你可能(还)不需要它们

  • 元类的魔力比99%的用户都要大,如果你不知道你是否需要它们,你不需要(真正需要它们的人肯定知道他们需要它们,不需要解释为什么)。
  • --巨蟒大师蒂姆·彼得斯 *
9rygscc1

9rygscc13#

我经常使用元类,它们是工具箱中非常强大的工具,有时使用元类会比不使用元类更优雅,代码更少。
我发现自己最常使用元类的地方是在创建类的过程中对类属性进行后处理,例如,在适当的地方设置对象的name属性(就像Django ORM可能的工作方式):

class AutonamingType(type):
    def __init__(cls, name, bases, attrs):
        for k,v in attrs.iteritems():
            if getattr(v, '__autoname__', False):
                v.name = k

class Autonamer(object):
    __metaclass__ = AutonamingType

如果你把它作为一个工具,并且你正在使用一个类,这个类必须知道它的name,然后才能do_something()

class Foo(object):
    __autoname__ = True
    def __init__(self, name=None):
        self.name = name
    def do_something(self):
        if self.name is None:
            raise ValueError('name is None')
        # now, do something

它可以使代码的其余部分在以下方面有所不同:

class Bar(object):
    myfoo1 = Foo('myfoo1')
    myfoo2 = Foo('myfoo2')
    myfoo3 = Foo('myfoo3')

还有这个

class Baaz(Autonamer):
    myfoo1 = Foo()
    myfoo2 = Foo()
    myfoo3 = Foo()

从而减少重复(以及变量名和赋值名可能不同步的机会)。

zu0ti5jz

zu0ti5jz4#

如果您正在寻找使用元类机制的示例,可以阅读django.forms的源代码。
表单定义的声明性风格是通过元类实现的。

83qze16e

83qze16e5#

它们很少被需要,但是在您想要向对象的基本行为添加行为的地方非常有用--比较面向方面编程,或者在持久性框架(如Hibernate)中完成的插装。
例如,您可能需要一个类来持久化或记录每个新对象。

agxfikkp

agxfikkp6#

也许没有什么东西可以专门用元类来完成,但是对于一些人(包括我的)来说,这是一个可以使用的有趣的工具,只是要小心不要滥用,因为它可能很棘手。
例如,我在最近的一个项目中使用了元编程,那是一个OpenOffice计算表,它使用一些pyUNO宏生成一些包含信息的文件,其中一个表向用户显示要填充的信息,其他的表可以用来描述元素的种类和属性,然后用户可以选择元素的数量和类型。并生成文件。宏将根据每张表上的配置通过元编程创建一个类。然后,用户可以示例化每个类并生成对象。
它可以不用元编程来完成,但对我来说,使用元编程功能来完成它似乎是很自然的。

idv4meu8

idv4meu87#

看一下Django源代码--例如元类被用来生成模型。
http://code.djangoproject.com/wiki/DynamicModels
在内部,Django使用元类来创建基于源代码中提供的类的模型,这意味着Django接收类的描述,并使用类的描述来创建模型,而不是将类作为实际的模型。

b1uwtaje

b1uwtaje8#

第一个评论者说元类here的使用是帮助他追踪已经发生了(很多)天的意外错误的“一块宝石”。

a9wyjsp7

a9wyjsp79#

有两件事要记住:

  • 你可能不需要它们。它们听起来很酷,但大多数人实际上并不需要它们。
  • Python通过type()支持动态类,通过super().foo()支持多重继承和方法链接,甚至可以在类中添加函数,在运行时绑定到类。

所以,大多数时候元类都有些矫枉过正。
然而,我发现它们非常有用的一件事是从多个类的属性(* 而不仅仅是方法 *)组装行为。
例如,将多个需求放在一起的验证框架:

  • http响应应该具有
  • 状态代码-在子类中指定的值
  • 内容类型-在子类中指定的值
  • 此外,检查预期成功的 class 的html响应应该:
  • 内容类型中状态代码. html应为200
  • 期望在别处...指定标题值
  • 然后测试示例只需要指定确切的标题。

元类系统可以将所有这些需求组合在一起,这样您的单元测试只需要指定标题,并且类“知道”它必须抓取<title>的页面,检查内容类型并检查响应代码。
这里有一个使用元类的例子。我们打印的信件有不同的页眉模板,这些模板被组装在一起。页脚可以使用相同的机制,但是使用mixins a la Python’s super() considered super!。这两种方法--组装类级属性和通过super链接__init__以外的方法--可以很好地一起工作。

def get_annos(v):
    return getattr(v, "__annotations__", {})

class MailMeta(type):

    def get_annos(cls: type, ancestors_in: list[type]):
        res = {}
        for acls in ancestors_in:
            res.update(**get_annos(acls))
        return res

    def _calc_annotations(cls, li_bases):
        """ concatenate the annotations for the class and ancestors """
        annos = cls.get_annos(li_bases + [cls])
        cls.annos = annos

    def _concat_template(cls, varname, bases : list[type]):
        """this glues together some templates
        assembling complex attributes from multiple classes is 
        something metaclasses are uniquely good at.
        how to do this can be tricky though!  "Unordered" attributes
        like sets or dictionaries can be easier but use `copy` 
        liberally.      
        """

        li = []
        cls_var = f"cls_{varname}"
        for acls in bases:
            v = getattr(acls, cls_var, "")
            if not v:
                continue
            if v in li:
                continue

            li.append(v)

        all_ = "".join(li)
        setattr(cls, varname, all_)

    def __init__(cls, name, bases, attrs, **kwargs):
        """assemble together some of the attributes from the mro"""

        li_bases2current = [
            v for v in list(reversed(cls.mro())) if not v is object and not v is cls
        ]

        cls._calc_annotations(li_bases2current)
        cls._concat_template("header", [cls] + list(reversed(li_bases2current)) )

        super().__init__(name, bases, attrs)
        

class Mail(metaclass=MailMeta):
    """ specify some base behavior"""
    name : str  # mail always need to state who it is for
    salutation : str 

    cls_header : str = ""
    template : str
    footer : str = ""

    def _set_annotations(self, **kwargs):
        """"""
        undefined = object()

        for attrname in self.annos.keys():
            v = kwargs.get(attrname,undefined)
            if v is not undefined:
                setattr(self, attrname, v)
            else:
                #maybe its set on the classes
                v = getattr(self, attrname,undefined)
                if v is undefined:
                    raise ValueError(f"{attrname} is undefined")

    def __init__(self, **kwargs):
        """ track which variables we were expecting"""
        self._set_annotations(**kwargs)
        self.fulltemplate = "\n".join([self.header,self.template,self.get_footer()])

    def get_footer(self):
        return f"\n{self.footer}"

    def print(self):
        print(f"\n{'-' * 40}\n{self.__class__.__name__}:\n")

        di = dict(**vars(self.__class__))
        di.update(**vars(self))
        print(self.fulltemplate % (di))        

class Email:
    cls_header = "Dear %(salutation)s %(name)s,"
    cls_footer = "Sincerely,"

class Spam(Email, Mail):
    amount : int
    template = "You have won a prize of $%(amount)s"

class Signature:
    t_footer = "%(from_)s"
    from_ : str

    def get_footer(self):
        return super().get_footer() + self.t_footer % vars(self)

class TrustedSpam(Signature, Spam):
    "Trusted cuz signed"

    template = "You have won $%(amount)s.  This is a once in a lifetime opportunity."

    def get_footer(self):
        return super().get_footer() + "\nPlease trust me"

class SnailMailTrustedSpamForDoctors(TrustedSpam):
    "Doctors like to be called Dr."

    salutation = "Dr."
    cls_header = "101 Honest Fellows Street\n\n"

try:
    mail = Mail(saluation="Ms.", template="")
except (ValueError,) as exc: 
    print(f"ooops.  forgot `name`.              All good: {exc=} ✅")

try:
    mail = Spam(salutation="Ms.", name="Johnson")
except (Exception,) as exc: 
    print(f"ooops.  forgot `amount` on a Spam.  All good: {exc=} ✅")

mail = Spam(salutation="Ms.", amount=2000, name="Johnson")
mail.print()

mail = TrustedSpam(salutation="Ms.", amount=2000, name="Johnson", from_="Joe Trustworthy")
mail.print()

mail = SnailMailTrustedSpamForDoctors(amount=10000, name="Gullible", from_="Joe Trustworthy")
mail.print()

并且输出:

ooops.  forgot `name`.              All good: exc=ValueError('name is undefined') ✅
ooops.  forgot `amount` on a Spam.  All good: exc=ValueError('amount is undefined') ✅

----------------------------------------
Spam:

Dear Ms. Johnson,
You have won a prize of $2000


----------------------------------------
TrustedSpam:

Dear Ms. Johnson,
You have won $2000.  This is a once in a lifetime opportunity.

Joe Trustworthy
Please trust me

----------------------------------------
SnailMailTrustedSpamForDoctors:

101 Honest Fellows Street

Dear Dr. Gullible,
You have won $10000.  This is a once in a lifetime opportunity.

Joe Trustworthy
Please trust me

相关问题