Sử dụng @property so với getters và setters


727

Đây là một câu hỏi thiết kế dành riêng cho Python:

class MyClass(object):
    ...
    def get_my_attr(self):
        ...

    def set_my_attr(self, value):
        ...

class MyClass(object):
    ...        
    @property
    def my_attr(self):
        ...

    @my_attr.setter
    def my_attr(self, value):
        ...

Python cho phép chúng ta làm một trong hai cách. Nếu bạn thiết kế chương trình Python, bạn sẽ sử dụng phương pháp nào và tại sao?

Câu trả lời:


613

Ưu tiên tính chất . Đó là những gì họ đang ở đó.

Lý do là tất cả các thuộc tính được công khai trong Python. Tên bắt đầu bằng một hoặc hai dấu gạch dưới chỉ là một cảnh báo rằng thuộc tính đã cho là một chi tiết triển khai có thể không giống nhau trong các phiên bản mã trong tương lai. Nó không ngăn bạn thực sự có được hoặc thiết lập thuộc tính đó. Do đó, truy cập thuộc tính tiêu chuẩn là cách thông thường, theo kiểu Pythonic, tốt, truy cập các thuộc tính.

Ưu điểm của các thuộc tính là chúng giống hệt nhau về mặt truy cập thuộc tính, vì vậy bạn có thể thay đổi từ cái này sang cái khác mà không có bất kỳ thay đổi nào đối với mã máy khách. Bạn thậm chí có thể có một phiên bản của một lớp sử dụng các thuộc tính (giả sử, theo mã theo hợp đồng hoặc gỡ lỗi) và một phiên bản không dành cho sản xuất, mà không thay đổi mã sử dụng nó. Đồng thời, bạn không phải viết getters và setters cho mọi thứ chỉ trong trường hợp bạn có thể cần kiểm soát truy cập tốt hơn sau này.


90
Tên thuộc tính có dấu gạch dưới kép được xử lý đặc biệt bởi Python; nó không chỉ là một quy ước đơn thuần. Xem docs.python.org/py3k/tutorial/groupes.html#private-variables
6502

63
Chúng được xử lý khác nhau, nhưng điều đó không ngăn bạn truy cập chúng. Tái
bút

4
và bởi vì các ký tự "@" xấu trong mã python và các trình phân tích @decorators mang lại cảm giác giống như mã spaghetti.
Berry Tsakala

18
Tôi không đồng ý. Làm thế nào là cấu trúc mã bằng với mã spaghetti? Python là một ngôn ngữ đẹp. Nhưng sẽ tốt hơn nữa với sự hỗ trợ tốt hơn cho những thứ đơn giản như đóng gói thích hợp và các lớp có cấu trúc.

69
Mặc dù tôi đồng ý trong hầu hết các trường hợp, hãy cẩn thận về việc ẩn các phương thức chậm đằng sau trình trang trí @property. Người dùng API của bạn hy vọng rằng quyền truy cập thuộc tính thực hiện như quyền truy cập thay đổi và việc đi quá xa so với mong đợi đó có thể khiến API của bạn khó sử dụng.
defrex

153

Trong Python, bạn không sử dụng getters hoặc setters hoặc thuộc tính chỉ để giải trí. Trước tiên, bạn chỉ cần sử dụng các thuộc tính và sau đó, chỉ khi cần, cuối cùng di chuyển đến một thuộc tính mà không phải thay đổi mã bằng các lớp của bạn.

Thực sự có rất nhiều mã với phần mở rộng .py sử dụng getters và setters và thừa kế và các lớp vô nghĩa ở mọi nơi, ví dụ như một tuple đơn giản sẽ làm, nhưng đó là mã từ những người viết bằng C ++ hoặc Java bằng Python.

Đó không phải là mã Python.


46
@ 6502, khi bạn nói các lớp vô nghĩa của [ở] ở khắp mọi nơi, ví dụ như một tuple đơn giản sẽ làm được . Các tên tốt hơn về khả năng đọc và tránh các lỗi, hơn là bộ dữ liệu đăng ký, đặc biệt là khi điều này được chuyển ra bên ngoài mô-đun hiện tại.
Hibou57

15
@ Hibou57: Tôi không nói rằng lớp học là vô dụng. Nhưng đôi khi một tuple là quá đủ. Tuy nhiên, vấn đề là những người đến từ Java hay C ++ không có lựa chọn nào khác ngoài việc tạo các lớp cho mọi thứ vì các khả năng khác chỉ gây khó chịu khi sử dụng trong các làn đường đó. Một triệu chứng điển hình khác của lập trình Java / C ++ khi sử dụng Python là tạo các lớp trừu tượng và phân cấp lớp phức tạp mà không có lý do gì trong Python bạn chỉ có thể sử dụng các lớp độc lập nhờ gõ vịt.
6502

39
@ Hibou57 vì điều đó bạn cũng có thể sử dụng têntuple
hugo24

5
@JonathonReinhart: nó trong thư viện chuẩn từ 2,6 ... see docs.python.org/2/library/collections.html
6502

1
Khi "cuối cùng di chuyển đến một tài sản nếu cần", rất có thể bạn sẽ phá mã bằng các lớp của mình. Các thuộc tính thường đưa ra các hạn chế - bất kỳ mã nào không mong đợi các hạn chế này sẽ bị hỏng ngay khi bạn giới thiệu chúng.
yaccob

118

Sử dụng các thuộc tính cho phép bạn bắt đầu với các truy cập thuộc tính thông thường và sau đó sao lưu chúng bằng getters và setters sau đó khi cần thiết .


3
@GregKrsak Có vẻ lạ vì nó là. "Điều người lớn đồng ý" là một meme trăn từ trước khi tài sản được thêm vào. Đó là phản ứng chứng khoán cho những người phàn nàn về việc thiếu công cụ sửa đổi truy cập. Khi các thuộc tính được thêm vào, đột nhiên đóng gói trở nên mong muốn. Điều tương tự cũng xảy ra với các lớp cơ sở trừu tượng. "Python luôn có chiến tranh với sự phá vỡ đóng gói. Tự do là nô lệ. Lambdas chỉ nên nằm trên một dòng."
johncip

71

Câu trả lời ngắn gọn là: tài sản thắng tay . Luôn luôn.

Đôi khi cần có getters và setters, nhưng ngay cả khi đó, tôi sẽ "giấu" chúng ra thế giới bên ngoài. Có rất nhiều cách để làm điều này trong Python ( getattr, setattr, __getattribute__, vv ..., nhưng là một trong rất ngắn gọn và sạch sẽ:

def set_email(self, value):
    if '@' not in value:
        raise Exception("This doesn't look like an email address.")
    self._email = value

def get_email(self):
    return self._email

email = property(get_email, set_email)

Đây là một bài viết ngắn giới thiệu chủ đề của getters và setters trong Python.


1
@BasicWolf - Tôi nghĩ rằng nó hoàn toàn rõ ràng tôi đang ở phía tài sản của hàng rào! :) Nhưng tôi thêm một đoạn vào câu trả lời của tôi để làm rõ điều đó.
mac

9
GỢI Ý: Từ "luôn luôn" là một gợi ý rằng tác giả đang cố gắng thuyết phục bạn bằng một khẳng định, không phải là một lập luận. Vì vậy, sự hiện diện của phông chữ đậm. (Ý tôi là, nếu bạn thấy CAPS thay vào đó, thì - whoa - nó phải đúng.) Hãy nhìn xem, tính năng "property" xảy ra khác với Java (vì lý do nào đó của Python vì lý do nào đó), và do đó, nhóm cộng đồng của Python tuyên bố nó sẽ tốt hơn Trong thực tế, các thuộc tính vi phạm quy tắc "Rõ ràng là tốt hơn so với ngầm định", nhưng không ai muốn thừa nhận nó. Nó biến nó thành ngôn ngữ, vì vậy bây giờ nó được tuyên bố là "Pythonic" thông qua một lập luận tautological.
Stuart Berg

3
Không có cảm giác tổn thương. :-P Tôi chỉ đang cố gắng chỉ ra rằng các quy ước "Pythonic" không nhất quán trong trường hợp này: "Rõ ràng là tốt hơn ngầm" là xung đột trực tiếp với việc sử dụng a property. ( Trông giống như một nhiệm vụ đơn giản, nhưng nó gọi là một hàm.) Do đó, "Pythonic" về cơ bản là một thuật ngữ vô nghĩa, ngoại trừ theo định nghĩa tautological: "Các quy ước Pythonic là những thứ mà chúng ta đã định nghĩa là Pythonic."
Stuart Berg

1
Bây giờ, ý tưởng có một bộ các quy ước theo một chủ đề là tuyệt vời . Nếu một tập hợp các quy ước như vậy đã tồn tại, thì bạn có thể sử dụng nó như một tập hợp tiên đề để hướng dẫn suy nghĩ của mình, không chỉ là một danh sách kiểm tra dài các thủ thuật để ghi nhớ, mà ít hữu ích hơn. Tiên đề có thể được sử dụng để ngoại suy , và giúp bạn tiếp cận các vấn đề chưa ai nhìn thấy. Thật xấu hổ khi propertytính năng này đe dọa đưa ra ý tưởng về tiên đề Pythonic gần như vô giá trị. Vì vậy, tất cả những gì chúng tôi còn lại là một danh sách kiểm tra.
Stuart Berg

1
Tôi không đồng ý. Tôi thích các thuộc tính trong hầu hết các tình huống, nhưng khi bạn muốn nhấn mạnh rằng việc thiết lập một cái gì đó có tác dụng phụ ngoài việc sửa đổi selfđối tượng , setters rõ ràng có thể hữu ích. Chẳng hạn, user.email = "..."có vẻ như nó không thể tạo ra một ngoại lệ vì có vẻ như chỉ cần đặt một thuộc tính, trong khi đó user.set_email("...")làm rõ rằng có thể có các tác dụng phụ như ngoại lệ.
bluenote10

65

[ TL; DR? Bạn có thể bỏ qua đến cuối cho một ví dụ mã .]

Tôi thực sự thích sử dụng một thành ngữ khác, một chút liên quan đến việc sử dụng như một từ tắt, nhưng thật tuyệt nếu bạn có trường hợp sử dụng phức tạp hơn.

Một chút nền tảng đầu tiên.

Các thuộc tính hữu ích ở chỗ chúng cho phép chúng ta xử lý cả cài đặt và nhận giá trị theo cách lập trình nhưng vẫn cho phép các thuộc tính được truy cập dưới dạng thuộc tính. Chúng ta có thể biến 'được' thành 'tính toán' (về cơ bản) và chúng ta có thể biến 'bộ' thành 'sự kiện'. Vì vậy, giả sử chúng ta có lớp sau, mà tôi đã mã hóa bằng các getters và setters giống như Java.

class Example(object):
    def __init__(self, x=None, y=None):
        self.x = x
        self.y = y

    def getX(self):
        return self.x or self.defaultX()

    def getY(self):
        return self.y or self.defaultY()

    def setX(self, x):
        self.x = x

    def setY(self, y):
        self.y = y

    def defaultX(self):
        return someDefaultComputationForX()

    def defaultY(self):
        return someDefaultComputationForY()

Bạn có thể tự hỏi tại sao tôi không gọi defaultXdefaultYtrong __init__phương thức của đối tượng . Lý do là vì trường hợp của chúng tôi, tôi muốn giả sử rằng các someDefaultComputationphương thức trả về các giá trị thay đổi theo thời gian, giả sử dấu thời gian và bất cứ khi nào x(hoặc y) không được đặt (trong đó, với mục đích của ví dụ này, "không đặt" có nghĩa là "đặt" thành Không ") Tôi muốn giá trị của tính toán mặc định xcủa (hoặc y).

Vì vậy, điều này là khập khiễng vì một số lý do mô tả ở trên. Tôi sẽ viết lại bằng các thuộc tính:

class Example(object):
    def __init__(self, x=None, y=None):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self.x or self.defaultX()

    @x.setter
    def x(self, value):
        self._x = value

    @property
    def y(self):
        return self.y or self.defaultY()

    @y.setter
    def y(self, value):
        self._y = value

    # default{XY} as before.

Chúng ta đã đạt được gì? Chúng tôi đã đạt được khả năng coi các thuộc tính này là các thuộc tính mặc dù, đằng sau hậu trường, chúng tôi kết thúc các phương thức chạy.

Tất nhiên, sức mạnh thực sự của các thuộc tính là chúng ta thường muốn các phương thức này thực hiện một việc gì đó ngoài việc chỉ nhận và thiết lập các giá trị (nếu không thì không có điểm nào trong việc sử dụng các thuộc tính). Tôi đã làm điều này trong ví dụ getter của tôi. Về cơ bản, chúng tôi đang chạy một thân hàm để lấy mặc định bất cứ khi nào giá trị không được đặt. Đây là một mô hình rất phổ biến.

Nhưng chúng ta đang mất gì và chúng ta không thể làm gì?

Khó chịu chính, theo quan điểm của tôi, là nếu bạn xác định một getter (như chúng tôi làm ở đây), bạn cũng phải xác định một setter. [1] Đó là tiếng ồn thêm làm tắc mã.

Một phiền toái khác là chúng ta vẫn phải khởi tạo xycác giá trị trong __init__. (Chà, tất nhiên chúng ta có thể thêm chúng bằng cách sử dụng setattr()nhưng đó là nhiều mã hơn.)

Thứ ba, không giống như trong ví dụ giống như Java, getters không thể chấp nhận các tham số khác. Bây giờ tôi có thể nghe bạn nói rồi, nếu nó lấy tham số thì nó không phải là một getter! Trong một ý nghĩa chính thức, đó là sự thật. Nhưng trong một ý nghĩa thực tế, không có lý do gì chúng ta không thể tham số hóa một thuộc tính được đặt tên - như x- và đặt giá trị của nó cho một số tham số cụ thể.

Thật tuyệt nếu chúng ta có thể làm một cái gì đó như:

e.x[a,b,c] = 10
e.x[d,e,f] = 20

ví dụ. Cách gần nhất chúng ta có thể nhận được là ghi đè lên bài tập để ngụ ý một số ngữ nghĩa đặc biệt:

e.x = [a,b,c,10]
e.x = [d,e,f,30]

và tất nhiên đảm bảo rằng trình thiết lập của chúng tôi biết cách trích xuất ba giá trị đầu tiên làm khóa cho từ điển và đặt giá trị của nó thành một số hoặc một cái gì đó.

Nhưng ngay cả khi chúng tôi đã làm điều đó, chúng tôi vẫn không thể hỗ trợ nó với các thuộc tính vì không có cách nào để nhận được giá trị vì chúng tôi không thể truyền tham số nào cho getter. Vì vậy, chúng tôi đã phải trả lại mọi thứ, giới thiệu một sự bất cân xứng.

Trình getter / setter kiểu Java không cho phép chúng ta xử lý việc này, nhưng chúng ta quay lại cần getter / setters.

Trong tâm trí của tôi, những gì chúng tôi thực sự muốn là một cái gì đó nắm bắt các yêu cầu sau đây:

  • Người dùng chỉ xác định một phương thức cho một thuộc tính nhất định và có thể chỉ ra rằng thuộc tính đó là chỉ đọc hay đọc-ghi. Thuộc tính thất bại kiểm tra này nếu thuộc tính có thể ghi.

  • Người dùng không cần xác định một biến phụ bên dưới hàm, vì vậy chúng ta không cần __init__hoặc setattrtrong mã. Biến chỉ tồn tại bởi thực tế chúng ta đã tạo thuộc tính kiểu mới này.

  • Bất kỳ mã mặc định nào cho thuộc tính thực thi trong chính thân phương thức.

  • Chúng ta có thể đặt thuộc tính làm thuộc tính và tham chiếu nó như một thuộc tính.

  • Chúng ta có thể tham số hóa thuộc tính.

Về mặt mã, chúng tôi muốn có một cách để viết:

def x(self, *args):
    return defaultX()

và có thể sau đó làm:

print e.x     -> The default at time T0
e.x = 1
print e.x     -> 1
e.x = None
print e.x     -> The default at time T1

và kể từ đó trở đi.

Chúng tôi cũng muốn có một cách để làm điều này cho trường hợp đặc biệt của thuộc tính tham số hóa, nhưng vẫn cho phép trường hợp gán mặc định hoạt động. Bạn sẽ thấy cách tôi giải quyết điều này dưới đây.

Bây giờ đến điểm (yay! Điểm!). Giải pháp tôi đưa ra cho việc này là như sau.

Chúng tôi tạo ra một đối tượng mới để thay thế khái niệm về một tài sản. Đối tượng dự định lưu trữ giá trị của một biến được đặt cho nó, nhưng cũng duy trì một điều khiển trên mã biết cách tính toán mặc định. Công việc của nó là lưu trữ tập hợp valuehoặc chạy methodnếu giá trị đó không được đặt.

Hãy gọi nó là một UberProperty.

class UberProperty(object):

    def __init__(self, method):
        self.method = method
        self.value = None
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def clearValue(self):
        self.value = None
        self.isSet = False

Tôi giả sử methodở đây là một phương thức lớp, valuelà giá trị của UberPropertyvà tôi đã thêm vào isSetbởi vì Nonecó thể là một giá trị thực và điều này cho phép chúng ta một cách rõ ràng để tuyên bố thực sự là "không có giá trị". Một cách khác là một trọng điểm của một số loại.

Điều này về cơ bản mang đến cho chúng ta một đối tượng có thể làm những gì chúng ta muốn, nhưng làm thế nào để chúng ta thực sự đưa nó vào lớp học? Vâng, tài sản sử dụng trang trí; Tại sao chúng ta không thể? Chúng ta hãy xem nó trông như thế nào (từ đây trở đi tôi sẽ chỉ sử dụng một 'thuộc tính' duy nhất, x).

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

Điều này thực sự không hoạt động, tất nhiên. Chúng tôi phải thực hiện uberPropertyvà đảm bảo rằng nó xử lý cả get và set.

Hãy bắt đầu với.

Nỗ lực đầu tiên của tôi chỉ đơn giản là tạo một đối tượng UberProperty mới và trả lại nó:

def uberProperty(f):
    return UberProperty(f)

Tất nhiên, tôi nhanh chóng phát hiện ra rằng điều này không hoạt động: Python không bao giờ liên kết có thể gọi được với đối tượng và tôi cần đối tượng để gọi hàm. Ngay cả việc tạo trình trang trí trong lớp cũng không hoạt động, vì mặc dù bây giờ chúng ta có lớp, chúng ta vẫn không có đối tượng để làm việc.

Vì vậy, chúng ta sẽ cần phải có thể làm nhiều hơn ở đây. Chúng tôi biết rằng một phương thức chỉ cần được trình bày một lần, vì vậy hãy tiếp tục và giữ trang trí của chúng tôi, nhưng sửa đổi UberPropertyđể chỉ lưu trữ methodtham chiếu:

class UberProperty(object):

    def __init__(self, method):
        self.method = method

Nó cũng không thể gọi được, vì vậy tại thời điểm này không có gì là làm việc.

Làm thế nào để chúng ta hoàn thành bức tranh? Chà, cuối cùng chúng ta sẽ làm gì khi tạo lớp mẫu bằng cách sử dụng trang trí mới của chúng tôi:

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

print Example.x     <__main__.UberProperty object at 0x10e1fb8d0>
print Example().x   <__main__.UberProperty object at 0x10e1fb8d0>

trong cả hai trường hợp, chúng tôi nhận lại được UberPropertyđiều mà tất nhiên không thể gọi được, vì vậy điều này không được sử dụng nhiều.

Những gì chúng ta cần là một số cách để tự động liên kết UberPropertycá thể được tạo bởi trình trang trí sau khi lớp đã được tạo cho một đối tượng của lớp trước khi đối tượng đó được trả về cho người dùng đó để sử dụng. Ừm, ừ, đó là một __init__cuộc gọi, anh bạn.

Hãy viết những gì chúng tôi muốn kết quả tìm thấy của chúng tôi là đầu tiên. Chúng tôi đang ràng buộc một UberPropertytrường hợp, vì vậy một điều rõ ràng để trở lại sẽ là BoundUberProperty. Đây là nơi chúng ta sẽ thực sự duy trì trạng thái cho xthuộc tính.

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

Bây giờ chúng tôi đại diện; Làm thế nào để có được những điều này trên một đối tượng? Có một vài cách tiếp cận, nhưng cách dễ nhất để giải thích chỉ sử dụng __init__phương pháp để thực hiện ánh xạ đó. Theo thời gian __init__được gọi là trang trí của chúng tôi đã chạy, vì vậy chỉ cần xem qua các đối tượng __dict__và cập nhật bất kỳ thuộc tính nào trong đó giá trị của thuộc tính là loại UberProperty.

Bây giờ, các thuộc tính uber rất tuyệt và có lẽ chúng ta sẽ muốn sử dụng chúng rất nhiều, vì vậy sẽ rất hợp lý khi tạo một lớp cơ sở thực hiện điều này cho tất cả các lớp con. Tôi nghĩ bạn biết lớp cơ sở sẽ được gọi là gì.

class UberObject(object):
    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)

Chúng tôi thêm điều này, thay đổi ví dụ của chúng tôi để kế thừa từ UberObjectvà ...

e = Example()
print e.x               -> <__main__.BoundUberProperty object at 0x104604c90>

Sau khi sửa đổi xthành:

@uberProperty
def x(self):
    return *datetime.datetime.now()*

Chúng tôi có thể chạy thử nghiệm đơn giản:

print e.x.getValue()
print e.x.getValue()
e.x.setValue(datetime.date(2013, 5, 31))
print e.x.getValue()
e.x.clearValue()
print e.x.getValue()

Và chúng tôi nhận được đầu ra mà chúng tôi muốn:

2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310

(Gee, tôi đang làm việc muộn.)

Lưu ý rằng tôi đã sử dụng getValue, setValueclearValueở đây. Điều này là do tôi chưa được liên kết trong các phương tiện để có những thứ này tự động được trả lại.

Nhưng tôi nghĩ rằng đây là một nơi tốt để dừng lại vì tôi đang mệt mỏi. Bạn cũng có thể thấy rằng chức năng cốt lõi mà chúng tôi muốn đã có; phần còn lại là thay đồ cửa sổ. Thay đổi cửa sổ khả năng sử dụng quan trọng, nhưng điều đó có thể đợi cho đến khi tôi có thay đổi để cập nhật bài viết.

Tôi sẽ hoàn thành ví dụ trong bài đăng tiếp theo bằng cách giải quyết những điều sau:

  • Chúng tôi cần đảm bảo rằng UberObject __init__luôn được các lớp con gọi.

    • Vì vậy, chúng tôi hoặc buộc nó được gọi ở đâu đó hoặc chúng tôi ngăn chặn nó được thực hiện.
    • Chúng ta sẽ xem làm thế nào để làm điều này với một siêu dữ liệu.
  • Chúng ta cần đảm bảo rằng chúng ta xử lý trường hợp phổ biến trong đó ai đó 'bí danh' một chức năng cho một thứ khác, chẳng hạn như:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
  • Chúng tôi cần e.xphải trả lại e.x.getValue()theo mặc định.

    • Những gì chúng ta thực sự sẽ thấy là đây là một lĩnh vực mà mô hình thất bại.
    • Hóa ra chúng ta sẽ luôn cần sử dụng lệnh gọi hàm để nhận giá trị.
    • Nhưng chúng ta có thể làm cho nó trông giống như một cuộc gọi chức năng thông thường và tránh phải sử dụng e.x.getValue(). (Làm điều này là rõ ràng, nếu bạn chưa sửa nó.)
  • Chúng tôi cần hỗ trợ cài đặt e.x directly, như trong e.x = <newvalue>. Chúng tôi cũng có thể làm điều này trong lớp cha, nhưng chúng tôi sẽ cần cập nhật __init__mã của chúng tôi để xử lý nó.

  • Cuối cùng, chúng ta sẽ thêm các thuộc tính được tham số hóa. Nó cũng khá rõ ràng về cách chúng ta cũng sẽ làm điều này.

Đây là mã như nó tồn tại cho đến nay:

import datetime

class UberObject(object):
    def uberSetter(self, value):
        print 'setting'

    def uberGetter(self):
        return self

    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)


class UberProperty(object):
    def __init__(self, method):
        self.method = method

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

    def uberProperty(f):
        return UberProperty(f)

class Example(UberObject):

    @uberProperty
    def x(self):
        return datetime.datetime.now()

[1] Tôi có thể đứng sau về việc liệu đây có còn là vấn đề không.


53
Vâng, đây là 'tldr'. Bạn có thể vui lòng tóm tắt những gì bạn đang cố gắng làm ở đây?
poolie

9
@Adam return self.x or self.defaultX()đây là mã nguy hiểm. Điều gì xảy ra khi self.x == 0nào?
Kelly Thomas

FYI, bạn có thể làm cho nó để bạn có thể tham số hóa getter, loại. Nó sẽ liên quan đến việc biến biến thành một lớp tùy chỉnh, trong đó bạn đã ghi đè __getitem__phương thức. Mặc dù điều này thật kỳ lạ, vì khi đó bạn có một con trăn hoàn toàn không chuẩn.
sẽ

2
@KellyThomas Chỉ cần cố gắng giữ cho ví dụ đơn giản. Để làm điều đó đúng, bạn cần phải tạo và xóa hoàn toàn mục nhập x dict , bởi vì ngay cả một giá trị Không có thể đã được đặt cụ thể. Nhưng vâng, bạn hoàn toàn đúng, đây là điều bạn cần xem xét trong trường hợp sử dụng sản xuất.
Adam Donahue

Các getters giống như Java cho phép bạn thực hiện chính xác cùng một tính toán, phải không?
qed

26

Tôi nghĩ cả hai đều có vị trí của họ. Một vấn đề với việc sử dụng @propertylà khó có thể mở rộng hành vi của getters hoặc setters trong các lớp con bằng cách sử dụng các cơ chế lớp tiêu chuẩn. Vấn đề là các hàm getter / setter thực tế bị ẩn trong thuộc tính.

Bạn thực sự có thể nắm giữ các chức năng, ví dụ như với

class C(object):
    _p = 1
    @property
    def p(self):
        return self._p
    @p.setter
    def p(self, val):
        self._p = val

bạn có thể truy cập vào các chức năng getter và setter như C.p.fgetC.p.fset, nhưng bạn không thể dễ dàng sử dụng các cơ sở phương pháp thông thường thừa kế (ví dụ như siêu) để mở rộng chúng. Sau khi đào sâu vào những điều phức tạp của siêu phẩm, bạn thực sự có thể sử dụng siêu theo cách này:

# Using super():
class D(C):
    # Cannot use super(D,D) here to define the property
    # since D is not yet defined in this scope.
    @property
    def p(self):
        return super(D,D).p.fget(self)

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for D'
        super(D,D).p.fset(self, val)

# Using a direct reference to C
class E(C):
    p = C.p

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for E'
        C.p.fset(self, val)

Tuy nhiên, việc sử dụng super () khá khó hiểu, vì thuộc tính phải được xác định lại và bạn phải sử dụng cơ chế siêu (cls, cls) hơi phản trực quan để có được một bản sao không liên kết của p.


20

Sử dụng các thuộc tính là để tôi trực quan hơn và phù hợp hơn với hầu hết các mã.

So sánh

o.x = 5
ox = o.x

so với

o.setX(5)
ox = o.getX()

đối với tôi khá rõ ràng là dễ đọc hơn. Ngoài ra các thuộc tính cho phép các biến riêng tư dễ dàng hơn nhiều.


12

Tôi thích sử dụng không trong hầu hết các trường hợp. Vấn đề với các thuộc tính là chúng làm cho lớp không minh bạch. Đặc biệt, đây là một vấn đề nếu bạn muốn đưa ra một ngoại lệ từ một setter. Ví dụ: nếu bạn có thuộc tính Account.email:

class Account(object):
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if '@' not in value:
            raise ValueError('Invalid email address.')
        self._email = value

sau đó người dùng của lớp không mong đợi rằng việc gán giá trị cho thuộc tính có thể gây ra ngoại lệ:

a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.

Do đó, ngoại lệ có thể không được xử lý và lan truyền quá cao trong chuỗi cuộc gọi để được xử lý đúng cách hoặc dẫn đến một dấu vết rất không hữu ích được trình bày cho người dùng chương trình (điều đáng buồn là quá phổ biến trong thế giới của python và java ).

Tôi cũng sẽ tránh sử dụng getters và setters:

  • bởi vì việc xác định chúng cho tất cả các thuộc tính trước rất tốn thời gian,
  • làm cho số lượng mã dài hơn một cách không cần thiết, điều này làm cho việc hiểu và duy trì mã trở nên khó khăn hơn,
  • nếu bạn đã định nghĩa chúng cho các thuộc tính chỉ khi cần thiết, giao diện của lớp sẽ thay đổi, làm tổn thương tất cả người dùng của lớp

Thay vì các thuộc tính và getters / setters tôi thích thực hiện logic phức tạp ở những nơi được xác định rõ như trong một phương thức xác nhận:

class Account(object):
    ...
    def validate(self):
        if '@' not in self.email:
            raise ValueError('Invalid email address.')

hoặc một phương thức Account.save tương tự.

Lưu ý rằng tôi không cố gắng nói rằng không có trường hợp nào khi các thuộc tính hữu ích, chỉ có điều bạn có thể tốt hơn nếu bạn có thể làm cho các lớp học của mình đơn giản và minh bạch đến mức bạn không cần chúng.


3
@ user2239734 Tôi nghĩ bạn hiểu sai về khái niệm tài sản. Mặc dù bạn có thể xác nhận giá trị trong khi thiết lập thuộc tính, nhưng không cần phải làm điều đó. Bạn có thể có cả thuộc tính và validate()phương thức trong một lớp. Một thuộc tính được sử dụng đơn giản khi bạn có một logic phức tạp đằng sau một obj.x = yphép gán đơn giản và tùy thuộc vào logic đó là gì.
Zaur Nasibov

12

Tôi cảm thấy như các thuộc tính là về việc cho phép bạn có được chi phí viết các getters và setters chỉ khi bạn thực sự cần chúng.

Văn hóa lập trình Java khuyên bạn không bao giờ nên cấp quyền truy cập vào các thuộc tính và thay vào đó, hãy xem qua các getters và setters và chỉ những thứ thực sự cần thiết. Thật là một chút dài dòng để luôn luôn viết những đoạn mã rõ ràng này và nhận thấy rằng 70% thời gian chúng không bao giờ được thay thế bởi một số logic không tầm thường.

Trong Python, mọi người thực sự quan tâm đến loại chi phí đó, để bạn có thể thực hiện các thực hành sau:

  • Không sử dụng getters và setters lúc đầu, khi không cần thiết
  • Sử dụng @propertyđể thực hiện chúng mà không thay đổi cú pháp của phần còn lại của mã của bạn.

1
"và lưu ý rằng 70% thời gian chúng không bao giờ bị thay thế bởi một số logic không tầm thường." - đó là một con số khá cụ thể, nó đến từ một nơi nào đó, hoặc bạn dự định nó là một thứ "kiểu đại đa số" (tôi không phải là người vô tư, nếu có một nghiên cứu định lượng số đó, tôi sẽ thực sự thích đọc nó)
Adam Parkin

1
Ồ không xin lỗi. Có vẻ như tôi có một số nghiên cứu để sao lưu con số này, nhưng tôi chỉ có nghĩa là "hầu hết thời gian".
Fulmicoton

7
Không phải mọi người quan tâm đến chi phí hoạt động, mà trong Python bạn có thể thay đổi từ truy cập trực tiếp sang phương thức truy cập mà không thay đổi mã máy khách, do đó ban đầu bạn không có gì để mất.
Neil G

10

Tôi ngạc nhiên khi không ai đề cập rằng các thuộc tính là các phương thức ràng buộc của lớp mô tả, Adam DonohueNeilenMarais hiểu chính xác ý tưởng này trong các bài đăng của họ - rằng getters và setters là các hàm và có thể được sử dụng để:

  • xác nhận
  • thay đổi dữ liệu
  • loại vịt (loại cưỡng chế sang loại khác)

Điều này trình bày một cách thông minh để ẩn chi tiết triển khai và mã hành trình như biểu thức thông thường, kiểu phôi, thử .. ngoại trừ các khối, xác nhận hoặc các giá trị được tính toán.

Nói chung, việc thực hiện CRUD trên một đối tượng thường có thể khá trần tục nhưng hãy xem xét ví dụ về dữ liệu sẽ được duy trì cho cơ sở dữ liệu quan hệ. ORM có thể ẩn chi tiết triển khai của các vernacenses SQL cụ thể trong các phương thức bị ràng buộc là fget, fset, fdel được định nghĩa trong một lớp thuộc tính sẽ quản lý khủng khiếp nếu .. elif .. các thang khác rất xấu trong mã OO - phơi bày đơn giản và thanh lịch self.variable = somethingvà che giấu các chi tiết cho nhà phát triển bằng ORM.

Nếu người ta chỉ nghĩ về các thuộc tính như một số dấu tích đáng sợ của ngôn ngữ Bondage và Discipline (tức là Java) thì họ đang thiếu điểm mô tả.


6

Trong các dự án phức tạp, tôi thích sử dụng các thuộc tính chỉ đọc (hoặc getters) với chức năng setter rõ ràng:

class MyClass(object):
...        
@property
def my_attr(self):
    ...

def set_my_attr(self, value):
    ...

Trong các dự án sống lâu, việc gỡ lỗi và tái cấu trúc tốn nhiều thời gian hơn so với việc tự viết mã. Có một số nhược điểm khi sử dụng @property.setterkhiến việc gỡ lỗi trở nên khó khăn hơn:

1) python cho phép tạo các thuộc tính mới cho một đối tượng hiện có. Điều này làm cho một sai lầm sau đây rất khó để theo dõi:

my_object.my_atttr = 4.

Nếu đối tượng của bạn là một thuật toán phức tạp thì bạn sẽ mất khá nhiều thời gian để cố gắng tìm hiểu lý do tại sao nó không hội tụ (chú ý thêm một 't' trong dòng trên)

2) setter đôi khi có thể phát triển thành một phương thức phức tạp và chậm (ví dụ: nhấn cơ sở dữ liệu). Sẽ khá khó để một nhà phát triển khác tìm ra lý do tại sao chức năng sau rất chậm. Anh ta có thể dành nhiều thời gian cho do_something()phương pháp định hình , trong khi my_object.my_attr = 4.thực sự là nguyên nhân của sự chậm lại:

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()

5

Cả @propertygetters và setters truyền thống có lợi thế của họ. Nó phụ thuộc vào trường hợp sử dụng của bạn.

Lợi ích của @property

  • Bạn không phải thay đổi giao diện trong khi thay đổi việc thực hiện truy cập dữ liệu. Khi dự án của bạn nhỏ, bạn có thể muốn sử dụng quyền truy cập thuộc tính trực tiếp để truy cập một thành viên lớp. Ví dụ: giả sử bạn có một đối tượng foothuộc loại Foocó thành viên num. Sau đó, bạn có thể chỉ cần có được thành viên này với num = foo.num. Khi dự án của bạn phát triển, bạn có thể cảm thấy cần phải có một số kiểm tra hoặc gỡ lỗi về quyền truy cập thuộc tính đơn giản. Sau đó, bạn có thể làm điều đó với một @property trong lớp. Giao diện truy cập dữ liệu vẫn giữ nguyên để không cần sửa đổi mã máy khách.

    Trích dẫn từ PEP-8 :

    Đố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 triển khai chức năng đằng sau cú pháp truy cập thuộc tính dữ liệu đơn giản.

  • Sử dụng @propertyđể truy cập dữ liệu trong Python được coi là Pythonic :

    • Nó có thể củng cố khả năng tự nhận dạng của bạn như một lập trình viên Python (không phải Java).

    • Nó có thể giúp bạn phỏng vấn xin việc nếu người phỏng vấn của bạn nghĩ rằng các getters và setters kiểu Java là chống mẫu .

Ưu điểm của getters và setters truyền thống

  • Getters và setters truyền thống cho phép truy cập dữ liệu phức tạp hơn truy cập thuộc tính đơn giản. Ví dụ, khi bạn đang thiết lập một thành viên lớp, đôi khi bạn cần một lá cờ cho biết nơi bạn muốn buộc hoạt động này ngay cả khi một cái gì đó không hoàn hảo. Mặc dù không rõ ràng làm thế nào để tăng quyền truy cập thành viên trực tiếp như thế nào foo.num = num, Bạn có thể dễ dàng tăng trình thiết lập truyền thống của mình bằng một forcetham số bổ sung :

    def Foo:
        def set_num(self, num, force=False):
            ...
  • Getters và setters truyền thống làm cho nó rõ ràng rằng một thành viên lớp truy cập thông qua một phương thức. Điều này có nghĩa là:

    • Những gì bạn nhận được do kết quả có thể không giống với những gì được lưu trữ chính xác trong lớp đó.

    • Ngay cả khi truy cập trông giống như truy cập thuộc tính đơn giản, hiệu suất có thể thay đổi rất nhiều từ đó.

    Trừ khi người dùng lớp của bạn mong đợi @propertyẩn đằng sau mọi tuyên bố truy cập thuộc tính, làm cho những điều đó rõ ràng có thể giúp giảm thiểu những bất ngờ cho người dùng lớp của bạn.

  • Như được đề cập bởi @NeilenMarais và trong bài đăng này , việc mở rộng các getters và setters truyền thống trong các lớp con dễ dàng hơn so với việc mở rộng các thuộc tính.

  • Getters và setters truyền thống đã được sử dụng rộng rãi trong một thời gian dài trong các ngôn ngữ khác nhau. Nếu bạn có những người từ các nền tảng khác nhau trong nhóm của bạn, họ trông quen thuộc hơn @property. Ngoài ra, khi dự án của bạn phát triển, nếu bạn có thể cần di chuyển từ Python sang ngôn ngữ khác không có @property, sử dụng getters và setters truyền thống sẽ giúp việc di chuyển mượt mà hơn.

Hãy cẩn thận

  • Cả @propertygetters và setters truyền thống cũng không làm cho thành viên lớp riêng tư, ngay cả khi bạn sử dụng dấu gạch dưới kép trước tên của nó:

    class Foo:
        def __init__(self):
            self.__num = 0
    
        @property
        def num(self):
            return self.__num
    
        @num.setter
        def num(self, num):
            self.__num = num
    
        def get_num(self):
            return self.__num
    
        def set_num(self, num):
            self.__num = num
    
    foo = Foo()
    print(foo.num)          # output: 0
    print(foo.get_num())    # output: 0
    print(foo._Foo__num)    # output: 0

2

Đây là một đoạn trích từ "Python hiệu quả: 90 cách cụ thể để viết Python tốt hơn" (Cuốn sách tuyệt vời. Tôi đánh giá cao nó).

Những điều cần nhớ

Xác định giao diện lớp mới bằng các thuộc tính công khai đơn giản và tránh xác định các phương thức setter và getter.

Sử dụng @property để xác định hành vi đặc biệt khi các thuộc tính được truy cập trên các đối tượng của bạn, nếu cần.

Tuân theo quy tắc ít gây bất ngờ nhất và tránh các tác dụng phụ kỳ lạ trong các phương pháp @property của bạn.

Đảm bảo rằng các phương thức @property nhanh chóng; đối với công việc chậm hoặc phức tạp, đặc biệt là liên quan đến I / O hoặc gây ra tác dụng phụ, thay vào đó, sử dụng các phương pháp bình thường.

Một cách sử dụng tiên tiến nhưng phổ biến của @property là chuyển đổi những gì đã từng là một thuộc tính số đơn giản thành một phép tính nhanh chóng. Điều này cực kỳ hữu ích vì nó cho phép bạn di chuyển tất cả việc sử dụng một lớp hiện có để có các hành vi mới mà không yêu cầu bất kỳ trang web cuộc gọi nào được viết lại (điều này đặc biệt quan trọng nếu có mã gọi mà bạn không kiểm soát). @property cũng cung cấp một điểm dừng quan trọng để cải thiện giao diện theo thời gian.

Tôi đặc biệt thích @property vì nó cho phép bạn đạt được tiến bộ gia tăng đối với một mô hình dữ liệu tốt hơn theo thời gian.
@property là một công cụ giúp bạn giải quyết các vấn đề bạn sẽ gặp trong mã thế giới thực. Đừng lạm dụng nó. Khi bạn thấy mình liên tục mở rộng các phương thức @property, có lẽ đã đến lúc cấu trúc lại lớp của bạn thay vì mở rộng hơn nữa về thiết kế kém của mã của bạn.

Sử dụng @property để cung cấp cho các thuộc tính hiện có chức năng mới.

Thực hiện tiến bộ gia tăng đối với các mô hình dữ liệu tốt hơn bằng cách sử dụng @property.

Cân nhắc việc tái cấu trúc một lớp và tất cả các trang web gọi khi bạn thấy mình sử dụng @property quá nhiều.

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.