其他语言(例如C ++)实际上在其代码中没有“类”的概念-它主要是针对编译器如何布局和连接此类对象的指令。这样,所有属性和方法都链接到相同的值和代码。所有检查和约束都得到执行;并且最终可执行文件中不需要类对象。甚至支持反射的Java也不会将类实际上像对象一样对待-它仅存储其元数据并提供操作它的接口。 另一方面,Python则没有这种区别:类可以作为参数传递,作为返回值返回,具有属性等,并且与函数类似:
>>> class A: ... pass >>> A <class 'A'> >>> A.__name__ 'A' >>> def instantiate(cls): ... return cls() >>> a = instantiate(A) >>> a <A object at 0x...> >>> def create_class(x): ... class A: ... def f(self): ... return x ... return A >>> A = create_class(1) >>> a = A() >>> a.f() 1
Decorating Classes 有趣的含义是,就像函数一样,可以Decorating Classes,您只需在顶部放置装饰器,然后在创建类的那一刻,将其通过管道传递给装饰器,并将返回的内容绑定到其名称上。虽然通常用包装器代替函数,但是类是就地修改的,例如遍历其方法并替换它们。例如:
>>> def double(f): ... def wrapper(*args, **kwargs): ... return 2 * f(*args, **kwargs) ... return wrapper >>> def double_all(cls): ... for key, value in cls.__dict__.items(): ... if key.startswith('_') or not callable(value): ... continue ... setattr(cls, key, double(value)) ... return cls >>> @double_all ... class A: ... def inc(self, x): ... return x + 1 ... def add(self, x, y): ... return x + y >>> a = A() >>> a.inc(1) 4 # (1 + 1) * 2 >>> a.add(1, 2) 6 # (1 + 2) * 2
这很简单:我们遍历该类的内容__dict__,跳过任何私有或魔术方法以及不可调用的属性,然后将其余部分替换为自身的两倍版本。一些注意事项:
__dict__
A.__dict__['x'] =1.
vars(o)
o.__dict__
type(o)
o.__class__
cls
one
'NoneType' object is not callable
import threading def threadsafe(cls): cls._lock = threading.Lock() for key, value in cls.__dict__.items(): if key.startswith('_') or not callable(value): continue setattr(cls, key, synchronized(value, cls._lock)) return cls def synchronized(f, lock): def wrapper(*args, **kwargs): with lock: return f(*args, **kwargs) return wrapper
我们绝对不希望一个线程在另一个文件写入文件的同时读取文件。因此您最好使文件访问同步。请注意,这意味着所有文件访问都是同步的,因为锁是在类级别定义的,即使在单独文件上工作的线程可以同时进行业务。我们可以使用稍微复杂一些的方法来解决此问题:
import threading def threadsafe(cls): for key, value in cls.__dict__.items(): if key.startswith('_') or not callable(value): continue setattr(cls, key, synchronized(value)) init = getattr(cls, '__init__', None) if init: def __init__(self, *args, **kwargs): init(self, *args, **kwargs) self._lock = threading.Lock() else: def __init__(self, *args, **kwargs): super(cls, self).__init__(*args, **kwargs) self._lock = threading.Lock() cls.__init__ = __init__ return cls def synchronized(f): def wrapper(self, *args, **kwargs): with self._lock: return f(self, *args, **kwargs) return wrapper
该代码的作用是将所有方法替换为与每个实例同步的自身版本_lock;并通过将其替换为init调用原始实例的实例将其添加到每个实例中,并设置该实例,_lock或者如果没有实例,则添加该实例。同样,请注意: Python 3使您可以super不带参数地进行调用:它可以自动知道您要表示的类和实例,但是可以通过检查定义其的类的上下文来实现。由于我们是在此上下文之外定义的,因此我们必须明确提供它们。 我们通过类访问方法,因此我们使它们不受约束。这意味着,当我们调用init替代方法init或f时wrapper,这是不够的,*args并且-**kwargs我们还需要通过self我们借用的借来供自己使用。 同样,您可能还记得,当我们刚开始谈论对象时,我提到过functools.total_ordering,现在希望它更有意义。作为练习,您可以自己实现它-一个类装饰器,将eq一个推断器和一个比较运算符外推到一个总顺序中,所有其他比较运算符都从该派生而来。
The Meta-Birds and Meta-Bees
希望到现在为止,我已经说服了您,将类像对象一样对待是有其优点的-但是,等待会变得更好。我们讨论了如何创建对象,以及最终如何object完成繁琐的工作。但是如何创建类?谁在拉弦? 让我们从头开始:您可能不知道,但是实际上您一直在创建类。就是这样:
>>> class A: ... pass # Class created!
诚然,这看起来并不完全类似于赋值,但是def语句也没有,它仍然创建了一个函数。虽然对象可以具有带有任何签名的构造函数,但根据其__init__s和__new__s的定义,类具有固定的格式(同样,类似于函数):名称,超类列表以及属性和方法的字典。这里是:
__init__s
__new__s
>>> A.__name__ 'A' >>> A.__bases__ (object,) >>> A.__dict__ {}
还有一个更有趣的例子:
>>> class B(A): ... x = 1 ... def f(self): ... return 2 >>> B.__name__ 'B' >>> B.__bases__ (A,) >>> B.__dict__ {'x': 1, 'f': <function B.f at 0x...>}
坦白地说,它只不过是语法糖:名称是从class;之后的标识符推断出来的 来自括号之间的标识符的超类;执行后,属性字典只是类主体的局部作用域。不相信我吗 你自己看:
>>> class A: ... for i in range(3): ... print('Hello, world!') Hello, world! Hello, world! Hello, world! >>> A.i 2
与C ++和Java不同,类不是针对编译器的一堆指令,它是一个活的有机体,它的主体像其他任何代码一样执行,即使它意味着打印内容。顺便说一句,在循环结束之后,它会i保留其最后一次迭代的值在范围内,因此2Python将其添加为类属性。 但是实际分配给谁呢?好吧,类似于object对象创建中的最终词,type类也是如此。是的,这与我们用来确定对象类型的功能相同:
>>> type(1) <class 'int'>
但是,当您使用三个参数调用它时,它实际上是类工厂的两倍。只需传入一个名称,一个超类元组和一个属性字典,它就会为您带来一堂课:
>>> A = type('A', (), {}) >>> A <class 'A'> >>> B = type('B', (A,), {'x': 1, 'f': lambda self: 2}) >>> B <class 'B'> >>> b = B() >>> b.x 1 >>> b.f() 2 >>> isinstance(b, A) True
令人兴奋,不是吗? 超越性 type实际上不仅仅是一个产生类的函数;这是类的类,也称为元类。这就是为什么您得到:
>>> class A: ... pass >>> A.__class__ <type 'type'> >>> type(A) <type 'type'>
与类如何定义其实例行为类似—元类也定义了其类行为。这都是非常元的,但是我们将逐步进行:当您要自定义对象的初始化时,只需执行以下操作:
>>> class A: ... def __init__(self, name, bases, attrs): ... print('Hello, world!') >>> a = A() Hello, world!
同样,当我们要自定义类的初始化时,__init__可以向其元类添加一个方法。确实所有类都继承自type,这是我们不能更改的,也确实是所有对象都继承自object,即使在Python 3中,您也不必显式编写它。因此,要定义元类,我们只需要将自己注入到type家庭中,就像我们之前将自身注入到object家庭中的方式一样:
__init__
>>> class M(type): ... def __init__(cls): ... print('Hello, world!')
为了与我们希望由除之外的元类创建的类进行通信type,我们将metaclass关键字添加到其声明中:
>>> class A(metaclass=M): ... pass Hello, world!
在解释器中按回车键的那一刻,将创建一个类对象-收集其名称和超类,执行其主体,并将所有内容传递给其元类。其中,对我们来说,是M。关于约定的一句话:如果在常规方法中,我们self用来表示实例(并cls在类方法中表示类),那么我们用于cls在元类中表示类(并mcs表示元类,等效于元类方法) )。 这种关系也适用于其他行为。假设我们不喜欢这种表示形式:
>>> class A: ... pass >>> A <class 'A'>
我们可以在类的元类中重写它:
>>> class M(type): ... def __repr__(cls): ... return f'{cls.__name__}!' >>> class A(metaclass=M): ... pass >>> A A!
或者,我们可以覆盖其他一些行为。怎么样…
>>> class M(type): ... def __getitem__(cls): ... return 1 >>> class A(metaclass=M): ... pass >>> A['x'] 1
这很怪异-但是如果您曾经使用过类型注释,则可能会看到诸如List[int];的语法。现在您知道它是如何完成的! 就是这样:我不知道为什么人们将元类视为一个超高级的话题。就像对象是由定义其行为的类塑造而成的一样,类(即对象本身)也是由元类塑造而成的。这只是一个已经熟悉的演变中的一个步骤:另一个类,定义了另一个行为-只有它继承type并具有一些固定的签名和一些语法糖;纳达玛斯。 关于元类的真相 尽管很酷,但是元类不是很实用。我的意思是,它们对于Python的实现绝对是必不可少的,但是您可以使用元类进行的任何其他操作-通常都可以不使用它。让我们来看一些用例,自己看看。 修改 一些元类会修改其类,例如,用断言其类型的类型化属性系统地替换其属性。这使您拥有一些不错的语法,如下所示:
>>> class A(metaclass=TypeSafe): ... x : int = 1 ... y : int = 2
这称为类注释,它与函数注释非常相似:
>>> A.__annotations__ {'x': <class 'int'>, 'y': <class 'int'>} >>> A.__dict__ {'x': 1, 'y': 2}
因此,元类将能够遍历它们,并用类型化的属性替换它们,如下所示:
class TypeSafe(type): def __init__(cls, name, bases, attrs): for key, type in cls.__annotations__.items(): default = getattr(cls, key, None) setattr(cls, key, TypedProperty(key, type, default)) class TypedProperty: def __init__(self, key, type, default): self.key = key self.type = type self.default = default def __get__(self, instance, cls): if instance is None: return self # If the value is not defined, fall back to the default if self.key not in instance.__dict__: instance.__dict__[self.key] = self.default return instance.__dict__[self.key] def __set__(self, instance, value): if not isinstance(value, self.type): raise AttributeError('{self.key} must be ' '{self.type.__name__}') instance.__dict__[self.key] = value # If the value is deleted, reset it to the default def __delete__(self, instance): instance.__dict__[self.key] = self.default class A(metaclass=TypeSafe): ...
如您所见,大多数代码实际上是TypedProperty;; 元类所做的全部工作就是在创建时修改类,并且坦率地说,使用类装饰器可以更轻松地完成此操作:
def type_safe(cls): for key, type in cls.__annotations__.items(): default = getattr(cls, key, None) setattr(cls, key, TypedProperty(key, type, default)) return cls class TypedProperty: ... # Same as before @type_safe class A: ...
注册 元类的另一个用例是注册-例如,在Django中,您定义了继承自Model的类,这些类的元类收集它们并在数据库中为每个类创建一个表。我们可以用装饰器来做……
>>> models = [] >>> def model(cls): ... models.append(cls) ... return cls
但这并不理想,因为它不支持继承:
>>> @model ... class A: ... pass >>> class B(A): ... pass >>> models [<class 'A'>]
您也希望B成为模型,但是由于装饰器只能在其下面的任何地方工作,因此不会在子类上重新调用它。另一方面,元类确实会传播:一旦元类创建了一个类,它的所有子类也都是它的“实例”,因此每个类都会被调用:
B
>>> models = [] >>> class ModelMetaclass(type): ... def __init__(cls, name, bases, attrs): ... models.append(cls) >>> class Model(metaclass=Model): ... pass >>> class A(Model): ... pass >>> class B(A): ... pass >>> models [<class 'Model'>, <class 'A'>, <class 'B'>]
这也混入了Model我们不希望看到的,但很容易将其过滤掉。但是,还有一个更好的解决方案:类可以定义特殊__init_subclass__方法,只要将其子类化即可调用该方法:
__init_subclass__
>>> models = [] >>> class Model: ... def __init_subclass__(subclass): ... models.append(subclass) >>> class A(Model): ... pass >>> class B(A): ... pass >>> models [<class 'A'>, <class 'B'>]
太完美了;在修改和注册之间,在类级别上发生的事情很少–因此,正如我所说的,元类不是很实用。 班级行为 “可是等等!” 您可能会惊叹道:“元类不是定制类行为的唯一方法吗?如果我们想支持一些有趣的语法怎么办?” 在那种情况下,您确实是对的-例如,如果没有使用custom编写元类,就无法覆盖类的表示repr。但是在实践中,List[int]我已经提到了Python生态系统中唯一常见的有趣类语法,甚至可以使用特殊class_getitem方法在类级别上完成:
>>> class A: ... def __class_getitem__(cls, key): ... return 1 >>> A['x'] 1
我们真正想要使用元类的唯一时间是,当我们做的事情非常非常粗略时—在这些情况下,我们可能最终会使用其独特的prepare方法。在执行类主体之前,会调用此有趣的机制,并返回一个字典(或其子类),该字典将用作主体的局部作用域-因此您可以执行类似的操作collections.OrderedDict以将其替换为a,以保留属性定义顺序:
>>> class Ordered: ... def __prepare__(mcs, name, bases): ... return collections.OrderedDict() ... def __new__(mcs, name, bases, attrs): ... cls = super().__new__(mcs, name, bases, attrs) ... cls._order = list(attrs) ... return cls >>> class A(metaclass=Ordered): ... x = 1 ... y = 2 >>> A._order ['x', 'y']
__new__顺便说一句,我之所以使用该特殊词典,是因为它走得太远了。创建并dict建立该类后,它会转换为标准词典(好叫mappingproxy),并失去所有特殊功能。 过去这很有用,但是自Python 3.6起,默认情况下对字典进行排序-因此,再次不需要元类。但是,我可以想到一些有趣的用例: 枚举水果 当我用C编程时,我曾经这样定义枚举:
__new__
enum Fruit{ APPLE, ORANGE, BANANA, }
并且APPLE会被自动分配0、1ORANGE和BANANA2。在Python中,我必须这样做:
class Fruit: APPLE = 0 ORANGE = 1 BANANA = 2
我实际上认为最好是明确的(即使在C语言中也是如此)。但是,用C语言比用Python语言更容易做的想法使我发疯,所以我着手寻求使这种语法起作用的方法: 水果类:
class Fruit: APPLE ORANGE BANANA
乍看起来,它看起来很荒谬,但实际上,它是绝对有效的Python代码;崩溃的原因与其说语法,不如说是因为这些愚蠢的语句包含一个唯一名称的表达式,该表达式得到求值并立即被丢弃—所有从未分配的引用名称,以及执行主体时,Python试图解决它们,我们得到了一个NameError。 但是,如果我们使用元类传递更宽容的字典怎么办?说,一个不抱怨缺少键的人,而是简单地为其分配计数器的下一个值吗?
import itertools class EnumDict(dict): def __init__(self): self.counter = itertools.count() def __getitem__(self, key): if key not in self: self[key] = next(self.counter) return super().__getitem__(key) class EnumMetaclass(type): def __prepare__(name, bases): return EnumDict() class Enum(metaclass=EnumMetaclass): pass
现在,我们要做的就是继承Enum,和等等:
>>> class Fruit(Enum): ... APPLE ... ORANGE ... BANANA >>> Fruit.APPLE 0 >>> Fruit.ORANGE 1 >>> Fruit.BANANA 2
魔法!当然,我们没有有定义Enum; metaclass=EnumMetaclass在Fruit就足够了,但使用继承看起来更优雅,感觉比较熟悉,所以它不会寝食不安的人。 大脑超负荷 另一个很酷的用例是在Python中支持重载-让用户使用相同的名称但不同的签名定义多个函数,然后调用正确的函数。由于Python没有类型,因此让我们从函数的Arity开始-即它有多少个参数。这是最终结果:
Enum;
metaclass=EnumMetaclass在Fruit
>>> class A(Overloaded): ... def f(self, x): ... print(1) ... def f(self, x, y): ... print(2) >>> a = A() >>> a.f(None) 1 >>> a.f(None, None) 2
为此,我们将用一个保留了每个键的列表的字典替换该类的字典,并在其中添加重定义符号-这样我们就可以将所有具有相同名称的方法分组,然后将它们包装在一个调度员基于其友好性:
class MultiDict(dict): def __getitem__(self, key): return super().__getitem__(key)[-1] def __setitem__(self, key, value): if key not in self: super().__setitem__(key, []) super().__getitem__(key).append(value)
为了使它起作用,我们必须提供支持__getitem__;如果主体的代码分配了一些变量并引用了该变量,例如x = 1then y = x + 1,则我们无法将其解析为列表,因此我们将返回分配给它的最后一个值,就像任何代码所期望的那样。覆盖了__setitem__和之后__getitem__,dict无论何时我们要实际获取或设置项目都必须调用,所以请原谅supers。然后:
__getitem__
x = 1then y = x + 1
__setitem__
class OverloadedMetaclass(type): def __prepare__(mcs, name, bases): return MultiDict() def __new__(mcs, name, bases, attrs): real_attrs = {} for key, values in attrs.items(): if callable(values[0]): real_attrs[key] = Overload(values) else: real_attrs[key] = values[-1] return super().__new__(mcs, name, bases, real_attrs) class Overload: def __init__(self, fs): self.fs = {} for f in fs: arity = f.__code__.co_argcount self.fs[arity] = f def __call__(self, *args, **kwargs): arity = len(args) + len(kwargs) f = self.fs[arity] return f(*args, **kwargs) class Overloaded(metaclass=OverloadedMetaclass): pass
这样做是MultiDict在创建类之前,将常规字典替换为,然后遍历其所有属性,将所有可调用项分组为,Overload并将其余的解析为分配给它们的最后一个值。Overload依次保持所有功能的一致性(由代码对象的友好提供co_argcount),并在每次调用该功能时将其委托给为此数量的参数注册的任何函数。 如果您喜欢冒险,甚至可以支持基于参数类型的重载,可以使用批注指定这些重载;我将把它留在这里:
MultiDict
>>> class A(Overloaded): ... def f(self, x: int): ... return x + 1 ... def f(self, x: str): ... print('Hello, world!') >>> a = A() >>> a.f(1) 2 >>> a.f('Hello, world!') Hello, world!
参数化类别 最后一个元类的技巧:它的一些特殊的方法,即__prepare__,__new__与__init__接受可变参数关键字参数,**kwargs。该字典由您在类的声明中放入的任何关键字填充,但metaclass其自身除外:
__prepare__
metaclass
>>> class M(type): ... def __init__(cls, name, bases, attrs, **kwargs): ... print(kwargs) >>> class A(metaclass=M, x=1, y=2): ... pass {'x': 1, 'y': 2}
这使您可以使用关键字定义类,从而使您可以对它们的定义进行参数化,并且可以方便地用于面向方面的编程,但是当我们到达那里时,我们将对其进行讨论。顺便说一句:__init_subclass__在子类定义中还接受其他关键字:
>>> class A: ... def __init_subclass__(cls, **kwargs): ... print(kwargs) >>> class B(A, x=1, y=2) ... pass {'x': 1, 'y': 2}
嗯是的。Python是巫术。 结论 在本文中,我们朝着启蒙迈出了又一步—这次,将类作为对象进行讨论,并了解如何在元类中定义其行为,就像在类中定义对象行为一样(差不多)。然后,我们发现实际上并没有什么用,因为有解决方案可以解决您可能需要的任何需要元类的问题。当然,如果您要制作魂器。下次,我们将讨论所有与对象无关的所有其他与主题相关的主题,然后继续讨论更大的事情:模块和包!
原文链接:https://blog.csdn.net/qq_44176343/article/details/106004990