Cách tiếp cận đúng để xác thực các thuộc tính của một thể hiện của lớp


83

Có một lớp Python đơn giản như thế này:

class Spam(object):
    __init__(self, description, value):
        self.description = description
        self.value = value

Tôi muốn kiểm tra các ràng buộc sau:

  • "mô tả không được để trống"
  • "giá trị phải lớn hơn 0"

Tôi có nên:
1. xác thực dữ liệu trước khi tạo đối tượng thư rác?
2. kiểm tra dữ liệu về __init__phương pháp?
3. tạo một is_validphương thức trên lớp Spam và gọi nó bằng spam.isValid ()?
4. Tạo một is_validphương thức tĩnh trên lớp Spam và gọi nó bằng Spam.isValid (description, value)?
5. kiểm tra dữ liệu trên khai báo setters?
6. v.v.

Bạn có thể đề xuất một cách tiếp cận được thiết kế tốt / Pythonic / không dài dòng (trên lớp có nhiều thuộc tính) / thanh lịch không?

Câu trả lời:


105

Bạn có thể sử dụng các thuộc tính Python để áp dụng rõ ràng các quy tắc cho từng trường riêng biệt và thực thi chúng ngay cả khi mã máy khách cố gắng thay đổi trường:

class Spam(object):
    def __init__(self, description, value):
        self.description = description
        self.value = value

    @property
    def description(self):
        return self._description

    @description.setter
    def description(self, d):
        if not d: raise Exception("description cannot be empty")
        self._description = d

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, v):
        if not (v > 0): raise Exception("value must be greater than zero")
        self._value = v

Một ngoại lệ sẽ được đưa ra khi cố gắng vi phạm các quy tắc, ngay cả trong __init__hàm, trong trường hợp đó, việc xây dựng đối tượng sẽ không thành công.

CẬP NHẬT: Khoảng giữa năm 2010 và bây giờ, tôi đã biết về operator.attrgetter:

import operator

class Spam(object):
    def __init__(self, description, value):
        self.description = description
        self.value = value

    description = property(operator.attrgetter('_description'))

    @description.setter
    def description(self, d):
        if not d: raise Exception("description cannot be empty")
        self._description = d

    value = property(operator.attrgetter('_value'))

    @value.setter
    def value(self, v):
        if not (v > 0): raise Exception("value must be greater than zero")
        self._value = v

1
+1 giải pháp thanh lịch, cảm ơn, bạn không nghĩ là hơi dài dòng cho một lớp học nhỏ như vậy?
systempuntoout

2
Đồng ý, nó không phải là giải pháp tốt nhất. Python thích các lớp có phạm vi tự do (nghĩ là gà), và ý tưởng về các thuộc tính kiểm soát quyền truy cập là một suy nghĩ muộn màng. Phải nói rằng, điều này sẽ không ngắn gọn hơn nhiều bằng bất kỳ ngôn ngữ nào khác mà tôi có thể nghĩ ra.
Marcelo Cantos

2
@MarceloCantos Tôi nhận ra đây là một câu hỏi cũ, nhưng dựa trên tài liệu (mặc dù dành cho Python 3), nên self.description = descriptionsử dụng dấu gạch dưới, tức là self._description = description, hay điều đó không quan trọng? Điều này có cần thiết hay chỉ là một cái gì đó tương tự như phiên bản của Python của các biến "riêng tư"?
John Bensin

12
@JohnBensin: Có và không. self.description = …chỉ định thông qua tài sản, trong khi self._description = …chỉ định trực tiếp cho trường cơ sở. Việc sử dụng cái nào trong quá trình xây dựng là một lựa chọn thiết kế, nhưng thường an toàn hơn nếu bạn luôn chỉ định thông qua tài sản. Ví dụ, đoạn mã trên sẽ đưa ra một ngoại lệ nếu bạn gọi Spam('', 1), vì nó phải như vậy.
Marcelo Cantos

1
Tôi không nghĩ rằng nó quá dài dòng. Cách thay thế là có thể đặt giá trị thành các giá trị không hợp lệ.
Tony Ennis

10

Nếu bạn chỉ muốn xác thực các giá trị khi đối tượng được tạo VÀ việc chuyển các giá trị không hợp lệ được coi là lỗi lập trình thì tôi sẽ sử dụng các xác nhận:

class Spam(object):
    def __init__(self, description, value):
        assert description != ""
        assert value > 0
        self.description = description
        self.value = value

Điều này ngắn gọn như bạn sẽ nhận được, và các tài liệu rõ ràng rằng đây là những điều kiện tiên quyết để tạo ra đối tượng.


cảm ơn Dave, bằng cách sử dụng khẳng định, làm thế nào để tôi chỉ định cho khách hàng của lớp đó những gì đã xảy ra (mô tả hoặc giá trị)? Bạn không nghĩ rằng khẳng định nên được sử dụng chỉ để kiểm tra các điều kiện không bao giờ nên xảy ra?
systempuntoout

1
Bạn có thể thêm thông báo vào câu lệnh khẳng định, ví dụ assert value > 0, "value attribute to Spam must be greater than zero". Các xác nhận thực sự là thông báo cho nhà phát triển và không nên bị mã máy khách bắt được, vì chúng chỉ ra lỗi lập trình. Nếu bạn muốn máy khách bắt và xử lý lỗi thì hãy nêu rõ ràng một ngoại lệ, chẳng hạn như ValueError, như được hiển thị trong các câu trả lời khác.
Dave Kirby

1
Để trả lời câu hỏi thứ hai của bạn, các xác nhận có nên được sử dụng để kiểm tra các điều kiện không bao giờ nên xảy ra - đó là lý do tại sao tôi đã nói "nếu truyền các giá trị không hợp lệ được coi là lỗi lập trình ...". Nếu đó không phải là trường hợp thì không sử dụng xác nhận.
Dave Kirby

def nên được chèn trước__init__
datapug

1
Cảm ơn @datapug, đã sửa lỗi chính tả.
Dave Kirby

7

Trừ khi bạn không muốn tự mình triển khai, bạn có thể chỉ cần sử dụng formencode . Nó thực sự tỏa sáng với nhiều thuộc tính và lược đồ (chỉ là lược đồ lớp con) và có rất nhiều nội trang trình xác thực hữu ích. Như bạn có thể thấy, đây là phương pháp "xác thực dữ liệu trước khi tạo đối tượng thư rác".

from formencode import Schema, validators

class SpamSchema(Schema):
    description = validators.String(not_empty=True)
    value = validators.Int(min=0)

class Spam(object):
    def __init__(self, description, value):
        self.description = description
        self.value = value

## how you actually validate depends on your application
def validate_input( cls, schema, **input):
    data = schema.to_python(input) # validate `input` dict with the schema
    return cls(**data) # it validated here, else there was an exception

# returns a Spam object
validate_input( Spam, SpamSchema, description='this works', value=5) 

# raises an exception with all the invalid fields
validate_input( Spam, SpamSchema, description='', value=-1) 

Bạn cũng có thể thực hiện việc kiểm tra __init__(và làm cho chúng hoàn toàn trong suốt bằng các bộ mô tả | bộ trang trí | siêu kính), nhưng tôi không phải là người thích điều đó. Tôi thích một rào cản rõ ràng giữa đầu vào của người dùng và các đối tượng bên trong.


6

nếu bạn chỉ muốn xác thực những giá trị được truyền cho hàm tạo, bạn có thể làm:

class Spam(object):
    def __init__(self, description, value):
        if not description or value <=0:
            raise ValueError
        self.description = description
        self.value = value

Điều này tất nhiên sẽ không ngăn cản bất cứ ai làm điều gì đó như thế này:

>>> s = Spam('s', 5)
>>> s.value = 0
>>> s.value
0

Vì vậy, cách tiếp cận đúng phụ thuộc vào những gì bạn đang cố gắng hoàn thành.


đây là cách tiếp cận thực tế của tôi; nhưng tôi không thích khi việc kiểm tra số thuộc tính và \ hoặc các ràng buộc phức tạp hơn. Nó có vẻ làm lộn xộn phương thức init quá nhiều.
systempuntoout

1
@system: bạn có thể tách kiểm tra tính hợp lệ thành phương pháp riêng: không có quy tắc cứng và nhanh về tình huống này.
SilentGhost

1

Bạn có thể thử pyfields:

from pyfields import field

class Spam(object):
    description = field(validators={"description can not be empty": lambda s: len(s) > 0})
    value = field(validators={"value must be greater than zero": lambda x: x > 0})

s = Spam()
s.description = "hello"
s.description = ""  # <-- raises error, see below

Nó mang lại

ValidationError[ValueError]: Error validating [<...>.Spam.description=''].
  InvalidValue: description can not be empty. 
  Function [<lambda>] returned [False] for value ''.

Nó tuân thủ python 2 và 3.5 (trái ngược với pydantic) và xác thực xảy ra mỗi khi giá trị được thay đổi (không chỉ lần đầu tiên, trái ngược với attrs). Nó có thể tạo hàm tạo cho bạn, nhưng không làm điều đó theo mặc định như hình trên.

Lưu ý rằng bạn có thể muốn tùy chọn sử dụng mini-lambdathay vì các hàm lambda cũ đơn thuần nếu bạn muốn các thông báo lỗi đơn giản hơn (chúng sẽ hiển thị biểu thức lỗi).

Xem pyfieldstài liệu để biết chi tiết (nhân tiện, tôi là tác giả;))

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.