Python中的Classes和Metaclasses详解

类和对象

类和函数一样都是Python中的对象。当一个类定义完成之后,Python将创建一个“类对象”并将其赋值给一个同名变量。类是type类型的对象(是不是有点拗口?)。

类对象是可调用的(callable,实现了 __call__方法),并且调用它能够创建类的对象。你可以将类当做其他对象那么处理。例如,你能够给它们的属性赋值,你能够将它们赋值给一个变量,你可以在任何可调用对象能够用的地方使用它们,比如在一个map中。事实上当你在使用map(str,[1,2,3])的时候,是将一个整数类型的list转换为字符串类型的list,因为str是一个类。可以看看下面的代码:

>>> class C(object):
...   def __init__(self,s):
...       print s
...
>>> myclass = C
>>> type(C)
<type 'type'>
>>> type(myclass)
<type 'type'>
>>> myclass(2)
2
<__main__.C object at 0x10e2bea50>
>>> map(myclass,3])
1
2
3
[<__main__.C object at 0x10e2be9d0>,<__main__.C object at 0x10e2bead0>,<__main__.C object at 0x10e2beb10>]
>>> map(C,3])
1
2
3
[<__main__.C object at 0x10e2be950>,<__main__.C object at 0x10e2beb50>,<__main__.C object at 0x10e2beb90>]
>>> C.test_attribute = True
>>> myclass.test_attribute
True

正因如此,Python中的“class”关键字不像其他语言(例如C++)那样必须出现在代码main scope中。在Python中,它能够在一个函数中嵌套出现,举个例子,我们能够这样在函数运行的过程中动态的创建类。看代码:

>>> def make_class(class_name):
...   class C(object):
...       def print_class_name(self):
...           print class_name
...   C.__name__ = class_name
...   return C
...
>>> C1,C2 = map(make_class,["C1","C2"])
>>> c1,c2 = C1(),C2()
>>> c1.print_class_name()
C1
>>> c2.print_class_name()
C2
>>> type(c1)
<class '__main__.C1'>
>>> type(c2)
<class '__main__.C2'>
>>> c1.print_class_name.__closure__
(<cell at 0x10ab6dbe8: str object at 0x10ab71530>,)

请注意,在这里通过make_class创建的两个类是不同的对象,因此通过它们创建的对象就不属于同一个类型。正如我们在装饰器中做的那样,我们在类被创建之后手动设置了类名。同样也请注意所创建类的print_class_name方法在一个closure cell中捕捉到了类的closure和class_name。如果你对closure的概念还不是很清楚,那么最好去看看前篇,复习一下closures和decorators相关的内容。
Metaclasses

如果类是能够制造对象的对象,那制造类的对象又该叫做什么呢(相信我,这并不是一个先有鸡还是先有蛋的问题)?答案是元类(Metaclasses)。大部分常见的基础元类都是type。当输入一个参数时,type将简单的返回输入对象的类型,这就不涉及元类。然而当输入三个参数时,type将扮演元类的角色,基于输入参数创建一个类并返回。输入参数相当简单:类名,父类及其参数的字典。后面两者可以为空,来看一个例子:
 

>>> MyClass = type("MyClass",(object,),{"my_attribute": 0})
>>> type(MyClass)
<type 'type'>
>>> o = MyClass()
>>> o.my_attribute
0

特别注意第二个参数是一个tuple(语法看起来很奇怪,以逗号结尾)。如果你需要在类中安排一个方法,那么创建一个函数并且将其以属性的方式传递作为第三个参数,像这样:
 

>>> def myclass_init(self,my_attr):
...   self.my_attribute = my_attr
...
>>> MyClass = type("MyClass",{"my_attribute": 0,"__init__": myclass_init})
>>> o = MyClass("Test")
>>> o.my_attribute
'Test'
>>> o.__init__
<bound method MyClass.myclass_init of <__main__.MyClass object at 0x10ab72150>>

我们可以通过一个可调用对象(函数或是类)来自定义元类,这个对象需要三个输入参数并返回一个对象。这样一个元类在一个类上实现只要定义了它的__metaclass__属性。第一个例子,让我们做一些有趣的事情看看我们能够用元类做些什么:
 

>>> def mymetaclass(name,parents,attributes):
...   return "Hello"
...
>>> class C(object):
...   __metaclass__ = mymetaclass
...
>>> print C
Hello
>>> type(C)
<type 'str'>

请注意以上的代码,C只是简单地将一个变量引用指向了字符串“Hello”。当然了,没人会在实际中写这样的代码,这只是为了演示元类的用法而举的一个简单例子。接下来我们来做一些更有用的操作。在本系列的第二部分我们曾看到如何使用装饰器类来记录目标类每个方法的输出,现在我们来做同样的事情,不过这一次我们使用元类。我们借用之前的装饰器定义:

def log_everything_metaclass(class_name,attributes):
  print "Creating class",class_name
  myattributes = {}
  for name,attr in attributes.items():
    myattributes[name] = attr
    if hasattr(attr,'__call__'):
      myattributes[name] = logged("%b %d %Y - %H:%M:%S",class_name + ".")(attr)
  return type(class_name,myattributes)
 
class C(object):
  __metaclass__ = log_everything_metaclass
 
  def __init__(self,x):
    self.x = x
 
  def print_x(self):
    print self.x
 
# Usage:
print "Starting object creation"
c = C("Test")
c.print_x()
 
# Output:
Creating class C
Starting object creation
- Running 'C.__init__' on Aug 05 2013 - 13:50:58
- Finished 'C.__init__',execution time = 0.000s
- Running 'C.print_x' on Aug 05 2013 - 13:50:58
Test
- Finished 'C.print_x',execution time = 0.000s

如你所见,类装饰器与元类有着很多共同点。事实上,任何能够用类装饰器完成的功能都能够用元类来实现。类装饰器有着很简单的语法结构易于阅读,所以提倡使用。但就元类而言,它能够做的更多,因为它在类被创建之前就运行了,而类装饰器则是在类创建之后才运行的。记住这点,让我们来同时运行一下两者,请注意运行的先后顺序:
 

def my_metaclass(class_name,attributes):
  print "In metaclass,creating the class."
  return type(class_name,attributes)
 
def my_class_decorator(class_):
  print "In decorator,chance to modify the class."
  return class_
 
@my_class_decorator
class C(object):
  __metaclass__ = my_metaclass
 
  def __init__(self):
    print "Creating object."
 
c = C()
 
# Output:
In metaclass,creating the class.
In decorator,chance to modify the class.
Creating object.

元类的一个实际用例

让我们来考虑一个更有用的实例。假设我们正在构思一个类集合来处理MP3音乐文件中使用到的ID3v2标签Wikipedia。简而言之,标签由帧(frames)组成,而每帧通过一个四字符的识别码(identifier)进行标记。举个例子,TOPE标识了原作者帧,TOAL标识了原专辑名称等。如果我们希望为每个帧类型写一个单独的类,并且允许ID3v2标签库用户自定义他们自己的帧类。那么我们可以使用元类来实现一个类工厂模式,具体实现方式可以这样:

frametype_class_dict = {}
 
class ID3v2FrameClassFactory(object):
  def __new__(cls,class_name,attributes):
    print "Creating class",class_name
    # Here we could add some helper methods or attributes to c
    c = type(class_name,attributes)
    if attributes['frame_identifier']:
      frametype_class_dict[attributes['frame_identifier']] = c
    return c
 
  @staticmethod
  def get_class_from_frame_identifier(frame_identifier):
    return frametype_class_dict.get(frame_identifier)
 
class ID3v2Frame(object):
  frame_identifier = None
  __metaclass__ = ID3v2FrameClassFactory
  pass
 
class ID3v2TitleFrame(ID3v2Frame):
  __metaclass__ = ID3v2FrameClassFactory
  frame_identifier = "TIT2"
 
class ID3v2CommentFrame(ID3v2Frame):
  __metaclass__ = ID3v2FrameClassFactory
  frame_identifier = "COMM"
 
title_class = ID3v2FrameClassFactory.get_class_from_frame_identifier('TIT2')
comment_class = ID3v2FrameClassFactory.get_class_from_frame_identifier('COMM')
print title_class
print comment_class
 
# Output:
Creating class ID3v2Frame
Creating class ID3v2TitleFrame
Creating class ID3v2CommentFrame
<class '__main__.ID3v2TitleFrame'>
<class '__main__.ID3v2CommentFrame'>

当然了,以上的代码同样可以用类装饰器来完成,以下是对应代码:

frametype_class_dict = {}
 
class ID3v2FrameClass(object):
  def __init__(self,frame_id):
    self.frame_id = frame_id
 
  def __call__(self,cls):
    print "Decorating class",cls.__name__
    # Here we could add some helper methods or attributes to c
    if self.frame_id:
      frametype_class_dict[self.frame_id] = cls
    return cls
 
  @staticmethod
  def get_class_from_frame_identifier(frame_identifier):
    return frametype_class_dict.get(frame_identifier)
 
@ID3v2FrameClass(None)
class ID3v2Frame(object):
  pass
 
@ID3v2FrameClass("TIT2")
class ID3v2TitleFrame(ID3v2Frame):
  pass
 
@ID3v2FrameClass("COMM")
class ID3v2CommentFrame(ID3v2Frame):
  pass
 
title_class = ID3v2FrameClass.get_class_from_frame_identifier('TIT2')
comment_class = ID3v2FrameClass.get_class_from_frame_identifier('COMM')
print title_class
print comment_class
 
Decorating class ID3v2Frame
Decorating class ID3v2TitleFrame
Decorating class ID3v2CommentFrame
<class '__main__.ID3v2TitleFrame'>
<class '__main__.ID3v2CommentFrame'>

如你所见,我们可以直接给装饰器传递参数,而元类却不能。给元类传递参数必须通过属性。正因如此,这里装饰器的解决方案更为清晰,同时也更容易维护。然而,同时也需要注意当装饰器被调用的时候,类已经建立完毕,这意味着此时就不能够修改其属性了。例如,一旦类建立完成,你就不能够修改__doc__。来看实际例子:

>>> def mydecorator(cls):
...   cls.__doc__ = "Test!"
...   return cls
...
>>> @mydecorator
... class C(object):
...   """Docstring to be replaced with Test!"""
...   pass
...
Traceback (most recent call last):
 File "<stdin>",line 2,in <module>
 File "<stdin>",in mydecorator
AttributeError: attribute '__doc__' of 'type' objects is not writable
>>> def mymetaclass(cls,attrs):
...   attrs['__doc__'] = 'Test!'
...   return type(cls,attrs)
...
>>> class D(object):
...   """Docstring to be replaced with Test!"""
...   __metaclass__ = mymetaclass
...
>>> D.__doc__
'Test!'

通过type生成元类

正如我们所说,最基本的元类就是type并且类通常都是type类型。那么问题很自然来了,type类型本身是一种什么类型呢?答案也是type。这也就是说type就是它自身的元类。虽然听起来有点诡异,但这在Python解释器层面而言是可行的。

type自身就是一个类,并且我们可以从它继承出新类。这些生成的类也能作为元类,并且使用它们的类可以得到跟使用type一样的类型。来看以下的例子:
 

>>> class meta(type):
...   def __new__(cls,attributes):
...       print "meta.__new__"
...       return super(meta,cls).__new__(cls,attributes)
...   def __call__(self,*args,**kwargs):
...       print "meta.__call__"
...       return super(meta,self).__call__(*args,**kwargs)
...
>>> class C(object):
...   __metaclass__ = meta
...
meta.__new__
>>> c = C()
meta.__call__
>>> type(C)
<class '__main__.meta'>

请注意当类创建对象时,元类的__call__函数就被调用,进而调用type.__call__创建对象。在下一节,我们将把上面的内容融合在一起。
要点集合

假定一个类C自己的元类为my_metaclass并被装饰器my_class_decorator装饰。并且,假定my_metaclass本身就是一个类,从type生成。让我们将上面提到的内容融合到一起做一个总结来显示C类以及它的对象都是怎么被创建的。首先,让我们来看看代码:

class my_metaclass(type):
  def __new__(cls,attributes):
    print "- my_metaclass.__new__ - Creating class instance of type",cls
    return super(my_metaclass,attributes)
 
  def __init__(self,attributes):
    print "- my_metaclass.__init__ - Initializing the class instance",self
    super(my_metaclass,self).__init__(self)
 
  def __call__(self,**kwargs):
    print "- my_metaclass.__call__ - Creating object of type ",self
    return super(my_metaclass,**kwargs)
 
def my_class_decorator(cls):
  print "- my_class_decorator - Chance to modify the class",cls
  return cls
 
@my_class_decorator
class C(object):
  __metaclass__ = my_metaclass
 
  def __new__(cls):
    print "- C.__new__ - Creating object."
    return super(C,cls).__new__(cls)
 
  def __init__(self):
    print "- C.__init__ - Initializing object."
 
c = C()
print "Object c =",c

现在,你可以花几分钟时间测试一下你的理解,并且猜一猜打印输出的顺序。

首先,让我们来看看Python的解释器是如何阅读这部分代码的,然后我们会对应输出来加深我们的理解。

1. Python首先看类声明,准备三个传递给元类的参数。这三个参数分别为类名(class_name),父类(parent)以及属性列表(attributs)。

2. Python会检查__metaclass__属性,如果设置了此属性,它将调用metaclass,传递三个参数,并且返回一个类。

3. 在这个例子中,metaclass自身就是一个类,所以调用它的过程类似创建一个新类。这就意味着my_metaclass.__new__将首先被调用,输入四个参数,这将新建一个metaclass类的实例。然后这个实例的my_metaclass.__init__将被调用调用结果是作为一个新的类对象返回。所以此时C将被设置成这个类对象。

4. 接下来Python将查看所有装饰了此类的装饰器。在这个例子中,只有一个装饰器。Python将调用这个装饰器,将从元类哪里得到的类传递给它作为参数。然后这个类将被装饰器返回的对象所替代。

5. 装饰器返回的类类型与元类设置的相同。

6. 当类被调用创建一个新的对象实例时,因为类的类型是metaclass,因此Python将会调用元类的__call__方法。在这个例子中,my_metaclass.__call__只是简单的调用了type.__call__,目的是创建一个传递给它的类的对象实例。

7. 下一步type.__call__通过C.__new__创建一个对象。

8. 最后type.__call__通过C.__new__返回的结果运行C.__init__。

9. 返回的对象已经准备完毕。

所以基于以上的分析,我们可以看到调用的顺序如下:my_metaclass.__new__首先被调用,然后是my_metaclass.__init__,然后是my_class_decorator。至此C类已经准备完毕(返回结果就是C)。当我们调用C来创建一个对象的时候,首先会调用my_metaclass.__call__(任何对象被创建的时候,Python都首先会去调用其类的__call__方法),然后C.__new__将会被type.__call__调用(my_metaclass.__call__简单调用了type.__call__),最后是C.__init__被调用。现在让我们来看看输出:
 

- my_metaclass.__new__ - Creating class instance of type <class '__main__.my_metaclass'>
- my_metaclass.__init__ - Initializing the class instance <class '__main__.C'>
- my_class_decorator - Chance to modify the class <class '__main__.C'>
- my_metaclass.__call__ - Creating object of type <class '__main__.C'>
- C.__new__ - Creating object.
- C.__init__ - Initializing object.
Object c = <__main__.C object at 0x1043feb90> <class '__main__.C'>

关于元类多说几句

元类,一门强大而晦涩的技法。在GitHub上搜索__metaclass__得到的结果多半是指向”cookbook”或其他Python教学材料的链接。一些测试用例(诸如Jython中的一些测试用例),或是其他一些写有__metaclass__ = type的地方只是为了确保新类被正常使用了。坦白地说,这些用例都没有真正地使用元类。过滤了下结果,我只能找到两个地方真正使用了元类:ABCMeta和djangoplugins。

ABCMeta是一个允许注册抽象基类的元类。如果想了解多些请查看其官方文档,本文将不会讨论它。

对于djangoplugins而言,基本的思想是基于这篇文章article on a simple plugin framework for Python,使用元类是为了创建一个插件挂载系统。我并没有对其有深入的研究,不过我感觉这个功能可以使用装饰器来实现。如果你有相关的想法请在 本文后留言。
总结笔记

通过理解元类能够帮助我们更深入的理解Python中类和对象的行为,现实中使用它们的情况可能比文中的例子要复杂得多。大部分元类完成的功能都可以使用装饰器来实现。所以当你的第一直觉是使用元类来解决你的问题,那么请你停下来先想想这是否必要。如果不是非要使用元类,那么请三思而行。这会使你的代码更易懂,更易调试和维护。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


本文从多个角度分析了vi编辑器保存退出命令。我们介绍了保存和退出vi编辑器的命令,以及如何撤销更改、移动光标、查找和替换文本等实用命令。希望这些技巧能帮助你更好地使用vi编辑器。
Python中的回车和换行是计算机中文本处理中的两个重要概念,它们在代码编写中扮演着非常重要的角色。本文从多个角度分析了Python中的回车和换行,包括回车和换行的概念、使用方法、使用场景和注意事项。通过本文的介绍,读者可以更好地理解和掌握Python中的回车和换行,从而编写出更加高效和规范的Python代码。
SQL Server启动不了错误1067是一种比较常见的故障,主要原因是数据库服务启动失败、权限不足和数据库文件损坏等。要解决这个问题,我们需要检查服务日志、重启服务器、检查文件权限和恢复数据库文件等。在日常的数据库运维工作中,我们应该时刻关注数据库的运行状况,及时发现并解决问题,以确保数据库的正常运行。
信息模块是一种可重复使用的、可编程的、可扩展的、可维护的、可测试的、可重构的软件组件。信息模块的端接需要从接口设计、数据格式、消息传递、函数调用等方面进行考虑。信息模块的端接需要满足高内聚、低耦合的原则,以保证系统的可扩展性和可维护性。
本文从电脑配置、PyCharm版本、Java版本、配置文件以及程序冲突等多个角度分析了Win10启动不了PyCharm的可能原因,并提供了解决方法。
本文主要从多个角度分析了安装SQL Server 2012时可能出现的错误,并提供了解决方法。
Pycharm是一款非常优秀的Python集成开发环境,它可以让Python开发者更加高效地进行代码编写、调试和测试。在Pycharm中设置解释器非常简单,我们可以通过创建新项目、修改项目解释器、设置全局解释器等多种方式进行设置。
Python中有多种方法可以将字符串转换为整数,包括使用int()函数、try-except语句、正则表达式、map()函数、ord()函数和reduce()函数。在实际应用中,应根据具体情况选择最合适的方法。
本文介绍了导入CSV文件的多种方法,包括使用Excel、Python和R等工具。同时,还介绍了导入CSV文件时需要注意的一些细节和问题。CSV文件是数据处理和分析中不可或缺的一部分,希望本文能够对读者有所帮助。
mongodb是一种新型的数据库,它采用了面向文档的数据模型,具有灵活性、高性能和高可用性等优势。但是,mongodb也存在数据结构混乱、安全性和学习成本高等问题。
当Python运行不了时,我们应该从代码、Python环境、操作系统和硬件设备等多个角度来排查问题,并采取相应的解决措施。
Python列表是一种常见的数据类型,排序是列表操作中的一个重要部分。本文介绍了Python列表降序排序的方法,包括使用sort()函数、sorted()函数以及自定义函数进行排序。使用sort()函数可以简单方便地实现降序排序,但会改变原始列表的顺序;使用sorted()函数可以保留原始列表的顺序,但需要创建一个新的列表;使用自定义函数可以灵活地控制排序的方式,但需要编写额外的代码。
本文介绍了如何使用Python输入一段英文并统计其中的单词个数,从去除标点符号、忽略单词大小写、排除常用词汇等多个角度进行了分析。此外,还介绍了使用NLTK库进行单词统计的方法。
虚拟环境可以帮助我们在同一台机器上运行不同版本的Python、安装不同的Python包,并且不会相互影响。创建虚拟环境的命令是python3 -m venv myenv,进入虚拟环境的命令是source myenv/bin/activate,退出虚拟环境的命令是deactivate。在虚拟环境中可以使用pip安装包,也可以使用Python运行程序。
本文从XHR对象、fetch API和jQuery三个方面分析了JS获取响应状态的方法及其应用。以上三种方法都可以轻松地发送HTTP请求,并处理响应数据。
桌面的命令包括常见的操作命令、系统命令、批处理命令以及第三方应用程序提供的命令。我们可以通过鼠标右键点击桌面、创建快捷方式、创建批处理文件等方式来运用这些命令,从而更好地管理计算机,提高工作效率。
本文分析了应用程序闪退的多个原因,包括应用程序本身存在问题、手机或平板电脑系统问题、硬件问题、网络问题和其他原因。同时,本文提供了解决闪退问题的多种方式,包括更新或卸载重新下载应用程序、升级系统或进行修复、清理手机缓存、清理不必要的文件或者是更换电池等方式来解决、确保网络信号的稳定性、注意用户隐私和安全问题。
本文介绍了使用Python下载图片的多种方法,包括使用Python标准库urllib.request、第三方库requests、多线程和异步IO。这些方法在不同情况下都有它们的优缺点。使用这些方法,我们可以轻松地将网络上的图片下载到本地,方便我们在离线状态下查看或处理这些图片。
MySQL数据文件是指存储MySQL数据库中数据的文件,存储位置的选择对数据库的性能、可靠性和安全性都有着重要的影响。本文从存储位置的选择、存储设备的选择、存储空间的管理和存储位置的安全性等多个角度对MySQL数据文件的存储位置进行分析,最后得出需要根据实际情况综合考虑多个因素,选择合适的存储位置和存储设备,并进行有效的存储空间管理和安全措施的结论。
AS400是一种主机操作系统,每个库都包含多个表。查询库表总数是一项基本任务。可以使用命令行、系统管理界面以及数据库管理工具来查询库表总数。查询库表总数可以帮助用户更好地管理和优化数据,包括规划数据存储、优化查询性能以及管理空间资源。