Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 297 additions & 0 deletions doc/pyclass-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
# 问题描述

目前,LLGo 仅支持调用 Python 函数,不支持处理类和对象。官方仓库 [goplus/lib/py](https://github.com/goplus/lib/py) 也缺少与 Python 类相关的 LLGo Bindings 代码示例。

为了扩展 LLGo 与 Python 生态系统的集成能力,需要添加对 Python 类的支持。一方面,llpyg 需要生成相应的 LLGo Bindings 代码,另一方面,LLGo 需要添加相应的处理逻辑。

## 符号信息

Python 类声明包含以下符号信息:
- 名称和继承关系
- 方法(包括类方法、实例方法、静态方法和特殊方法)
- 属性(包括类属性、实例属性)

Python 类示例:
```Python
class Animal:
def __init__(self, name):
self._name = name

def speak(self):
pass

class Dog(Animal):

DOG_NAME = "Dog"

def __init__(self, name, age):
super().__init__(name)
self._age = age

def speak(self):
print(f"Dog {self._name} is speaking")

@property
def age(self):
return self._age

@age.setter
def age(self, age):
self._age = age

@classmethod
def bark(cls, msg):
print(f"Dog is barking {msg}")

@staticmethod
def sleep():
print("Dog is sleeping")

def __str__(self):
return f"Dog {self._name} is {self._age} years old"
```

使用示例:
```Python
dog = Dog("Buddy", 3)
dog.speak()
age = dog.age
dog.age = 4
Dog.DOG_NAME = "Buddy"
print(Dog.DOG_NAME) # Buddy
Dog.bark("Woof")
Dog.sleep()
print(dog)
```

## LLGo Bindings 设计

### 设计原则

以下是 LLGo链接 Python 符号的核心代码:
```go
func (b Builder) pyLoadAttrs(obj Expr, name string) Expr {
attrs := strings.Split(name, ".")
fn := b.Pkg.pyFunc("PyObject_GetAttrString", b.Prog.tyGetAttrString())
for _, attr := range attrs {
obj = b.Call(fn, obj, b.CStr(attr))
}
return obj
}
```
LLGo 通过不断调用 `PyObject_GetAttrString` 来获取 Python 对象。基于此逻辑,LLGo Bindings 的设计旨在**方便用户使用的同时使 Go 符号能够正确链接到对应的 Python 符号**。


### 类
将 Python 类转换为 Go 结构体,通过嵌入结构体实现类似继承的功能(包括多继承)。

例如 `Dog` 类继承自 `Animal` 类:
```Python
class Animal:
pass

class Dog(Animal):
pass
```
对应的 Go 结构体:
```Go
type Animal struct {
py.Object
}

type Dog struct {
Animal
}
```


### 实例

对于类实例,通过声明构造函数 `New[ClassName]` 进行创建。

Python 代码示例:
```Python
class Dog(Animal):
def __init__(self, name, age):
super().__init__(name)
self._age = age
```
符号链接方式:
```Python
dog = mod.Dog("Buddy", 3)
```

LLGo Binding 设计:链接的符号为 `py.[ClassName]`
> 根据 LLGo 执行逻辑,通过链接 `py.[ClassName]` 来得到类实例实际上是先调用类的 `__new__` 方法,再调用 `__init__` 方法。Python 规定两个方法的参数必须一致。因此:
> - 当类显式声明了 `__init__` 方法时,构造函数的参数从 `__init__` 方法中获取。
> - 当类显式声明了 `__new__` 方法时,但未显式声明 `__init__` 方法时,构造函数的参数从 `__new__` 方法中获取。
Comment thread
toaction marked this conversation as resolved.
> - 当两个方法都没有显式声明时,查看父类是否声明了初始化方法,若都不存在,则不提供该类的构造函数或提供空参的构造函数。

```Go
//go:linkname NewDog py.Dog
func NewDog(name *py.Object, age *py.Object) *Dog
```

LLGo 用户使用方式:

```Go
dog := NewDog(py.Str("Buddy"), py.Long(3))
```

### 方法
在 Python 类中,方法分为实例方法、魔法方法、类方法和静态方法。

**实例方法和魔法方法**属于类实例:
```Python
class Dog(Animal):
def speak(self):
print(f"Dog {self._name} is speaking")

def __str__(self):
return f"Dog {self._name} is {self._age} years old"
```
Python 符号链接方式:
```Python
# 实例方法
mod.Dog.speak(dog)
# 魔法方法
mod.Dog.__str__(dog)
```
对于实例方法和魔法方法,在进行方法调用时,传入的第一个参数必须为实例对象。因此,将它们转为**结构体方法**。

LLGo Binding 设计:链接的符号为 `py.[ClassName].[methodName]`。
> 对于魔法方法,去除前后下划线,使其更符合命名规范与用户使用习惯。
```Go
//llgo:link (*Dog).Speak py.Dog.speak
func (d *Dog) Speak() *py.Object {
return nil
}

//llgo:link (*Dog).Str py.Dog.__str__
func (d *Dog) Str() *py.Object {
return nil
}
```
LLGo 用户使用方式:
```Go
dog := NewDog(py.Str("Buddy"), py.Long(3))
dog.Speak()
dog.Str()
```

**类方法和静态方法**的调用与类实例无关:
```Python
class Dog:
@classmethod
def bark(cls, msg):
print(f"Dog is barking {msg}")

@staticmethod
def sleep():
print("Dog is sleeping")
```
符号链接方式:
```Python
# class method
mod.Dog.bark("Hello")
# static method
mod.Dog.sleep()
```
在进行方法调用时,参数与类实例和类对象都无关。因此可以将它们转为**函数**。

LLGo Binding 设计:链接的符号为 `py.[ClassName].[methodName]`。
> 为了接近 Python 的语法以及防止命名冲突,添加 `[ClassName]` 作为前缀。
```go
//go:linkname DogBark py.Dog.bark
func DogBark(msg *py.Object) *py.Object

//go:linkname DogSleep py.Dog.sleep
func DogSleep() *py.Object
```
用户使用方式:
```go
DogBark("Hello")
DogSleep()
```

### 属性
在 Python 类中,属性分为实例属性和类属性。

**实例属性**包含两种类别:property 和 attribute。Python 推荐使用 property 来实现属性的封装和访问控制。
```Python
class Dog(Animal):
@property
def age(self):
return self._age

@age.setter
def age(self, age):
self._age = age
```
符号链接方式:
```Python
# get
age = mod.Dog.age.__get__(dog)
# set
mod.Dog.age.__set__(dog, 4)
```
通过方法调用的方式来获取和设置属性值,传入的第一个参数必须为实例对象。因此可以将它们转为**结构体方法**。

LLGo Binding 设计:链接的符号为 `py.[ClassName].[attributeName].__get__` 和 `py.[ClassName].[attributeName].__set__`。
> 对于属性的获取操作,为了符合用户使用习惯,不添加 Get 前缀。

```Go
//llgo:link (*Dog).Age py.Dog.age.__get__
func (d *Dog) Age() *py.Object {
return nil
}

//llgo:link (*Dog).SetAge py.Dog.age.__set__
func (d *Dog) SetAge(age *py.Object) {
}
```

LLGo 使用方式:

```Go
dog := NewDog(py.Str("Buddy"), py.Long(3))
dog.Age()
dog.SetAge(py.Long(4))
```

**类属性**属于类对象。在 Python 中可以直接通过类对象来获取和设置属性值。
```Python
class Dog(Animal):
DOG_NAME = "Dog"
```
符号链接方式:
```Python
DOGNAME = mod.Dog.DOG_NAME
```
与 property 不同,类属性为具体的 Python 对象,无法通过方法调用的方式来获取和设置自身属性值。若想得到类属性,目前有两种处理方式:
1. 直接获取类属性,得到的是 `py.Object` 对象。LLGo Binding 链接的符号为 `py.[ClassName].[AttributeName]`。

> 为了接近 Python 的语法以及防止命名冲突,添加 `[ClassName]` 作为前缀。
```go
//go:linkname DogDOGNAME py.Dog.DOG_NAME
var DogDOGNAME *py.Object
```
用户使用方式:
```go
dogName := DogDOGNAME
```
缺点:无法直接对类属性进行赋值操作。

2. 先获取类对象,然后通过 `getAttribute` 和 `setAttribute` 方法获取和设置类属性:
> 为了防止与结构体命名冲突,添加 Class 作为后缀
```go
//go:linkname DogClass py.Dog
var DogClass *py.Object
```
用户使用方式:
```go
dogClass := Dog
dogName := dogClass.getAttribute(py.Str("DOG_NAME"))
dogClass.setAttribute(py.Str("DOG_NAME"), py.Str("Buddy"))
```
缺点:用户无法直接查看类中存在哪些属性。
Loading