我可以在构造函数方法之外声明 Python 类字段吗?



我绝对是Python的新手(我来自Java),我对类字段有以下疑问。

考虑这样的代码:

class Toy():
def __init__(self, color, age):
self.color = color
self.age = age
action_figure = Toy('red', 10)

所做的是清晰且非常简单的:

它正在定义一个Toy类。构造函数正在定义两个字段来设置其值。最后(在main中)创建一个新的Toy实例,在构造函数调用中传递字段的值。

在Java中定义相同的类,我做了这样的事情:

public class Toy {
private String color;
private int age;
// CONSTRUCTOR:
public Dog(String color, int age) {
this.color = color;
this.age = age;
}
}

它很相似,但我已经发现了一个很大的区别。在我的 Java 代码中,我将类字段声明为构造函数外部的变量。在 Python 中,我直接在构造函数中定义类字段。所以这意味着在 Java 中我可以声明多个字段并使用构造函数方法仅初始化这些字段的子集,例如:

public class Toy {
private String color;
private int age;
private String field3;
private String field4;
private String field5;
// CONSTRUCTOR:
public Dog(String color, int age) {
this.color = color;
this.age = age;
}
}

其中我还有field3field4field5字段,这些字段不会被我的构造函数初始化。(以防我随后可以使用 setter 方法设置它们的值。

我可以在 Python 中做类似的事情吗?是否可以在构造函数方法之外声明类字段?

python 中的类与 c++/java 中的类有着根本的不同,因为 c++/java 类具有固定的数据结构和大小(字节),因为每个属性都是在所有方法之外声明或定义的(通常作为私有变量),但在 python 中,一切都是动态的(动态类型)。

选择在构造函数中定义属性与其他方法是为了其他人能够快速理解您的代码/数据结构(尽管由于动态性,调用python类数据结构是不合适的)

作为动态性的示例,您甚至可以在运行时向类甚至实例添加新的方法和属性:

class A:
pass

在运行时向类添加内容(这些内容将添加到该类的所有现有和将来的实例中):

A.key = val
def f(self):
return 0
A.myfunction = f
a = A()
a.myfunction()
# 0

在运行时向单个实例添加内容:

a=A()
a.attr='something'
def f(self):
return 0
a.fun=f.__get__(a)
a.fun()
# 0

在python中,你可以做这样的事情:

class Toy():
def__init__(self, color, age):
self.color = color
self.age = age
def another_method(self, f):
self.field3 = f + 4
return self.field3

但为了清楚起见,通常建议(更多参数在这里:https://stackoverflow.com/a/38378757/4709400)初始化构造函数中的所有实例变量,因此您将执行以下操作:

class Toy():
def__init__(self, color, age):
self.color = color
self.age = age
self.field3 = None
self.field4 = 0 # for instance
self.field5 = "" # for instance
def another_method(self, f):
self.field3 = f + 4
return self.field3

在Python中并不需要这样做。在 Java 中称为"实例变量"的东西可以随时添加到类的实例中:

class Person:
def __init__(self, name):
self.name = name
def get_a_job(self):
self.job = "Janitor"
print(f"{self.name} now has a job!")
p1 = Person("Tom")
p2 = Person("Bob")
p1.get_a_job()
print(p1.job)
print(p2.job)

输出:

Tom now has a job!
Janitor
Traceback (most recent call last):
File "...", line 17, in <module>
print(p2.job)
AttributeError: 'Person' object has no attribute 'job'
>>> 

由于 Python 是动态类型的,因此无需事先声明变量,但它们在运行时初始化。这也意味着您不必向构造函数添加实例属性,但您可以在以后随时添加它们。实际上,您可以向任何对象添加属性,包括类对象本身。向构造函数添加实例属性主要是一致性和可读性的问题。

添加到类定义中的数据属性在 Python 中称为类属性(我不懂 Java,但我相信,这对应于静态变量)。这很有用,例如跟踪所有类实例:

class Dog:
lineage = {'Phylum':'Chordata', 'Class':'Mammalia', 'Species':'Canis lupus'}
all_dogs = []
def __init__(self, fur_color, tail_length):
self.fur_color = fur_color
self.tail_length = tail_length
self.all_dogs.append(self)  # class attributes can be accessed via the instance
Bello = Dog('white',50)
print(Dog.all_dogs)
print(Dog.[0].fur_color)

正如你所指出的,这不是核心python语言的一部分。

这对我来说也是一个缺失的功能,原因有几个:

  • 读性/可维护性:使用经典的 Python 方式在构造函数或其他地方动态定义属性,在读取代码时,对象的"合约"(或至少是预期的鸭子合约)并不明显。

  • compacity:仅用self.<foo> = <foo>创建长构造函数并不是最有趣的,您需要的字段越多,您必须编写的行就越多

  • 能够扩展字段的协定,例如在默认值可变的情况下添加默认值工厂,或添加值验证器

  • 能够创建混合类,即依赖于某些字段实现某些功能的类,但不强制使用任何构造函数。

这就是我创建pyfields的原因。使用此库,每个字段都定义为一个类成员:

from pyfields import field
from typing import List
class Toy:
color: str = field(doc="The toy's color, a string.")
age: int = field(doc="How old is this Toy. An integer number of years.")
field3: str = field(default='hello', check_type=True)
field4: List[str] = field(default_factory=lambda obj: ['world'])
field5: str = field(default=None, 
validators={'should be 1-character long': lambda x: len(x) == 1})
def __init__(self, color, age):
self.color = color
self.age = age

t = Toy(color='blue', age=12)
print(t.field3 + ' ' + t.field4[0])
print(t.field5 is None)
t.field5 = 'yo'

收益 率

hello world
True
Traceback (most recent call last):
...
valid8.entry_points.ValidationError[ValueError]: Error validating [Toy.field5=yo]. InvalidValue: should be 1-character long. Function [<lambda>] returned [False] for value 'yo'.

请注意,我使用上面的python 3.7+类型提示语法,但pyfields与旧版本(python 2,python 3.5)兼容,请参阅文档。

您甚至可以通过自动创建构造函数或使用@autofields为您生成field()调用来进一步简化此示例。pyfields还提供了@autoclass,因此您甚至可以轻松生成其他类行为,例如字符串表示、相等、转换为字典等。请参阅自动分类文档。

请注意,pyfields受到巨人的启发,如attrs,但独特的之处在于它保留了隔离原则。因此,它不会在背后摆弄__init____setattr__。因此,这允许在集合上验证字段(不仅在构造函数中),还可以开发定义字段和方法的优雅混合类,但没有构造函数。

从 Python 3.7 开始,@dataclass装饰器提供了所需的行为。

from dataclasses import dataclass
from typing import Optional
@dataclass
class Toy1:
color: str
age: int
field3: Optional[str] = None
fielg4: Optional[str] = None
field5: Optional[str] = None
def __init__(self, color:str, age: int) -> None:
self.color = color
self.age = age
toy1 = Toy('red', 2)

构造函数甚至可以被抑制,行为是相同的。

@dataclass
class Toy2:
color: str
age: int
field3: Optional[str] = None
fielg4: Optional[str] = None
field5: Optional[str] = None
toy2 = Toy('red', 2)

@dataclass将启用其他函数,例如实例的漂亮字符串表示形式(__repr__):Toy(color='red', age=2, field3=None, fielg4=None, field5=None)

或实例之间的直接比较:

toy1 == toy2
True

全面的解释及其背后的合理性可以在以下位置找到: https://realpython.com/python-data-classes/

如果要在运行时强制实施这些类型,则可以在此模型中使用pydantic

最新更新