Cách pythonic để sử dụng getters và setters là gì?


340

Tôi đang làm nó như sau:

def set_property(property,value):  
def get_property(property):  

hoặc là

object.property = value  
value = object.property

Tôi mới sử dụng Python, vì vậy tôi vẫn đang khám phá cú pháp và tôi muốn có một số lời khuyên về việc này.

Câu trả lời:


684

Hãy thử điều này: Tài sản Python

Mã mẫu là:

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """I'm the 'x' property."""
        print("getter of x called")
        return self._x

    @x.setter
    def x(self, value):
        print("setter of x called")
        self._x = value

    @x.deleter
    def x(self):
        print("deleter of x called")
        del self._x


c = C()
c.x = 'foo'  # setter called
foo = c.x    # getter called
del c.x      # deleter called

2
Là setter cho x được gọi trong trình khởi tạo khi khởi tạo _x?
Casey

7
@Casey: Không. Tham chiếu đến ._x(không phải là thuộc tính, chỉ là thuộc tính đơn giản) bỏ qua propertygói. Chỉ tham khảo để .xđi qua property.
ShadowRanger

272

Cách pythonic để sử dụng getters và setters là gì?

Cách "Pythonic" không phải là sử dụng "getters" và "setters", mà là sử dụng các thuộc tính đơn giản, như câu hỏi minh họa và delđể xóa (nhưng các tên được thay đổi để bảo vệ ... nội dung vô tội):

value = 'something'

obj.attribute = value  
value = obj.attribute
del obj.attribute

Nếu sau này, bạn muốn sửa đổi cài đặt và nhận, bạn có thể làm như vậy mà không phải thay đổi mã người dùng, bằng cách sử dụng trình propertytrang trí:

class Obj:
    """property demo"""
    #
    @property            # first decorate the getter method
    def attribute(self): # This getter method name is *the* name
        return self._attribute
    #
    @attribute.setter    # the property decorates with `.setter` now
    def attribute(self, value):   # name, e.g. "attribute", is the same
        self._attribute = value   # the "value" name isn't special
    #
    @attribute.deleter     # decorate with `.deleter`
    def attribute(self):   # again, the method name is the same
        del self._attribute

(Mỗi cách sử dụng trang trí sao chép và cập nhật đối tượng thuộc tính trước đó, vì vậy lưu ý rằng bạn nên sử dụng cùng tên cho mỗi bộ, nhận và xóa chức năng / phương thức.

Sau khi xác định ở trên, cài đặt gốc, nhận và xóa mã là như nhau:

obj = Obj()
obj.attribute = value  
the_value = obj.attribute
del obj.attribute

Bạn nên tránh điều này:

def set_property(property,value):  
def get_property(property):  

Thứ nhất, ở trên không hoạt động, bởi vì bạn không cung cấp một đối số cho trường hợp thuộc tính sẽ được đặt thành (thường self), đó sẽ là:

class Obj:

    def set_property(self, property, value): # don't do this
        ...
    def get_property(self, property):        # don't do this either
        ...

Thứ hai, điều này trùng lặp mục đích của hai phương pháp đặc biệt, __setattr____getattr__.

Thứ ba, chúng tôi cũng có setattrgetattrđược xây dựng trong các chức năng.

setattr(object, 'property_name', value)
getattr(object, 'property_name', default_value)  # default is optional

Các @propertytrang trí là để tạo ra getters và setters.

Ví dụ: chúng tôi có thể sửa đổi hành vi cài đặt để đặt các hạn chế giá trị được đặt:

class Protective(object):

    @property
    def protected_value(self):
        return self._protected_value

    @protected_value.setter
    def protected_value(self, value):
        if acceptable(value): # e.g. type or range check
            self._protected_value = value

Nói chung, chúng tôi muốn tránh sử dụng propertyvà chỉ sử dụng các thuộc tính trực tiếp.

Đây là những gì được mong đợi bởi người dùng Python. Theo quy tắc ít gây ngạc nhiên nhất, bạn nên cố gắng cung cấp cho người dùng những gì họ mong đợi trừ khi bạn có một lý do rất thuyết phục ngược lại.

Trình diễn

Ví dụ: giả sử chúng tôi cần thuộc tính được bảo vệ của đối tượng của chúng tôi là một số nguyên nằm trong khoảng từ 0 đến 100, và ngăn chặn việc xóa nó, với các thông báo phù hợp để thông báo cho người dùng về cách sử dụng đúng:

class Protective(object):
    """protected property demo"""
    #
    def __init__(self, start_protected_value=0):
        self.protected_value = start_protected_value
    # 
    @property
    def protected_value(self):
        return self._protected_value
    #
    @protected_value.setter
    def protected_value(self, value):
        if value != int(value):
            raise TypeError("protected_value must be an integer")
        if 0 <= value <= 100:
            self._protected_value = int(value)
        else:
            raise ValueError("protected_value must be " +
                             "between 0 and 100 inclusive")
    #
    @protected_value.deleter
    def protected_value(self):
        raise AttributeError("do not delete, protected_value can be set to 0")

(Lưu ý __init__đề cập đến self.protected_valuenhưng các phương thức thuộc tính đề cập đến self._protected_value. Điều này là để __init__sử dụng thuộc tính thông qua API công khai, đảm bảo nó được "bảo vệ".)

Và cách sử dụng:

>>> p1 = Protective(3)
>>> p1.protected_value
3
>>> p1 = Protective(5.0)
>>> p1.protected_value
5
>>> p2 = Protective(-5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> p1.protected_value = 7.3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 17, in protected_value
TypeError: protected_value must be an integer
>>> p1.protected_value = 101
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> del p1.protected_value
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 18, in protected_value
AttributeError: do not delete, protected_value can be set to 0

Làm tên quan trọng?

Có họ làm . .setter.deletertạo bản sao của tài sản ban đầu. Điều này cho phép các lớp con sửa đổi hành vi đúng cách mà không thay đổi hành vi trong cha mẹ.

class Obj:
    """property demo"""
    #
    @property
    def get_only(self):
        return self._attribute
    #
    @get_only.setter
    def get_or_set(self, value):
        self._attribute = value
    #
    @get_or_set.deleter
    def get_set_or_delete(self):
        del self._attribute

Bây giờ để làm việc này, bạn phải sử dụng các tên tương ứng:

obj = Obj()
# obj.get_only = 'value' # would error
obj.get_or_set = 'value'  
obj.get_set_or_delete = 'new value'
the_value = obj.get_only
del obj.get_set_or_delete
# del obj.get_or_set # would error

Tôi không chắc nơi này sẽ hữu ích, nhưng trường hợp sử dụng là nếu bạn muốn một thuộc tính get, set và / hoặc chỉ xóa. Có lẽ tốt nhất để gắn bó với cùng một tài sản có cùng tên.

Phần kết luận

Bắt đầu với các thuộc tính đơn giản.

Nếu sau này bạn cần chức năng xung quanh cài đặt, nhận và xóa, bạn có thể thêm nó với trình trang trí thuộc tính.

Tránh các chức năng được đặt tên set_...get_...- đó là những gì thuộc tính dành cho.


Bên cạnh việc sao chép chức năng có sẵn, tại sao nên viết setters và getters của riêng bạn? Tôi hiểu nó có thể không phải là cách của Pythonic, nhưng có vấn đề nào thực sự nghiêm trọng mà người ta có thể gặp phải không?
dùng1350191

4
Trong bản demo của bạn, __init__phương thức đề cập đến self.protected_valuenhưng getter và setters đề cập đến self._protected_value. Bạn có thể vui lòng giải thích làm thế nào điều này hoạt động? Tôi đã kiểm tra mã của bạn và nó hoạt động như hiện tại - vì vậy đây không phải là một lỗi đánh máy.
codeforester

2
@codeforester Tôi đã hy vọng trả lời trong câu trả lời của tôi trước đó, nhưng cho đến khi tôi có thể, nhận xét này sẽ đủ. Tôi hy vọng bạn có thể thấy rằng nó sử dụng tài sản thông qua api công cộng, đảm bảo nó được "bảo vệ". Sẽ không có ý nghĩa gì khi "bảo vệ" nó bằng một tài sản và sau đó sử dụng api không công khai thay vào __init__đó?
Aaron Hall

2
Vâng, @AaronHall đã nhận nó ngay bây giờ. Tôi đã không nhận ra self.protected_value = start_protected_valuelà thực sự gọi hàm setter; Tôi nghĩ rằng đó là một nhiệm vụ.
codeforester

1
Imho đây sẽ là câu trả lời được chấp nhận, nếu tôi hiểu chính xác python chỉ lấy điểm ngược lại so với ví dụ java. Thay vì đặt mọi thứ riêng tư theo mặc định và viết thêm một số mã khi cần công khai trong python, bạn có thể đặt mọi thứ ở chế độ công khai và thêm quyền riêng tư sau này
idclev 463035818

27
In [1]: class test(object):
    def __init__(self):
        self.pants = 'pants'
    @property
    def p(self):
        return self.pants
    @p.setter
    def p(self, value):
        self.pants = value * 2
   ....: 
In [2]: t = test()
In [3]: t.p
Out[3]: 'pants'
In [4]: t.p = 10
In [5]: t.p
Out[5]: 20

17

Sử dụng @property@attribute.settergiúp bạn không chỉ sử dụng cách "pythonic" mà còn để kiểm tra tính hợp lệ của các thuộc tính cả trong khi tạo đối tượng và khi thay đổi nó.

class Person(object):
    def __init__(self, p_name=None):
        self.name = p_name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, new_name):
        if type(new_name) == str: #type checking for name property
            self._name = new_name
        else:
            raise Exception("Invalid value for name")

Bằng cách này, bạn thực sự 'ẩn' _namethuộc tính khỏi các nhà phát triển ứng dụng khách và cũng thực hiện kiểm tra loại thuộc tính tên. Lưu ý rằng bằng cách làm theo cách tiếp cận này ngay cả trong khi bắt đầu, setter được gọi. Vì thế:

p = Person(12)

Sẽ dẫn đến:

Exception: Invalid value for name

Nhưng:

>>>p = person('Mike')
>>>print(p.name)
Mike
>>>p.name = 'George'
>>>print(p.name)
George
>>>p.name = 2.3 # Causes an exception

16

Kiểm tra @propertytrang trí .


33
Đây là khá nhiều câu trả lời chỉ liên kết.
Aaron Hall


7
Làm thế nào đây là một câu trả lời đầy đủ? Một liên kết không phải là một câu trả lời.
Andy_A̷n̷d̷y̷

Tôi nghĩ đó là một câu trả lời tốt, bởi vì tài liệu ở đó nêu rõ cách sử dụng nó (và sẽ duy trì hiện hành nếu việc triển khai trăn thay đổi, và nó hướng OP đến phương thức mà câu trả lời đang đề xuất. @ Jean-FrançoisCorbett đã nêu rõ 'làm thế nào' đó là một câu trả lời hoàn chỉnh.
HunnyBear

Trong mọi trường hợp, câu trả lời này không thêm bất cứ điều gì hữu ích vào các câu trả lời khác và lý tưởng nhất là nên bị xóa.
Georgy

5

Bạn có thể sử dụng bộ truy cập / bộ biến đổi (tức là @attr.setter@property) hoặc không, nhưng điều quan trọng nhất là phải nhất quán!

Nếu bạn đang sử dụng @propertyđể truy cập một thuộc tính, vd

class myClass:
    def __init__(a):
        self._a = a

    @property
    def a(self):
        return self._a

sử dụng nó để truy cập mọi thuộc tính * ! Sẽ là một thực tế tồi khi truy cập một số thuộc tính bằng cách sử dụng @propertyvà để một số thuộc tính khác ở chế độ công khai (tức là tên không có dấu gạch dưới) mà không có người truy cập, ví dụ: không làm

class myClass:
    def __init__(a, b):
        self.a = a
        self.b = b

    @property
    def a(self):
        return self.a

Lưu ý rằng self.bkhông có trình truy cập rõ ràng ở đây mặc dù nó công khai.

Tương tự với setters (hoặc mutators ), hãy sử dụng @attribute.setternhưng phải nhất quán! Khi bạn làm ví dụ

class myClass:
    def __init__(a, b):
        self.a = a
        self.b = b 

    @a.setter
    def a(self, value):
        return self.a = value

Thật khó cho tôi để đoán ý định của bạn. Một mặt bạn đang nói rằng cả hai abđều công khai (không có dấu gạch dưới hàng đầu trong tên của họ) vì vậy về mặt lý thuyết tôi nên được phép truy cập / biến đổi (get / set) cả hai. Nhưng sau đó, bạn chỉ định một trình biến đổi rõ ràng chỉ cho a, điều đó cho tôi biết rằng có lẽ tôi không thể thiết lập được b. Vì bạn đã cung cấp một trình biến đổi rõ ràng, tôi không chắc liệu việc thiếu trình @propertytruy cập rõ ràng ( ) có nghĩa là tôi không thể truy cập vào một trong các biến đó hay bạn chỉ đơn giản là sử dụng @property.

* Ngoại lệ là khi bạn rõ ràng muốn làm cho một số biến có thể truy cập hoặc có thể thay đổi nhưng không phải cả hai hoặc bạn muốn thực hiện một số logic bổ sung khi truy cập hoặc biến đổi một thuộc tính. Đây là khi cá nhân tôi đang sử dụng @property@attribute.setter(nếu không thì không có người nhận / người gây đột biến rõ ràng cho các thuộc tính công khai).

Cuối cùng, các đề xuất PEP8 và Google Style Guide:

PEP8, Thiết kế để kế thừa cho biết:

Đối với các thuộc tính dữ liệu công khai đơn giản, tốt nhất là chỉ hiển thị tên thuộc tính, không có các phương thức truy cập / trình biến đổi phức tạp . Hãy nhớ rằng Python cung cấp một đường dẫn dễ dàng để tăng cường trong tương lai, nếu bạn thấy rằng một thuộc tính dữ liệu đơn giản cần phát triển hành vi chức năng. Trong trường hợp đó, sử dụng các thuộc tính để ẩn việc thực hiện chức năng đằng sau cú pháp truy cập thuộc tính dữ liệu đơn giản.

Mặt khác, theo Hướng dẫn về Phong cách / Thuộc tính của Ngôn ngữ Python , khuyến nghị là:

Sử dụng các thuộc tính trong mã mới để truy cập hoặc đặt dữ liệu trong đó bạn thường sử dụng các phương thức truy cập hoặc setter đơn giản, nhẹ. Các thuộc tính nên được tạo ra với các @propertytrang trí.

Ưu điểm của phương pháp này:

Khả năng đọc được tăng lên bằng cách loại bỏ các lệnh gọi get và set rõ ràng để truy cập thuộc tính đơn giản. Cho phép tính toán được lười biếng. Được coi là cách Pythonic để duy trì giao diện của một lớp. Về mặt hiệu suất, cho phép các thuộc tính bỏ qua các phương thức truy cập tầm thường khi truy cập biến trực tiếp là hợp lý. Điều này cũng cho phép các phương thức truy cập được thêm vào trong tương lai mà không phá vỡ giao diện.

và khuyết điểm:

Phải kế thừa từ objecttrong Python 2. Có thể ẩn các hiệu ứng phụ giống như quá tải toán tử. Có thể gây nhầm lẫn cho các lớp con.


1
Tôi rất không đồng ý. Nếu tôi có 15 thuộc tính trên đối tượng của mình và tôi muốn một thuộc tính được tính toán @property, việc sử dụng phần còn lại cũng @propertycó vẻ như là một quyết định tồi.
Quelklef

Đồng ý nhưng chỉ khi có một cái gì đó cụ thể về thuộc tính cụ thể này mà bạn cần @property(ví dụ: thực hiện một số logic đặc biệt trước khi trả về một thuộc tính). Nếu không, tại sao bạn sẽ trang trí một thuộc tính với @properyvà không phải thuộc tính khác?
Tomasz Bartkowiak

@Quelklef xem sidenote trong bài (được đánh dấu bằng dấu hoa thị).
Tomasz Bartkowiak

Chà ... Nếu bạn không làm một trong những điều được đề cập bởi sidenote, thì bạn không nên sử dụng @propertyđể bắt đầu, phải không? Nếu getter của bạn là return this._xvà setter của bạn this._x = new_xthì việc sử dụng hoàn @propertytoàn là ngớ ngẩn.
Quelklef

1
Hmm, có lẽ. Cá nhân tôi sẽ nói nó không ổn --- nó hoàn toàn không cần thiết. Nhưng tôi có thể thấy bạn đến từ đâu. Tôi đoán tôi vừa đọc bài viết của bạn vừa nói rằng "điều quan trọng nhất khi sử dụng @propertylà sự nhất quán".
Quelklef

-1

Bạn có thể sử dụng các phương pháp kỳ diệu __getattribute____setattr__.

class MyClass:
    def __init__(self, attrvalue):
        self.myattr = attrvalue
    def __getattribute__(self, attr):
        if attr == "myattr":
            #Getter for myattr
    def __setattr__(self, attr):
        if attr == "myattr":
            #Setter for myattr

Hãy nhận ra điều đó __getattr____getattribute__không giống nhau. __getattr__chỉ được gọi khi không tìm thấy thuộc tính.

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.