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

qrjkbowd  于 2023-03-04  发布在  Python
关注(0)|答案(6)|浏览(112)

我绝对是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中做类似的事情吗?我可以在构造函数方法之外声明类字段吗?

uurv41yg

uurv41yg1#

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
t2a7ltrp

t2a7ltrp2#

在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
qf9go6mv

qf9go6mv3#

在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'
>>>
fnx2tebb

fnx2tebb4#

由于Python是动态类型的,你不必事先声明变量,而是在运行时初始化它们。这也意味着你不必向构造函数添加示例属性,而是可以在以后的任何时间添加它们。事实上,你可以向任何对象添加属性,包括类对象本身。向构造函数添加示例属性主要是一致性和可读性的问题。
添加到类定义中的数据属性在Python中被称为class attributes(我不懂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)
1mrurvl1

1mrurvl15#

正如您所注意到的,这不是核心python语言的一部分。
这对我来说也是一个缺失的功能,原因有几个:

*可读性/可维护性:使用在构造函数中或在别处动态定义属性的经典Python方式,当阅读代码时,对象的“契约”(或至少预期的Duck契约)是什么并不明显。
*致密性:只使用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'.

注意,我使用的是python3.7+的类型提示语法,但是pyfields兼容旧版本(python 2,python3.5),参见documentation
您甚至可以通过自动创建构造函数或使用@autofields为您生成field()调用来进一步简化此示例。pyfields还提供了@autoclass,因此您甚至可以轻松生成其他类行为,如字符串表示、等式、转换为dict等。
注意,pyfields的灵感来自attrs这样的巨头,但它的独特之处在于它保留了分离原则,因此它不会在背后摆弄__init____setattr__,因此允许在set上验证字段(而且不仅仅是在构造函数中),并且还开发了优雅的混合类定义字段和方法,但是没有构造函数。

dtcbnfnu

dtcbnfnu6#

从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

相关问题