面向对象编程¶
AI Summary powered by ChatGPT
Python中最丰富且可玩性最高的编程范式是面向对象编程(OOP)。在OOP中,可以控制Python对象的行为,包括迭代、打印、上下文管理等操作。语法重载使得在不同库中可以体验到各种特殊的语法行为。例如,datetime库提供日期输出格式;pathlib库支持路径操作;NumPy重载操作符实现广播机制;pandas和plotnine库实现花式索引和图像组合;Flask简洁美观地配置web应用;PyTorch通过torch.no_grad控制局部梯度计算。另外,文章还介绍了类的基础知识、方法和魔法方法的实现。通过__call__方法,类可实现可调用对象。元类可定制类的行为,例如要求方法名用小写字母。在Python中,类是元类的对象,可以通过自定义元类实现高级特性。不要过度关注复杂语法,学用相结合,使用频繁的才是真会的。
虽然函数式编程也很好玩,但是我认为Python最丰富、可玩性最高的还是OOP(面向对象编程,Object Oriented Programming)。
在这里你几乎可以控制Python对象的一切行为:
for
语句如何迭代对象print(object)
会输出什么东西with
语句如何创建对象,语句结束之后执行什么操作()
,[]
,.
,+
,-
,*=
,/=
等操作符的行为
诸如此类。你甚至可以用元类(metaclass)来控制定义类的过程,比如要求所有的方法命名都必须用小写字母。
也正是因为Python在语法重载上的高度自由,我们可以在各类库中见识到千奇百怪的语法行为:
datetime
datetime是Python的标准库之一,提供处理时间和日期的接口。我们可以在f-string中使用特殊的format输出日期。
>>> from datetime import datetime
>>> t = datetime.now()
>>> print(t)
2024-03-07 21:42:39.762597
>>> print(f"{t:%X}")
21:42:39
>>> print(f"{t:%X %x}")
21:42:39 03/07/24
pathlib
pathlib是Python的标准库之一,提供了方便快捷的路径操作接口。例如我们可以用除法/
来组合路径:
>>> from pathlib import Path
>>> p = Path('/etc')
>>> q = p / 'init.d' / 'reboot'
>>> q
PosixPath('/etc/init.d/reboot')
NumPy
NumPy中比较著名的就是广播机制了,能做到这一点是因为NumPy重载了numpy.ndarray
的各种运算符。例如乘法的广播:
import numpy as np
arr = np.array([1,2,3])
print(arr*3) # print [3 6 9]
pandas
pandas更是不必多说,DataFrame
实现的各种花式索引、赋值、广播操作都让人印象深刻。例如:
df.loc[df.age > 30]
Flask
Flask是一个很好用的web框架,它的Python包实现了一套非常简洁美观的语法(尤其是Flask使用装饰器来配置路由、错误回调等)。
我们用几行代码就可以创建一个Flask应用:
from flask import Flask
app = Flask(__name__)
@app.route("/") # 访问 / 路径时返回的内容
def hello_world():
return "<p>Hello, World!</p>"
@app.errorhandler(404) # 找不到页面时返回的内容
def page_not_found(error):
return render_template('page_not_found.html'), 404
plotnine
如果你用过R语言的ggpolt2绘画包,一定对它的语法印象深刻,你可以使用加法来组合图像:
library(ggplot2)
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point()
Python中也有一个类似的包,语法非常简洁优雅:
from plotnine import ggplot, geom_point, aes, stat_smooth, facet_wrap
from plotnine.data import mtcars
(ggplot(mtcars, aes("wt", "mpg", color="factor(gear)"))
+ geom_point()
+ stat_smooth(method="lm")
+ facet_wrap("~gear"))
PyTorch
最后再举一个PyTorch的例子。在模型推理的过程中,我们一般不需要计算梯度。那么就可以使用torch.no_grad
这样的上下文管理器来控制局部不计算梯度:
import torch
x = torch.tensor([1.], requires_grad=True)
with torch.no_grad():
y = x * 2
类的基础知识¶
之前的两篇文章内置关键字和内置类已经零零碎碎地回答了下面的问题:
- 什么是类?
- 类就是某些对象的抽象,几乎python中一切的东西都是类(或者由类生成的对象或者叫实例 i.e. instance)
- 为什么使用类?
- 因为类是一类对象的抽象,可以很好的提供这些对象的统一接口便于维护,极大提高了代码的复用率。同时类之间的继承关系也可以很方便地简化代码。
- 类的关键在于抽象和复用。
- 如何自定义一个类?
- 使用
class
关键字
- 使用
- 如何访问类的属性、方法?
- 使用
.
运算符
- 使用
- 从类到对象的过程?
- 例如
a = A()
- 首先会运行
A.__new__(cls)
方法来创建一个实例。 - 然后会运行
A.__init__(self)
方法来初始化实例。 - 于是乎一个实例就被创造好了。
- 例如
- 如何继承一个类?
- 这么写:
class MyClass(FatherClass)
- 这么写:
这篇文章我们来补充一些OOP的知识。
属性(attribute)¶
属性就是类命名空间中的变量
类属性和实例属性¶
我们定义这样一个类来展示类中几种不同的属性:
>>> class Example:
... class_attribute = '类属性'
... __private = '私有 类属性'
... _private = '惯用私有 类属性'
... def __init__(self):
... self.instance_attribute_in_method = '实例属性'
... @property
... def property_attribute(self):
... return 'property装饰的类属性'
>>> dir(Example)
[ '_Example__private', '__class__', '__delattr__',
'__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__',
'__getstate__', '__gt__', '__hash__',
'__init__', '__init_subclass__', '__le__',
'__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__',
'_private', 'class_attribute', 'property_attribute']
>>> e = Example()
>>> e.__dir__()
[ '_Example__private', '__class__', '__delattr__',
'__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__',
'__gt__', '__hash__', '__init__', '__init_subclass__',
'__le__', '__lt__', '__module__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__',
'_private', 'class_attribute',
'instance_attribute_in_method', 'property_attribute']
>>> e.__dict__
{'instance_attribute_in_method': '实例属性'}
e.__dir__()
可以查到所有的类属性以及实例属性: _Example__private
:(私有变量会被重命名)_private
class_attribute
class_attribute_in_method
property_attribute
但是e.__dict__
只能查到通过self.
赋值的变量(也就是实例属性)。
我们可以尝试调用这些变量:
类属性是共享的
请注意,类属性是这个类的所有实例共享的。每个实例都可以访问、修改这个属性。请不要把本该属于实例的属性放到类属性中。
>>> e.class_attribute
'类属性'
>>> try:
... print(e.__private)
... except AttributeError as error:
... print(error)
'Example' object has no attribute '__private'
>>> e.instance_attribute_in_method
'实例属性'
>>> e.property_attribute
'property装饰的类属性'
>>> e._Example__private
'私有 类属性'
>>> e._private
'惯用私有 类属性'
此外,如果同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例:
>>> class Warehouse:
... purpose = 'storage'
... region = 'west'
...
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east
模式匹配¶
__match_args__
,定义了match
语句中对象的行为,规定了使用哪些变量来进行匹配:
class A:
__match_args__ = ('a','b')
def __init__(self, x, y, z):
self.a = x
self.b = y
self.c = z
obj = A(2,2,3)
match obj:
case A(2,2):
print(2,2) # this case matched
case A(1,1):
print(1,1)
限定属性¶
最后,类定义中还可以规定一个特殊的属性:__slots__
,使用它可以声明所有的属性。从而显著节省空间、提高属性的查找速度。
用法就是:
class A:
__slots__ = ['data']
pass
data
这个属性了,不可以在class
的定义中使用其他属性,实例化后也不可以通过赋值的方式增加其他属性。 方法(method)¶
方法就是命名空间中的函数
如果你直接在class
的定义内写一个普通的函数:
>>> class Example:
... def f(s):
... print(s)
Example.f
来调用。 >>> Example.f('asd')
asd
实例化之后它的行为就会变化,他会默认把自身作为第一个参数传入:
把实例自身作为参数传入类方法
Python中约定俗称的习惯是,如果你需要使用实例自身作为第一个参数传入,那么这个形参应该定义为self
(前面的例子说明了,定义成其他的也不是不行,这是个软约束)。
另外,在其他语言中也广泛存在这样的行为,例如JavaScript的this
、Java中的this
、C++中也有this
指针。这些概念和Python的self
非常类似。
>>> e = Example()
>>> e.f()
<__main__.Example object at 0x102c437d0>
>>> e.f('asd')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Example.f() takes 1 positional argument but 2 were given
类方法装饰器¶
我们在函数式编程已经介绍了装饰器。在类的方法定义中,我们也常用装饰器来实现一些功能。
根据我们之前的介绍,所谓装饰器就是一个返回值是函数的函数。Python内置函数中可以用作装饰器的有三个:
staticmethod
,classmethod
和property
。也有很多其他的装饰器被python安置在了各个标准库中,例如functools.cache
等。
@staticmethod
装饰的静态方法可以解决我们上面提到的函数行为被改变的问题,静态方法不会把self
作为第一个参数传入。
@classmethod
装饰的类方法可以在不实例化的使用。
@property
装饰的方法会成为一个属性。这个属性会有getter
, setter
, deleter
三个方法,分别使用@property
, @x.setter
, @x.deleter
装饰即可。使用这个装饰器可以更加精细地控制属性的行为,例如我希望person.age
这个属性永远是正整数,就可以写一个方法来实现。
例如:
class C:
def __init__(self):
self._x = None
@property
def x(self):
"""I'm the 'x' property."""
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
再如abc
包中的@abstractmethod
,是用于声明抽象方法的装饰器。一般用于抽象基类中为实现的方法。
更多的内容我们在介绍标准库的时候再来看。
多态(polymorphism)¶
多态是一个计算机术语,也就是同一个接口在不同的类上表现出的行为不同。例如:
>>> class Person:
... def __init__(self, name):
... self.name = name
... def hello(self):
... print(f"I am {self.name}")
...
>>> class Doctor(Person):
... def hello(self):
... print(f"I am Dr.{self.name}")
...
>>> alice = Person('Alice')
>>> alice.hello()
I am Alice
>>> galler = Doctor('Galler')
>>> galler.hello()
I am Dr.Galler
魔法方法(magic method)¶
类的方法中最好玩的一部分就是魔法方法了。诚如其名,魔法一般,无所不能。
这些方法是Python设计的用于定义类的一些重要行为(例如重载运算符)的方法,命名前后都有双下划线。例如:
__init__
__new__
__call__
需要注意,很多时候我们并不是完全重写这些方法,只是想要在默认的行为中加上一点内容。所以我们首先需要学会如何复现默认的行为(实际上这在基础语法的文中已经展示过了)。
例如我们想要在__hash__
被调用的时候输出一句话,然后采用默认的行为:
class A:
def __hash__(self):
print('__hash__() is called')
return super().__hash__(self) # 父类的hash函数
这里面super()
返回A
的父类。所以super().__hash__(self)
实际上就是调用A
的父类的__hash__
方法,在这个例子里就是object.__hash__(self)
(python类定义时默认的父类是object)。
学会了这一手,就可以来看Python中各种神奇的魔法方法。
类的构造方法¶
也就是__new__
、__init__
和__del__
这三个方法。分别定义了如何生成类的实例、如何初始化实例和实例被销毁(Python有自动的内存回收机制)时候的行为。
-
__new__
的目的主要是允许不可变类型的子类 (例如 int, str 或 tuple) 定制实例创建过程:>>> class inch(float): ... "Convert from inch to meter" ... def __new__(cls, arg=0.0): ... return float.__new__(cls, arg*0.0254) ... >>> print(inch(12)) 0.3048
-
__init__
几乎是每个类都会重载的重要方法,它控制着类的实例化行为。一个基类如果有__init__
方法,则其所派生的类如果也有__init__
方法,就必须显式地调用它以确保实例基类部分的正确初始化;例如:super().__init__([args...])
。 -
__del__
控制类销毁的行为。如果一个基类具有__del__
方法,则其所派生的类如果也有__del__
方法,就必须显式地调用它以确保实例基类部分的正确清除。
下面给出一个文件处理的例子:
from os.path import join
class FileObject:
'''文件对象的装饰类,用来保证文件被删除时能够正确关闭。'''
def __init__(self, filepath='~', filename='sample.txt'):
# 使用读写模式打开filepath中的filename文件
self.file = open(join(filepath, filename), 'r+')
def __del__(self):
self.file.close()
del self.file
何谓对象的销毁?
根据Python的垃圾回收机制(garbage collector)当对象的引用计数为0的时候对象就会被销毁。所以del x
并不直接调用 x.__del__()
,前者会将 x 的引用计数减一,而后者仅会在 x 的引用计数变为零时被调用。
要查看对象的引用计数可以使用gc.get_count()
函数。
由于调用__del__
方法时周边状况已不确定,在其执行期间发生的异常将被忽略,改为打印一个警告到sys.stderr
。
类的表示方法¶
__repr__
, __str__
, __format__
, __hash__
, __bool__
和__bytes__
依次控制了repr(object)
, str(object)
, hash(object)
, format(object)
, bool(object)
和bytes(object)
的行为。
例如:
>>> class Vector:
... def __init__(self, *data):
... self.data = data
... def __repr__(self):
... return 'vec(%s)' % (','.join(map(str,self.data)))
>>> v = Vector(1,2,3)
>>> print(v)
vec(1,2,3)
__hash__
和__eq__
的关联
如果一个类没有定义 __eq__()
方法那么它也不应该定义 __hash__()
操作;
如果它定义了 __eq__()
但没有定义 __hash__()
,则其实例将不可被用作可哈希多项集的条目。
如果一个类定义了可变对象并实现了 __eq__()
方法,则它不应该实现 __hash__()
,因为 hashable 多项集的实现要求键的哈希值是不可变的(如果对象的哈希值发生改变,它将位于错误的哈希桶中)。
比较运算符¶
运算符号与方法名称的对应关系如下:
x<y
调用x.__lt__(y)
x<=y
调用x.__le__(y)
x==y
调用x.__eq__(y)
x!=y
调用x.__ne__(y)
x>y
调用x.__gt__(y)
x>=y
调用x.__ge__(y)
这些运算符一般需要返回True
or False
。
当然,也可以返回任意值。比如我就要定义a < 2
是a的二进制值向左移2(这个操作的一般写法是a << 2
),也不是不可以。
二元运算符¶
主要包括(+, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |
)。
对应英文简写(add, sub, mul, matmul, truediv, floordiv, mod, divmod, pow, lshift, rshift, and, xor, or
)。
例如a + b
会(默认)调用运算符a.__add__(b)
,如果a
没有实现这个方法并且a和b不是同一个类,那么就会调用b的反射(reverse)的运算符b.__radd__(a)
,如果这个方法也没实现就会抛出错误。
此外还有一些加强(implemented)运算符(+=, -=, *=, @=, /=, //=, %=, **=, <<=, >>=, &=, ^=, |=
)。
例如a += b
等价于a = a.__iadd__(b)
,如果没实现__iadd__
就会调用a.__add__(b)
。
一元运算符¶
运算符号与方法名称的对应关系如下:
-a
调用a.__neg__()
+a
调用a.__pos__()
abs(a)
调用a.__abs__()
~a
调用a.__invert__()
数值转换¶
float(a)
调用a.__float__()
complex(a)
调用a.__complex__()
int(a)
调用a.__int__()
round(a)
调用a.__round__()
math.trunc(a)
调用a.__trunc__()
math.floor(a)
调用a.__floor__()
math.ceil(a)
调用a.__ceil__()
operator.index(a)
调用a.__index__()
实例属性访问控制¶
__getattribute__
和__getattr__
, __setattr__
, __delattr__
, __dir__
这几个方法可以控制实例属性被访问时的行为。
其中__getattribute__
和__getattr__
这两个方法的取名非常迷惑,它们的触发条件有所不同:
- 只要我们尝试访问一个类实例的属性,就会触发
__getattribute__
方法。- 特别注意,我们在写类定义的时候
self.xxx
也是会触发__getattribute__
的,所以需要特别避免递归调用。
- 特别注意,我们在写类定义的时候
- 只有我们尝试一个不存在的类实例的属性,才会触发
__getattr__
方法。
例如我们想让这个类被访问到不存在属性的时候返回None
,就可以这么写:
class A:
def __init__(self, x):
self.x = x
def __getattribute__(self, name):
return super().__getattribute__(name)
def __getattr__(self, name):
return None
a = A(1)
print(a.x) # print 1
print(a.data) # print None
当我们给实例的属性赋值时会触发__setattr__
,例如a.data = 1
。
另外在定义类的时候, self.data=1
也是会触发这个方法的,因此需要格外注意避免循环调用。
当我们删除实例的属性时会触发__delattr__
,例如del a.data
。
__dir__
方法则控制了dir(object)
的行为,Python要求这个函数必须返回一个序列(例如列表)。
迭代器¶
一个典型的例子就是python内置的
range
类
我们之前提到过,定义了__next__
的是一个迭代器,定义了__iter__
的是一个可迭代对象。使用iter(iterable_object)
可以返回一个迭代器。
此外实现__reversed__
这个方法可以让对象支持reversed()
内置函数,按照规范这个方法应当返回一个逆序迭代器。
使用for
语句可以遍历一个可迭代对象(实际上会创建一个迭代器),每一次迭代实际上就是通过.__next__()
获取迭代器中的下一个值。
例如我们自己写一个整数迭代器:
class Range:
"""Range(n) is a iterator from 1 to n"""
def __init__(self, n) -> None:
self.max = n
self.now = 0
self.inc = 1 # 增量,顺序为1,逆序为-1
def __next__(self):
while self.now != self.max:
self.now += self.inc
return self.now
else:
raise StopIteration
def __iter__(self):
# 顺序迭代
return self
def __reversed__(self):
# 逆序迭代
self.inc = -1
self.max, self.now = self.now+1, self.max+1
return self
for i in Range(3):
# 输出 1 2 3
print(i)
for j in reversed(Range(3)):
# 输出 3 2 1
print(j)
描述器¶
实现了__get__
, __set__
和__delete__
三个方法中的任意一个的对象称为描述器。他的主要作用是充当另外一个拥有者类的一个可变属性。
例如:
import os
class DirectorySize:
"""描述器"""
def __get__(self, obj, objtype=None):
return len(os.listdir(obj.dirname))
class Directory:
size = DirectorySize() # Descriptor instance
def __init__(self, dirname):
self.dirname = dirname # Regular instance attribute
这个例子中,Directory.size
就是一个根据dirname
来动态变化的属性。
更多描述器的使用指南参见官网。
容器¶
一个典型的例子就是python内置的字典
__len__
定义了len(object)
的行为,通常我们会返回容器的大小。__length_hint__
,定义了一个长度的估计值,实现这个估计的方法可以提高性能。__getitem__
定义了[key]
这一取值操作符的行为。__setitem__
则定义了对象在等号左侧时object[key] =
的赋值行为。__delitem__
定义了del object[key]
这样删除内容的行为。__missing__
定义了找不到key
时候的行为。__iter__
和__reversed__
分别定义了顺序迭代和逆序迭代的行为。__contains__
定义了成员检测操作符in
的行为。
切片的行为
实际上切片的行为也是通过__getitem__
、__setitem__
和__delitem__
这三个方法定义的。
以下形式的调用
a[1:2] = b
a[slice(1, 2, None)] = b
所以,想要支持切片只要在这三个魔法方法里实现对slice
类参数的支持就行了。
特别的,...
在python中和Ellipsis
是完全相同的,要在切片中定义这个参数的行为就需要特别判断一下。
pandas.DataFrame的花式索引
如果你用过pandas那么一定会感叹于各种花式索引的便利。
你可以使用以下的索引方式:
df[key]
df.loc[key, ...]
df.iloc[key, ...]
传入的参数也是很多样的。
读者可以自行思考这些索引的实现方式。
上下文管理器¶
一个典型的例子是python的
open
函数
定义了__enter__(self)
和__exit__(self, exc_type, exc_value, traceback)
的类是一个上下文管理器,可以在with
语句中使用。
with ContextManager() as c:
# with 语句会首先调用__enter__,把返回值赋值给as后的变量
pass
# 语句结束后运行__exit__
# 如果没有异常,三个参数都是None
可调用对象¶
很多python的内置数据类型都实现了这个方法,例如
list
再如dict
,他们都可以当成一个函数来使用。
__call__
定义了对象被调用时的行为:object(args, ...)
。
例如:
class Hello:
def __init__(self, name) -> None:
self.name = name
def __call__(self):
print(f'Hello {self.name}.')
h = Hello('python')
h() # print Hello python.
另外值得一提的是,既然类可以实现__call__
从而变成callable
对象,那么它自然可以作为一个装饰器,这就是类装饰器。
例如:
class entryExit(object):
def __init__(self, f):
self.f = f
def __call__(self):
print("Entering", self.f.__name__)
self.f()
print("Exited", self.f.__name__)
@entryExit
def func():
print("inside func()")
func()
Entering func
inside func()
Exited func
具体的原理读者可以回想我们对@
语法糖的解释。
协程行为¶
这部分比较专业,异步编程常用于网络通信工程,我只在写爬虫的时候偶尔能用上。
实现了__await__
的对象是可等待(awatiable)对象。
使用
async def
定义的异步函数必须返回一个可等待对象。
__aiter__
和__anext__
定义了异步迭代器的行为。例如:
class Reader:
async def readline(self):
...
def __aiter__(self):
return self
async def __anext__(self):
val = await self.readline()
if val == b'':
raise StopAsyncIteration
return val
__aenter__
和__aexit__
定义了异步上下文管理器的行为。例如:
class AsyncContextManager:
async def __aenter__(self):
await log('entering context')
async def __aexit__(self, exc_type, exc, tb):
await log('exiting context')
泛型(generic type)¶
PEP 484 - Type Hints规范了Python的类型提示。
有种写法你肯的见过:l: list[int] = [1,2,3]
。
这行代码的type hint用到了list[int]
,那么自然有一种语法可以实现类的[]
操作符。
它的实现方式是__class_getitem__
方法,这个方法应当返回一个 GenericAlias
对象。
当在类上定义时,__class_getitem__
会自动成为类方法。 因此,当它被定义时没有必要使用 @classmethod
来装饰。
这个设计的目的就是允许标准库泛型类的运行时形参化以更方便地对这些类应用类型提示。
控制类的创建¶
我们说Python万物皆对象,实际上类是元类的对象。
默认情况下,类是使用元类type()
来构建的,这时候类的__class__
是type
。
例如:
class A:
pass
A = type("A", (), {})
当一个类定义(class
语块)被执行时,将发生以下步骤:
- 调用基类的
__mro_entries__
方法,解析 MRO 条目; - 确定适当的元类;
- 如果没有基类且没有显式指定元类,则使用
type()
; - 如果给出一个显式元类而且 不是
type()
的实例,则其会被直接用作元类; - 如果给出一个
type()
的实例作为显式元类,或是定义了基类,则使用最近派生的元类。
- 如果没有基类且没有显式指定元类,则使用
- 调用元类的
__prepare__
方法,准备类命名空间; - 执行类定义的主体(类似于
exec
类定义的所有代码); - 调用元类的
__new__
方法,创建类对象。
父类¶
当一个类继承另一个类时,会在这个父类上调用 __init_subclass__()
。 这样,就使得编写改变子类行为的类成为可能。
例如:
class Philosopher:
def __init_subclass__(cls, /, default_name, **kwargs):
super().__init_subclass__(**kwargs)
cls.default_name = default_name
class AustralianPhilosopher(Philosopher, default_name="Bruce"):
pass
元类¶
元类可以实现的自定义行为就更多了。
例如我们开头提到的,要求所有的方法命名都必须用小写字母:
class Meta(type):
def __new__(cls, name, bases, dict):
flag = all(name==name.lower() for name in dict)
assert flag, "CapitalName"
return type.__new__(cls, name, bases, dict)
def __init__(self, name, bases, dict):
return type.__init__(self, name, bases, dict)
def __call__(cls, *args, **kwds):
print("meta_called")
return type.__call__(cls, *args, **kwds)
class MyClass(metaclass=Meta):
def Apple(self):
pass
Traceback (most recent call last):
File "/Users/yang/Desktop/example.py", line 12, in <module>
class MyClass(metaclass=Meta):
File "/Users/yang/Desktop/example.py", line 3, in __new__
assert all(name==name.lower() for name in dict), "CapitalName"
AssertionError: CapitalName
把方法改成小写就可以正常定义了:
class MyClass(metaclass=Meta):
def apple(self):
pass
def __call__(cls):
print("subclass_called")
pass
obj = MyClass()
obj()
# print meta_called then print subclass_called
指的注意的是,元类的__call__
是在MyClass
的对象被调用时才运行(如果子类也有__call__
那么会先调用元类,在调用子类)。
这个时候MyClass.__class__
是<class '__main__.Meta'>
,不再是常见的<class 'type'>
。
这也佐证了我们对类的叙述:类是元类的对象。
写在最后¶
Python语法的介绍到此为止了。后面会介绍各种库(标准库和第三方库)的使用,以及一些具体的实战案例。
我在本系列教程的第一篇文章说:Python是少儿编程的摇篮,我上二年级的小侄子都可以学会。
好吧,我承认Python的语法如此庞杂,小侄子估计不太能搞定,大概只能入个门。
奉劝各位读者莫要过分纠结这些fancy的语法,记住:你不会的语法全都对你没用,如果你哪天需要用了,自然就学会了。
编程领域不适用"书到用时方恨少",学得越多忘得越快,天天用的才是真的学到手的。
此致。
Created: 2023-01-13 15:21:08