熟悉了传统的 C++/Java 类定义的风格,来感受一下 Python 是如何定义类的。本篇是阅读 《The Quick Python Book》第二版关于类定义的笔记,由原书内容进一步引申,不过是依照本人的思考顺序来组织的。在理解 Python 类定义的同时头脑中应该闪现出 JavaScript/Java 如何定义类的情景。
最简单的类定义
class MyClass:
pass
由于 class MyClass
后面要有个冒号,而冒号后总得有点东西才能表示该类定义结束了,于是放个 pass
当占位符。Python 也像 Java 一样,有一个根类,叫做 object,例如上面的定义
1 2 3 4 5 |
>>> MyClass.__bases__ (<class 'object'>,) >>> import inspect >>> inspect.getmro(MyClass) (<class '__main__.MyClass'>, <class 'object'>) |
我们能看到它隐式的基类是 object
, 而不用显式的声明为 class MyClass(object)
。看到 __bases__
属性是一个 Tuple, 意识到 Python 是支持多重继承的。
实例的属性
Python 实例的属性不需要像 Java 那样放在类中方法外来定义,我们可以随时随地它实例新增属性,或在类定义外删除某个属性,俗话是 on the fly。这个特性有点像 JavaScript 的类。
用下面的代码来解释说明
1 2 3 4 5 6 7 8 9 10 |
class Circle: pass my_circle = Circle() #1 my_circle.radius = 5 #2 print(my_circle.radius) #3 my_circle.hello = lambda name: print(name) my_circle.hello('World') #4 输出 'World' del my_circle.radius #5 hello 也是个属性,所以也能 del my_circle.hello print(my_circle.radius) #6 |
我们首先创建一个最粗糙的类 Circle
- #1, 创建 Circle 实例的方式是类名后加上括号当方法用,没有 new 关键字,有点类似 Scala 的 case class 的实例创建。后面我们会知道 Circle() 会映射到对
__init__
方法的调用 - #2,新创建的 my_circle 没有任何自定的属性,想要加新属性直接用点号添加就行,该属性不存在就会创建
- #3, 输出新加的属性,输出 5
- 用 Lambda 随时增加一个方法也不是事,但是这个 Lambda 却不知如何访问当前实例 my_circle 中的成员了
- #4,
del
关键字还能删除实例的属性 - #5,属性已删除,因此会报出 AttributeError: 'Circle' object has no attribute 'radius' 的错误
看看下面的实例方法也可以操作实例的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Circle: def __init__(self): self.radius = 1 def foo(self): #实践中不建议下面的操作,实例属性应该在构建函数 __init__ 中声明 del self.radius self.color = 'red' my_circle = Circle() print(my_circle.radius) # 1 # print(my_circle.color) # AttributeError: 'Circle' object has no attribute 'color' my_circle.foo() # 调用该方法才创建 my_circle 的 color 属性 print(my_circle.color) # 'red' print(my_circle.radius) # AttributeError: 'Circle' object has no attribute 'radius' |
简单的来讲,Python 的实例属性就是绑定在 self
上的属性。
上方代码只是演示了 Python 提供了那些特性,实际编码中应该在 __init__
中引入属性,而不应该恶意使用 Python 的这一便利,上帝打开一扇窗不一定允许你翻窗进来。
关于实例方法
在其他面向对象语言中,一提到实例方法我们都会说,调用时会传递一个隐式参数表示调用者实例本身,一般用 this
表示,如 Java/C++/C# 等,JavaScript 就把 this
搞得更复杂无比。比如我们在 Java 中用反射来调用一个方法时 method.invoke(object, parms...)
不得不显式的传入当前实例。
而 Python 中的实例方法就不再对调用者参数遮遮掩掩,明确的声明为第一个参数,通常命名为 self
, 你想改成别的名称也无妨,比如 me
,当然最好不要给别人造成太大的冲击。实例方法的第一个参数写在那里,但调用的时候却不用显式传入,而是实参与方法的形参依序后推。
1 2 3 4 |
class MyClass: def foo(self, a, b, c): self.a = a print(b, c) |
调用实例方法
1 2 |
mc = MyClass() mc.foo(5, 6, 7) #5, 6, 7 分别对应到上面的 a, b, c |
定义 foo
方法式,把 self
放在第一个参数方便我们访问当前实例的成员。Python 的实例方法用了 self
之后在访问成员变量与局部变量不在模棱两可。例如在 Java 中
1 2 3 4 5 |
public void foo() { //String name = "World"; System.out.println(name); this.name = name; } |
方法中的 name
可能是在引用一个局部变量(如果 name 在方法内部声明),也可能是引用一个实例变量(方法内未声明),只有明确用 this.name
才是对实例变量 name
的引用。然而在 Python 中没有这种情况,使用实例变量必须是 self.name
, 不带 self
的话,直接 name
也一定是在使用局部变量。即使方法中要使用类变量也必须明确前缀:
1 2 3 4 5 6 |
class MyClass: name = 'Hola' def foo(self, a): print(self.name, MyClass.name, self.__class__.name) print(name) #1 |
以上 #1 处会得到错误:NameError: name 'name' is not defined
私有属性与方法
Python 没有像 private 那样的关键字来表明私有属性或方法,同样是用命名约定来说告诉编译器是否是私有的。Python 约定双下划线 __
开头,但不以 __
结尾命名的就是私有的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class MyClass: def __init__(self): self.__x = 12 # __x private def __bar(self): # __bar private pass def __baz__(self): # __baz__ public pass mc = MyClass() mc.__baz__() # ✔︎ print(mc.__x) # ✘ mc.__bar() # ✘ |
构造方法 __init__
Python 的构造方法可以当作是特殊名称的实例方法来看待,额外的两个特性:1)它在用类名当成方法名使用时被调用,2)隐式返回该类的一个实例,即 self
。它的第一个参数也是 self
, 其他参数顺推,我们认为一旦进入 __init__
方法后,self
便创建就绪。然后可以基于 self
初始化实例成员。除此之外构造方法没有什么特别的,和其他实例方法完全一样,支持默认参数,变参等,甚至 __init__
也能作为普通方法来调用。
1 2 3 4 5 6 7 8 |
class MyClass: def __init__(self, name): self.name = name print(name) mc = MyClass('Hola') #1 x = mc.__init__('X') #2 print(x) # 输出 None |
- #1, 类名当方法名来用 MyClass(..), 会调用相应的
__init__
方法,返回 MyClass 的实例 - #2, 把
__init__
当成普通方法来调用,所以它返回的是None
和普通实例方法一样,构造方法也不支持重载,后声明的同名方法会把前面的方法定义覆盖掉。但 Python 可以借助于方法的默认参数来达到与 Java 等其他语言方法重载相当的效果。
类变量
既然有实例变量,Python 也有类变量,类比于其他面向对象语言,类变量就是不依赖于实例而存在的变量,并且为所有实例共享。Python 在访问类变量也是既能通过类名,也能通过实例来引用,推荐用类来引用类变量,这一点 C# 比较好,语法上杜绝用实例来引用类变量。
什么是类变量,写在类当中但游离于方法之外的变量就是类变量。例如:
1 2 3 4 5 6 7 8 9 10 11 12 |
class MyClass: pi = 3.14159 def foo(self): print(self.pi, MyClass.pi, self.__class__.pi) #1 self.pi = 3.14 print(self.pi, MyClass.pi, self.__class__.pi) # print(pi) # NameError: name 'pi' is not defined mc = MyClass() mc.foo() |
以上代码输出
3.14159 3.14159 3.14159
3.14 3.14159 3.14159
这里演示了类变量可以通过实例或类来访问,上面 #1 表示的三种形式。不建议通过实例来访问类变量,因为通过实例不能明确是在访问实例变量还是类变量。由上可知 self.pi
优先访问实例变量 pi
, 找不到实例变量 pi
才试图访问类变量 pi
,再涉及到类的继承关系就更复杂些。因此最好是使用哪个类的类变量就明确的写出特定的类名,像这里的 MyClass.pi
。
对于不同引用类变量的方式,再来看几个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Parent: pi = 3.14159 def foo(self): return self.pi #下面的 c.foo() 调用返回的是 3.14 # 这里写成 self.__class__.pi 也是返回 3.14, 根据需求也许总是要 Parent.pi class Child(Parent): pi = 3.14 c = Child() print(c.pi) #3.14 print(c.foo()) #3.14 print(Parent.pi, Child.pi) # 3.14159 3.14 |
所以安全稳妥的方式还是 ClassName.variableName
。
另外,类变量也可以动态增加或删除
类方法和静态方法
什么?Python 的类方法与静态方法还不一回事,在 Java 里只要有 static
修饰的方法即是类方法也是静态方法。相比于实例方法,Python 的类方法的第一个参数表示当前类
类方法
如果以下方式来定义一个类方法 hello
1 2 3 |
class MyClass: def hello(cls, name): # 其实就是 def hello(self, name) print(cls, name) |
由于函数中的参数只是个名称,即使第一个参数名写成了 cls
, 它于 def hello(self, name)
定义是没有区别的,所以它实际上是一个实例方法。还必须加个装饰告诉它是一个类方法而非实例方法,cls
是当前类,而非当前实例。
1 2 3 4 5 6 7 |
class MyClass: @classmethod # 标识这是一个类方法,并且方法第一个参数为类本身,约定用 cls 表示 class def hello(cls, name): print(cls, name) MyClass.hello('hello') # 类方法的调用,MyClass 作为隐式参数对应于 cls |
静态方法
如果只简单的定义一个无参数的方法
1 2 3 4 |
class MyClass: def hello(): # 这里会提示错误:Method must have a first parameter, usually called 'self' print('hello') |
无参数的方法或都方法的第一个参数既不想是 cls
也不想是 self
, 那么就要用 @staticmethod 把它标识为一个静态方法
1 2 3 4 5 6 7 8 9 10 11 12 |
class MyClass: @staticmethod def hello(): print('hello') @staticmethod def greeting(name): print(name) MyClass.hello() MyClass.greeting('world') |
方法的调用方式
Python 定义方法时没有学其他面向对象语言那样把 this(self) 指向实际自身的参数隐去,而是声明时写在第一个位置上,但调用时可跳过。其实 Python 调用方法时也能显式的通过第一个参数传入 self 或 cls, 这时候调用的主体就是类名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class MyClass: def instance_method(self, name): print('foo', name) @classmethod def class_method(cls, name): print('bar', name) @staticmethod def static_method(name): print('baz', name) mc = MyClass() mc.instance_method('instance method 1') MyClass.instance_method(mc, 'instance method 2') mc.class_method('class method 1') MyClass.class_method('class method 2') mc.static_method('static method 1') MyClass.static_method('static method 2') |
上面例子列出了三种类型方法的不同调用方式。
Python 属性 @Property
最后提一下 Python 真正叫做属性的东西,@Property,执行以下的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Temperature: def __init__(self): self._temp_fahr = 0 @property def temp(self): print("@property") return (self._temp_fahr - 32) * 5 / 9 @temp.setter def temp(self, new_temp): print("@temp.setter") self._temp_fahr = new_temp * 9/5 + 32 @temp.getter def temp(self): print("@temp.getter") return (self._temp_fahr - 32) * 5 / 9 t = Temperature() print(t.temp) t.temp = 23 |
输出为
@temp.getter
-17.77777777777778
@temp.setter
t.temp 调用了 @temp.getter 对应的取值方法,t.temp = 23 调用了 @temp.setter 对应的设值方法。你会发现这个类有两个 def temp(self)
方法定义,只是有不同的装饰, @property 对应的方法在这里没起到作用,相当于是
1 2 3 |
@property def temp(self): pass |
其实 temp.getter
才显得多余,所以通常让 @Property 注解的方法承担 getter 的责任
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Temperature: def __init__(self): self._temp_fahr = 0 @property def temp(self): print("@property") return (self._temp_fahr - 32) * 5 / 9 @temp.setter def temp(self, new_temp): print("@temp.setter") self._temp_fahr = new_temp * 9/5 + 32 |
@temp.getter 一般没什么用处
本文链接 https://yanbin.blog/understand-python-class-definition/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
这个有意思。但是工程实践中用的却不多。工程实践中更关注list,set,tuple以及map,filter,lambda,comprehension,multithreading, multiprocessing,GIL,requests,stream等东西。如果一个项目过于庞大,一个好的做法是把module组织好,实现shared modules between projects,还有就是上面写的,使用面向对象的方式来组织业务逻辑。