- 发表在
Python 类
- 作者
- 名字
- Wisdom Keeper
类提供了把数据和功能绑定在一起的方法。创建新类时创建了新的对象类型,从而能够创建该类型的新实例。实例具有能维持自身状态的属性,还具有能修改自身状态的方法(由其所属的类来定义)。 和其他编程语言相比,Python 的类只使用了很少的新语法和语义。Python 的类有点类似于 C++ 和 Modula-3 中类的结合体,而且支持面向对象编程(OOP)的所有标准特性:类的继承机制(inheritance)支持多个基类(Java不支持多继承,但可以通过接口(interface)来实现类似的功能。即implements
多个接口)、派生(derived)的类能覆盖基类(override)的方法、类的方法能调用基类中的同名方法(使用super()
)。对象可包含任意数量和类型的数据。和模块一样,类也支持 Python 动态特性:在运行时创建,创建后还可以修改。
# python
class MyClass:
def __init__(self, name):
self.name = name
# 创建类的实例,在 java 里创建实例为 new MyClass("Alice")
obj = MyClass("Alice")
# 动态添加属性
obj.age = 30
print(f"Name: {obj.name}, Age: {obj.age}") # 输出: Name: Alice, Age: 30
# 动态添加方法
def greet(self):
print(f"Hello, my name is {self.name}")
# 将方法绑定到实例
import types
obj.greet = types.MethodType(greet, obj)
# 调用动态添加的方法
obj.greet() # 输出: Hello, my name is Alice
可跳过以下内容
Bonus: 与之对应 Java 是一种静态类型语言,类和对象的结构在编译时就已经确定,不能在运行时动态地创建或修改类的结构(如添加或删除方法和属性)。不过,Java 提供了一些机制来实现一定程度的动态行为:
- 反射(Reflection):Java 的反射机制允许在运行时检查类、接口、字段和方法,并对其进行操作。通过反射,可以在运行时动态地调用方法、访问和修改字段。
import java.lang.reflect.Field;
public class DynamicFieldExample {
/**
* 什么是 throws Exception?
* throw Exceptions到方法的调用者,告诉调用者这个方法可能会抛出某些异常
* 什么时候需要 throws Exception?
* 当方法内部可能会抛出异常,但是方法本身不处理异常时,且异常为checked exceptions
* 时必须需要在方法签名中声明 throws Exception
* 注意,这里的Exception是所有exceptions的父类,你可以用任何继承该类的异常替换
* runtime exceptions 即运行时发生的错误 继承 unchecked exceptions 不需要在
* 方法签名中声明 throws Exception
* 这是因为未检查异常通常表示编程错误,如逻辑错误或不正确的 API 使用,而不是可以合理预期和恢复的情况。
* 你开发的时候就要修复这些错误,而不是想着在运行时处理他们。
*/
public static void main(String[] args) throws Exception {
// 创建一个实例
MyClass obj = new MyClass();
// 通过对象实例(obj)获取类对象
Class<?> clazz = obj.getClass();
// 动态添加字段,Field 类表示类或接口的一个字段(成员变量)。通过 Field 对象,
// 可以获取字段的元数据(如字段名、类型等),并可以动态地访问和修改字段的值。
Field field = clazz.getDeclaredField("age");
field.setAccessible(true);
field.set(obj, 30);
// 访问动态添加的字段
int age = (int) field.get(obj);
System.out.println("Age: " + age);
// 输出: Age: 30,哈哈,有没有发现private字段也能访问修改,so cool!
// 其实嘛,访问修饰符主要作用还是让开发者知道这个字段该不该访问,该不该修改,从java的角度完全限制访问是不可能的
}
}
class MyClass {
private int age;
}
- 动态代理(Dynamic Proxy):Java 提供了动态代理机制,可以在运行时创建代理类,代理类可以拦截方法调用并进行相应的处理。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public interface HelloWorld {
void sayHello();
}
class HelloWorldImpl implements HelloWorld {
@Override
public void sayHello() {
System.out.println("Hello, world!");
}
}
public class DynamicProxyHandler implements InvocationHandler {
private Object target;
public DynamicProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method call");
Object result = method.invoke(target, args);
System.out.println("After method call");
return result;
}
}
public class DynamicProxyExample {
public static void main(String[] args) {
// 创建目标对象
HelloWorld target = new HelloWorldImpl();
// 创建代理处理器
DynamicProxyHandler handler = new DynamicProxyHandler(target);
// 创建代理实例
HelloWorld proxy = (HelloWorld) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
handler
);
// 调用代理方法
proxy.sayHello();
}
}
/**
* Before method: sayHello
* Hello, world!
* After method: sayHello
*/
Proxy 和 Reflection 属于元编程(Meta Programming),程序可以像数据一样被操作。简单来说,就是代码可以像数据一样在运行时修改。
动态代理(Proxy)和反射(Reflection)通常会同时出现,这是因为动态代理的实现依赖于反射机制。
- 动态代理的实现:Java 的动态代理机制使用 java.lang.reflect.Proxy 类来创建代理实例,而 Proxy 类本身依赖于反射来调用目标对象的方法。
- 方法调用的拦截:动态代理通过实现 InvocationHandler 接口来拦截方法调用。InvocationHandler 的 invoke 方法使用反射来调用目标对象的方法。
- 灵活性和动态性:反射允许在运行时检查和调用类的方法和字段,这为动态代理提供了必要的灵活性,使得代理对象可以在运行时动态地处理方法调用。
名称和对象
对象之间相互独立,多个名称(甚至是多个作用域内的多个名称)可以绑定到同一对象。这在其他语言中通常被称为别名(aliasing),也即多个名称(变量)引用同一个内存位置或对象。
class University:
def __init__(self, name):
self.name = name
wku = University("wenzhou-kean")
wenzhou_kean = wku
# 注意 wku 和 wenzhou_kean 为不同的名称绑定到的同一对象。
Python 初学者通常不容易理解这个概念,处理数字、字符串、元组等不可变基本类型时,可以不必理会。
my_age = 18
yours = my_age
my_age += 1
print(yours)
# 18
# 因为是不可变类型,这些对象一旦创建就不能修改。修改会导致新对象产生,
# 不会影响到原有对象
但是,对于涉及可变对象(如列表、字典,以及大多数其他类型)的 Python 代码的语义,别名可能会产生意料之外的效果。这样做,通常是为了让程序受益,因为别名在某些方面就像指针。 例如,传递对象的代价很小,因为实现只传递一个指针,你不用复制整个对象,节约了内存。 如果函数修改了作为参数传递的对象,调用者就可以看到更改——无需像 Pascal 那样用两个不同的机制来传参。
program Example;
type
IntArray = array of Integer;
procedure ModifyArray(var arr: IntArray);
begin
SetLength(arr, Length(arr) + 1);
arr[High(arr)] := 4;
end;
var
myArray: IntArray;
begin
SetLength(myArray, 3);
myArray[0] := 1;
myArray[1] := 2;
myArray[2] := 3;
ModifyArray(myArray);
WriteLn(Length(myArray)); // 输出: 4
end.
ChatGPT 曾言,ModifyArray使用var关键字按引用传递数组,因此对数组的修改会反映在原始数组中。如果不使用var关键字,数组将按值传递,函数内的修改不会影响原始数组。
作用域和命名空间
Python 中的命名空间(Namespace)是一个从名称到对象(万物皆对象, 其实也是对象,且小整数对象和str一样共享引用)的映射。
we = "wku_sra_member" # we 是一个名称,"wku_sra_member" 是一个对象,即名称对对象的映射
you = "wku_sra_member"
print(you is we) # string 共享引用
your_class = 2028
my_class = 2028
print(your_class is my_class)
# False
my_age = 18 # 小整数对象共享引用
yours = 18
print(yours is my_age)
# True
小整数缓存:Python 对小整数(通常是 -5 到 256)进行缓存和重用。这意味着在这个范围内的整数对象会被共享引用 大部分命名空间都是通过 Python 字典来实现的。
Tips: is 用来检查两个对象是否是同一个对象,== 用来检查两个对象是否相等。
有几种命名空间:
- 内置命名空间:包含了内置函数的名称,比如
abs()
、char()
、Exception
等。 - 全局命名空间:在模块中定义的名称,是模块对象的属性。模块的命名空间是全局命名空间。
是滴,模块也是对象,模块的属性是全局命名空间,模块的方法是全局函数。
前置:
if __name__ == "__main__":
有任何你不想在模块被导入时执行的代码,可以放在这个条件下。这样,只有直接运行模块时,才会执行这部分代码。 因为当你导入一个模块时,模块中的所有顶级代码(即不在函数或类中的代码)都会被执行。
# recruit.py
our_purpose = "help you get better and make the world a better place"
our_requirement_to_you = "passion, curiosity, openness, and most importantly non-aggressiveness"
class Student:
def __init__(self, occupation=None):
self.occupation = occupation
def join_us(you):
you.occupation = "member of wku-sra"
if __name__ == "__main__":
stewie = Student()
join_us(stewie)
print(stewie.occupation)
# 执行这个模块会输出: member of wku-sra
import recruit
print(recruit.our_purpose) # 访问这个模版对象的属性并打印
# 输出: help you get better and make the world a better place
brian = recruit.Student()
recruit.join_us(brian) # 调用模版对象的方法
print(brian.occupation)
# 输出: member of wku-sra
- 局部命名空间:在函数内定义的名称,是函数的局部变量。每个函数调用都会创建一个新的局部命名空间。 对象的属性集合也是命名空间的一种形式。关于命名空间的一个重要知识点是,不同命名空间中的名称之间绝对没有关系;例如,两个不同的模块都可以定义 maximize 函数,且不会造成混淆。用户使用函数时必须要在函数名前面加上模块名。
点号之后的名称是 属性。例如,表达式 z.real
中,real
是对象 z
的属性。严格来说,对模块中名称的引用是属性引用:表达式 modname.funcname
中,modname
是模块对象,funcname
是模块的属性。模块属性和模块中定义的全局名称之间存在直接的映射:它们共享相同的命名空间(说人话,模块属性==模块中定义的全局名称)!
属性可以是只读的或者可写的。 在后一种情况下(可写),可以对属性进行赋值。 模块属性是可写的:你可以写入 modname.the_answer = 42
。 也可以使用 del
语句删除可写属性。 例如,del modname.the_answer
将从名为modname
对象中移除属性 the_answer
。
# 如何实现只读
class MyClass:
def __init__(self, value):
self.__value = value
# 以 _ 开头的属性是私有的,但只是程序员之间的约定,不是强制,你要修改_value 也是可以的,__value 才是完全私有的(class definition里)。
# 当然,你真想访问__value也是可以的,python只是做name mangling
# 双下滑和单下滑前缀均会对语义产生影响,在from mofule import *时均不会被引入
# 都到这里了,顺便提一嘴 __dunderscore__ 定义特殊的方法和特性
@property
def value(self):
return self.__value
# 示例
obj = MyClass(10)
print(obj.value) # 输出: 10
obj.value = 20 # 会引发 AttributeError: can't set attribute
命名空间是在不同时刻创建的,且拥有不同的生命周期。
- 内置名称的命名空间是在 Python 解释器启动时创建的,永远不会被删除。
- 模块的全局命名空间在读取模块定义时创建;通常,模块的命名空间也会持续到解释器退出。从脚本文件读取或交互式读取的,由解释器顶层调用执行的语句是
__main__
模块调用的一部分,也拥有自己的全局命名空间。 - 内置名称实际上也在模块里,即
builtins
。
函数的局部命名空间在函数被调用时被创建,并在函数返回或抛出未在函数内被处理的异常时,被删除。(实际上,用“遗忘”来描述实际发生的情况会更好一些。)当然,每次递归调用都有自己的局部命名空间。
一个命名空间的 作用域
是 Python 代码中的一段文本区域text region),从这个区域可直接访问该命名空间。 “可直接访问”的意思是,这意味着在这个文本区域内,可以不使用限定名称(如模块名或类名即recruit.our_purpose
中的recruit
)直接引用命名空间中的名称。
作用域决定了名称的可见性和生命周期。
作用域虽然是被静态确定的,但会被动态使用。执行期间的任何时刻,都会有 3 或 4 个“命名空间可直接访问”的嵌套作用域:
最内层作用域,包含局部名称,并首先在其中进行搜索
那些外层闭包函数的作用域,包含“非局部、非全局”的名称,从最靠内层的那个作用域开始,逐层向外搜索。
倒数第二层作用域,包含当前模块的全局名称
最外层(最后搜索)的作用域,是内置名称的命名空间
如果一个名称被声明为全局(global
),则所有引用和赋值都将直接指向“倒数第二层作用域”,即包含模块的全局名称的作用域。 要重新绑定在最内层作用域以外找到的变量,可以使用 nonlocal
语句;如果未使用 nonlocal
声明,这些变量将为只读(尝试写入这样的变量将在最内层作用域中创建一个 新的 局部变量,而使得同名的外部变量保持不变)。
def outer_function():
x = "outer"
def inner_function():
nonlocal x
x = "inner"
print("Inner:", x)
inner_function()
print("Outer:", x)
outer_function()
# Inner: inner
# Outer: inner
# 如果不使用 nonlocal 语句,inner_function 中的 x 将是一个新的局部变量,不会影响 outer_function 中的 x。
通常,当前局部作用域将(按字面文本)引用当前函数的局部名称。在函数之外,局部作用域引用与全局作用域一致的命名空间:模块的命名空间。 类定义在局部命名空间内再放置另一个命名空间。
这个命名空间包含类的所有属性和方法。这个类的命名空间是嵌套在定义该类的作用域中的。
划重点,作用域是按字面文本确定的:模块内定义的函数的全局作用域就是该模块的命名空间,无论该函数从什么地方或以什么别名被调用。另一方面,实际的名称搜索是在运行时动态完成的。但是,Python 正在朝着“编译时静态名称解析”的方向发展,因此不要过于依赖动态名称解析!(局部变量已经是被静态确定了。)
Python 有一个特殊规定。如果不存在生效的 global
或 nonlocal
语句,则对名称的赋值总是会进入最内层作用域。赋值不会复制数据,只是将名称绑定到对象。删除也是如此:语句 del x
从局部作用域引用的命名空间中移除对 x 的绑定。所有引入新名称的操作都是使用局部作用域:尤其是 import
语句和函数定义会在局部作用域中绑定模块或函数名称。
global
语句用于表明特定变量在全局作用域里,并应在全局作用域中重新绑定;nonlocal
语句表明特定变量在外层作用域中,并应在外层作用域中重新绑定。
def scope_test():
def do_local():
spam = "local spam"
print("Inside do_local:", spam)
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"
print("Inside do_nonlocal:", spam)
def do_global():
global spam
spam = "global spam"
print("Inside do_global:", spam)
spam = "test spam"
do_local()
print("After do_local:", spam)
do_nonlocal()
print("After do_nonlocal:", spam)
do_global()
print("After do_global:", spam)
scope_test()
print("In global scope:", spam)
# Inside do_local: local spam
# After do_local: test spam
# Inside do_nonlocal: nonlocal spam
# After do_nonlocal: nonlocal spam
# Inside do_global: global spam
# After do_global: nonlocal spam
# In global scope: global spam
注意,局部 赋值(这是默认状态)不会改变 scope_test 对 spam 的绑定。 nonlocal 赋值会改变 scope_test 对 spam 的绑定,而 global 赋值会改变模块层级的绑定。 而且,global 赋值前没有 spam 的绑定。
类
定义语法:
class ClassName:
<statement-1>
.
.
.
<statement-N>
与函数定义 (def
语句) 一样,类定义必须先执行才能生效。
在实践中,类定义内的语句通常都是函数定义,但也可以是其他语句。这部分内容稍后再讨论。类里的函数定义一般是特殊的参数列表,这是由方法调用的约定规范所指明的
当进入类定义时,将创建一个新的命名空间,并将其用作局部作用域。 因此,所有对局部变量的赋值都是在这个新命名空间之内。 特别的,函数定义会绑定到这里的新函数名称。
当 Python 解释器遇到类定义时,它会创建一个新的命名空间。这个命名空间是一个独立的区域,用于存储类中定义的所有变量和方法。类中定义的所有变量和方法都属于这个局部作用域,而不是全局作用域。 在类定义中,对局部变量的赋值操作都是在这个新的命名空间内进行的。这意味着这些变量只在类的局部作用域内可见,而不会影响全局作用域中的变量。在类定义中定义的函数(即方法)会绑定到这个新的命名空间中的函数名称。这些方法可以访问类的局部变量和其他方法。 当 (从结尾处) 正常离开类定义时,将创建一个 类对象。 这基本上是一个围绕类定义所创建的命名空间的包装器; 原始的 (在进入类定义之前有效的) 作用域将重新生效,类对象将在这里与类定义头所给出的类名称进行绑定 (在这个示例中为 ClassName)。
类对象装了类定义中的所有变量和方法,并提供了对这些成员的访问机制。他封装了类的命名空间(包含所有变量和方法),是类的实例化的工厂。
类对象支持两种操作:属性引用和实例化。
- 属性引用 使用 Python 中所有属性引用所使用的标准语法: obj.name。 有效的属性名称是类对象被创建时存在于类命名空间中的所有名称。
class MyClass:
"""在类中直接定义的是静态属性,也被叫做类属性,这个属性被所有实例共享,可通过类名直接访问
也可以通过实例访问,但是不推荐,因为这样会让人误以为是实例属性,
修改静态属性会影响所有实例,因为这些静态属性是在实例之间共享的"""
i = 12345
def f(self):
return 'hello world'
MyClass.i
和 MyClass.f
就是有效的属性引用,将分别返回一个整数和一个函数对象。 类属性也可以被赋值,因此可以通过赋值来更改 MyClass.i
的值。 __doc__
也是一个有效的属性,将返回所属类的文档字符串: "A simple example class"。
- 实例化:使用函数表达式,可以把类对象视为是返回该类的一个新实例的不带参数的函数。
x = MyClass() # 创建类的新 实例 并将此对象分配给局部变量 x
实例化操作 (“调用”类对象) 会创建一个空对象(类似java里的默认构造器)。 许多类都希望创建的对象实例是根据特定初始状态定制的。 因此一个类可能会定义名为 __init__()
的特殊方法
def __init__(self):
self.data = []# 这个就是实例属性啦
# 当一个类定义了 __init__() 方法时,类的实例化会自动为新创建的类实例发起调用 __init__()
# 你也可以提供参数给 __init__() 方法
class Complex:
def __init__(self, realpart, imagpart):
self.r = realpart
self.i = imagpart
x = Complex(3.0, -4.5)
x.r, x.i
# (3.0, -4.5)
对象实例
对象实例(instance)所能理解的唯一操作是属性引用。 有两种有效的属性名称:数据属性和方法。
- 数据属性不需要声明;像局部变量一样,首次赋值时它们就产生了。 例如,如果
x
是上面创建的MyClass
的实例,那么x.counter
将创建一个名为counter
的新数据属性,而x.counter = 1
将修改它。注意,这样做不会影响类属性(这里的counter为实例属性)。 你也可以使用del
语句删除实例属性。且该数据属性只对当前实例有效。 - 方法是“属于”一个对象的函数。 对象的方法在定义类时就已经绑定了。 这就是为什么它们有一个
self
参数(类似java的this
,字如其意,self
和this
都用来表示引用自己),以便它们可以访问它们的数据属性。- 注意通过实例访问类的方法时,不需要传递self
参数。但是,如果通过类访问方法,则必须传递self
参数。python x.f() MyClass.f(x)
Bonus: 静态方法(static method)方法和类方法(class method) - 静态方法是类的方法,不需要访问类的实例。静态方法使用
@staticmethod
装饰器来定义。静态方法不需要self
参数和cls
,因此它们不会访问实例的数据属性。不能访问类属性或实例属性,只能访问定义在方法内部的局部变量。通常用于逻辑上属于类但不需要访问类或实例的任何属性或方法的功能。
class MyClass:
@staticmethod
def static_method():
print("This is a static method.")
# 通过类名访问静态方法
MyClass.static_method() # 没访问实例
# 通过实例访问静态方法,可行但不推荐,在某些语言里为禁止行为
obj = MyClass()
obj.static_method()
- 类方法 (
@classmethod
),需要cls
参数,表示类本身。可以访问类属性和类方法,但不能直接访问实例属性。通常用于需要访问或修改类属性的方法,或者需要在不创建实例的情况下调用的方法。
class MyClass:
class_attribute = 0
@classmethod
def class_method(cls):
print(f"This is a class method. Class attribute: {cls.class_attribute}")
# 通过类名访问类方法
MyClass.class_method() # 输出: This is a class method. Class attribute: 0
# 通过实例访问类方法
obj = MyClass()
obj.class_method() # 输出: This is a class method. Class attribute: 0
Bonus in Bonus:
java
static and instances
访问类型 | 静态方法 (static) | 实例方法 (instance) |
---|---|---|
类属性 | 可以访问 | 可以访问 |
实例属性 | 不能直接访问 | 可以访问 |
类方法 | 可以访问 | 可以访问 |
实例方法 | 不能直接访问 | 可以访问 |
局部变量 | 可以访问 | 可以访问 |
其实吧,python 的 class method 更像java的 static method
方法对象
方法的特殊之处就在于实例对象会作为函数的第一个参数被传入。 在我们的示例中,调用x.f()
其实就相当于 MyClass.f(x)
。 总之,调用一个具有 n 个参数的方法就相当于调用再多一个参数的对应函数,这个参数值为方法所属实例对象,位置在其他参数之前 通常,方法在调用时会自动将实例作为第一个参数传递给方法。 但是,有时需要将方法作为函数对象进行传递,而不是作为绑定方法进行调用(这意味着你可以将方法本身传递给另一个函数或变量,而不立即调用它)。 这可以通过方法对象的 __func__
属性来实现。 例如,x.f
是一个方法对象,但 x.f.__func__
是一个函数对象。
方法的运作方式如下。 当一个实例的非数据属性被引用时,将搜索该实例所属的类。 如果名称表示一个属于函数对象的有效类属性,则指向实例对象和函数对象的引用将被打包为一个方法对象。 当传入一个参数列表调用该方法对象时,将基于实例对象和参数列表构造一个新的参数列表,并传入这个新参数列表调用相应的函数对象。
函数对象本身不绑定到任何实例,因此无法直接访问实例的私有属性和方法。未绑定到特定实例,可以在不同的上下文中重用。例如,可以将函数对象传递给其他函数或作为回调函数使用。
区别 | 方法对象 | 函数对象 |
---|---|---|
绑定 | 绑定到类实例 | 类的属性,不绑定到实例 |
传递实例 | 自动传递实例 (self) | 需要显式传递实例 (self) |
访问方式 | 通过实例访问类的方法 | 通过方法对象的 __func__ 属性 |
重用 | 绑定到特定实例 | 不绑定到特定实例,可以重用 |
实例变量
一般来说,实例变量用于每个实例的唯一数据,而类变量用于类的所有实例共享的属性和方法:
class Dog:
kind = 'canine' # class variable 类变量 shared by all instances 被所有实例共享
def __init__(self, name):
self.name = name # instance variable 实例变量 unique to each instance 每个实例独有
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # shared by all dogs
'canine'
>>> e.kind # shared by all dogs
'canine'
>>> d.name # unique to d
'Fido'
>>> e.name # unique to e
'Buddy'
补充
- 如果同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例:
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
- 类不能用于实现纯抽象数据类型。 实际上,在 Python 中没有任何东西能强制隐藏数据 --- 它是完全基于约定的
- 可以向一个实例对象添加他们自己的数据属性而不会影响方法的可用性,只要保证避免名称冲突
- 方法的第一个参数常常被命名为 self。 这也不过就是一个约定: self 这一名称在 Python 中绝对没有特殊含义。 但是要注意,不遵循此约定会使得你的代码对其他 Python 程序员来说缺乏可读性,而且也可以想像一个 类浏览器 程序的编写可能会依赖于这样的约定。
- 任何一个作为类属性的函数都为该类的实例定义了一个相应方法。 函数定义的文本并非必须包含于类定义之内:将一个函数对象赋值给一个局部变量也是可以的
# Function defined outside the class
def f1(self, x, y):
return min(x, x+y)
class C:
f = f1
def g(self):
return 'hello world'
h = g
方法可以通过使用 self 参数的方法属性调用其他方法
def add(self, x):
self.data.append(x)
方法可以通过与普通函数相同的方式引用全局名称。与方法相关联的全局作用域就是包含该方法的定义语句的模块。(类永远不会被用作全局作用域。)尽管一个人很少会有好的理由在方法中使用全局作用域中的数据,全局作用域依然存在许多合理的使用场景:举个例子,导入到全局作用域的函数和模块可以被方法所使用,定义在全局作用域中的函数和类也一样。通常,包含该方法的类本身就定义在全局作用域中,而在下一节中我们将会发现,为何有些时候方法需要引用其所属类。
每个值都是一个对象,因此具有 类 (也称为 类型),并存储为 object.class 。
继承
派生类(derivedC)的定义方式如下:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
名称 BaseClassName
必须定义于可从包含所派生的类的定义的作用域访问的命名空间中(你能让dericed class 找到base class) 派生类定义的执行过程与基类相同。 当构造类对象时,基类会被记住。 此信息将被用来解析属性引用:如果请求的属性在类中找不到,搜索将转往基类中进行查找。 如果基类本身也派生自其他某个类,则此规则将被递归地应用。 派生类的实例化没有任何特殊之处: DerivedClassName() 会创建该类的一个新实例。 方法引用将按以下方式解析:搜索相应的类属性,如有必要将按基类继承链逐步向下查找,如果产生了一个函数对象则方法引用就生效。 派生类可能会重写其基类的方法。 因为方法在调用同一对象的其他方法时没有特殊权限,所以基类方法在尝试调用调用同一基类中定义的另一方法时,可能实际上调用是该基类的派生类中定义的方法。 在派生类中的重写方法实际上可能想要扩展而非简单地替换同名的基类方法。 有一种方式可以简单地直接调用基类方法:即调用 BaseClassName.methodname(self, arguments)
。 有时这对客户端来说也是有用的。 (请注意仅当此基类可在全局作用域中以 BaseClassName 的名称被访问时方可使用此方式。)
Bonus:
- 当你定义一个派生类时,首先会构造父类,然后再构造派生类。这是因为派生类需要继承父类的属性和方法,所以必须先确保父类已经被正确构造。
- 在 Java 中,调用父类构造函数的
super()
必须是子类构造函数中的第一个语句。如果不这样做,编译器会报错。 - python 不要求 super() 必须是第一个语句,但建议你像写java代码一样把super()放在构造函数的第一行。
class BaseClass:
def __init__(self):
print("BaseClass constructor called")
class DerivedClass(BaseClass):
def __init__(self):
# 首先调用父类的构造函数
super().__init__()
print("DerivedClass constructor called")
# 创建派生类的实例
derived_instance = DerivedClass()
Python有两个内置函数可被用于继承机制:
- 使用 isinstance() 来检查一个实例的类型: isinstance(obj, int) 仅会在 obj.class 为 int 或某个派生自 int 的类时为 True。
- 使用 issubclass() 来检查类的继承关系: issubclass(bool, int) 为 True,因为 bool 是 int 的子类。 但是,issubclass(float, int) 为 False,因为 float 不是 int 的子类。
多重继承
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
对于多数目的来说,在最简单的情况下,你可以认为搜索从父类所继承属性的操作是深度优先、从左到右的(depth-first and left-to-right),当层次结构存在重叠时不会在同一个类中搜索两次。 因此,如果某个属性在 DerivedClassName 中找不到,就会在 Base1 中搜索它,然后(递归地)在 Base1 的基类中搜索,如果在那里也找不到,就将在 Base2 中搜索,依此类推。
Bonus:深度优先且从左到右的遍历算法的示例,遍历二叉树
class TreeNode:
def __init__(self, value=0, left=None, right=None):
self.value = value
self.left = left
self.right = right
def dfs(node):
if node is None:
return
# 访问当前节点
print(node.value)
# 递归遍历左子树
dfs(node.left)
# 递归遍历右子树
dfs(node.right)
# 示例用法
if __name__ == "__main__":
# 创建一个二叉树
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
root.right.left = TreeNode(6)
root.right.right = TreeNode(7)
# 执行深度优先且从左到右的遍历
dfs(root)
私有变量
仅限从一个对象内部访问的“私有”实例变量在 Python 中并不存在。 但是,大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如 _spam) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。 这应当被视为一个实现细节,可能不经通知即加以改变。 由于存在对于类私有成员的有效使用场景(例如避免名称与子类所定义的名称相冲突),因此存在对此种机制的有限支持,称为 名称改写。 任何形式为 spam 的标识符(至少带有两个前缀下划线,至多一个后缀下划线)的文本将被替换为 _classnamespam,其中 classname 为去除了前缀下划线的当前类名称。 这种改写不考虑标识符的句法位置,只要它出现在类定义内部就会进行。
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # private copy of original update() method
class MappingSubclass(Mapping):
def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)
上面的示例即使在 MappingSubclass 引入了一个 __update
标识符的情况下也不会出错,因为它会在 Mapping 类中被替换为 _Mapping__update
而在 MappingSubclass 类中被替换为 _MappingSubclass__update
。 请注意,改写规则的设计主要是为了避免意外冲突;访问或修改被视为私有的变量仍然是可能的。这在特殊情况下甚至会很有用,例如在调试器中。 请注意传递给 exec() 或 eval() 的代码不会将发起调用类的类名视作当前类;这类似于 global 语句的效果,因此这种效果仅限于同时经过字节码编译的代码。 同样的限制也适用于 getattr()
, setattr()
和 delattr()
,以及对于 __dict__
的直接引用。