Python类的奇怪案例


其他语言(例如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__,跳过任何私有或魔术方法以及不可调用的属性,然后将其余部分替换为自身的两倍版本。一些注意事项:

  1. 不幸的是,班级__dict__不支持作业。如果您尝试这样做,A.__dict__['x'] =1.那就行不通了。使用setattr代替。
  2. 有些人不喜欢这种__dict__表示法,因为它看起来有点底层,因此您可能会看到它们使用内置vars函数代替(对于类和对象)。vars(o)简单的回报o.__dict__,就像type(o)简单的回报o.__class__
  3. cls完成后别忘了从装饰器返回-否则它将返回one,它将绑定到A,并且'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的定义,类具有固定的格式(同样,类似于函数):名称,超类列表以及属性和方法的字典。这里是:

>>> 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家庭中的方式一样:

>>> 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成为模型,但是由于装饰器只能在其下面的任何地方工作,因此不会在子类上重新调用它。另一方面,元类确实会传播:一旦元类创建了一个类,它的所有子类也都是它的“实例”,因此每个类都会被调用:

>>> 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__方法,只要将其子类化即可调用该方法:

>>> 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编程时,我曾经这样定义枚举:

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开始-即它有多少个参数。这是最终结果:

>>> 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。然后:

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),并在每次调用该功能时将其委托给为此数量的参数注册的任何函数。 如果您喜欢冒险,甚至可以支持基于参数类型的重载,可以使用批注指定这些重载;我将把它留在这里:

>>> 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其自身除外:

>>> 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