Khi nào tôi nên sử dụng các lớp trong Python?


175

Tôi đã lập trình trong python khoảng hai năm; chủ yếu là các công cụ dữ liệu (gấu trúc, mpl, numpy), nhưng cũng có các kịch bản tự động hóa và các ứng dụng web nhỏ. Tôi đang cố gắng trở thành một lập trình viên tốt hơn và tăng kiến ​​thức về con trăn của tôi và một trong những điều làm tôi khó chịu là tôi chưa bao giờ sử dụng một lớp học (ngoài việc sao chép mã bình ngẫu nhiên cho các ứng dụng web nhỏ). Tôi thường hiểu chúng là gì, nhưng dường như tôi không thể quấn lấy đầu mình tại sao tôi lại cần chúng qua một chức năng đơn giản.

Để thêm tính cụ thể cho câu hỏi của tôi: Tôi viết hàng tấn báo cáo tự động luôn liên quan đến việc lấy dữ liệu từ nhiều nguồn dữ liệu (mongo, sql, postgres, apis), thực hiện nhiều hoặc một ít dữ liệu munging và định dạng, ghi dữ liệu vào csv / excel / html, gửi nó trong một email. Các tập lệnh nằm trong khoảng từ ~ 250 dòng đến ~ 600 dòng. Có bất kỳ lý do cho tôi để sử dụng các lớp để làm điều này và tại sao?


15
Không có gì sai khi viết mã không có lớp nếu bạn có thể quản lý mã đẹp hơn. Các lập trình viên OOP có xu hướng phóng đại các vấn đề do các ràng buộc từ thiết kế ngôn ngữ hoặc sự hiểu biết hời hợt về các mẫu khác nhau.
Jason Hu

Câu trả lời:


132

Các lớp học là trụ cột của lập trình hướng đối tượng . OOP rất quan tâm đến tổ chức mã, tái sử dụng và đóng gói.

Đầu tiên, từ chối trách nhiệm: OOP trái ngược một phần với Lập trình hàm , đây là một mô hình khác được sử dụng rất nhiều trong Python. Không phải ai lập trình bằng Python (hoặc chắc chắn là hầu hết các ngôn ngữ) đều sử dụng OOP. Bạn có thể làm rất nhiều thứ trong Java 8 không hướng đối tượng. Nếu bạn không muốn sử dụng OOP, thì đừng. Nếu bạn chỉ viết các tập lệnh một lần để xử lý dữ liệu mà bạn sẽ không bao giờ sử dụng lại, thì hãy tiếp tục viết theo cách của bạn.

Tuy nhiên, có rất nhiều lý do để sử dụng OOP.

Một số lý do:

  • Tổ chức: OOP định nghĩa các cách mô tả và tiêu chuẩn nổi tiếng và xác định cả dữ liệu và thủ tục trong mã. Cả dữ liệu và thủ tục có thể được lưu trữ ở các mức định nghĩa khác nhau (trong các lớp khác nhau) và có những cách tiêu chuẩn để nói về các định nghĩa này. Đó là, nếu bạn sử dụng OOP theo cách tiêu chuẩn, nó sẽ giúp bản thân sau này của bạn và những người khác hiểu, chỉnh sửa và sử dụng mã của bạn. Ngoài ra, thay vì sử dụng một cơ chế lưu trữ dữ liệu phức tạp, tùy ý (dicts dicts hoặc list hoặc dicts hoặc list of dicts of sets, or any), bạn có thể đặt tên cho các phần của cấu trúc dữ liệu và thuận tiện tham khảo chúng.

  • Trạng thái: OOP giúp bạn xác định và theo dõi trạng thái. Chẳng hạn, trong một ví dụ cổ điển, nếu bạn đang tạo một chương trình xử lý học sinh (ví dụ: chương trình lớp), bạn có thể giữ tất cả thông tin bạn cần về họ tại một điểm (tên, tuổi, giới tính, cấp lớp, các khóa học, lớp, giáo viên, đồng nghiệp, chế độ ăn uống, nhu cầu đặc biệt, v.v.) và dữ liệu này được duy trì miễn là đối tượng còn sống và có thể dễ dàng truy cập.

  • Đóng gói : Với đóng gói, thủ tục và dữ liệu được lưu trữ cùng nhau. Các phương thức (thuật ngữ OOP cho các hàm) được xác định ngay bên cạnh dữ liệu mà chúng hoạt động và tạo ra. Trong một ngôn ngữ như Java cho phép kiểm soát truy cập hoặc bằng Python, tùy thuộc vào cách bạn mô tả API công khai của mình, điều này có nghĩa là các phương thức và dữ liệu có thể bị ẩn khỏi người dùng. Điều này có nghĩa là nếu bạn cần hoặc muốn thay đổi mã, bạn có thể làm bất cứ điều gì bạn muốn để triển khai mã, nhưng vẫn giữ các API công khai như cũ.

  • Kế thừa : Kế thừa cho phép bạn xác định dữ liệu và thủ tục ở một nơi (trong một lớp), sau đó ghi đè hoặc mở rộng chức năng đó sau. Chẳng hạn, trong Python, tôi thường thấy mọi người tạo các lớp con của dictlớp để thêm chức năng bổ sung. Một thay đổi phổ biến là ghi đè phương thức đưa ra một ngoại lệ khi một khóa được yêu cầu từ một từ điển không tồn tại để đưa ra một giá trị mặc định dựa trên một khóa không xác định. Điều này cho phép bạn mở rộng mã của riêng mình ngay bây giờ hoặc sau này, cho phép người khác mở rộng mã của bạn và cho phép bạn mở rộng mã của người khác.

  • Khả năng sử dụng lại: Tất cả những lý do này và những lý do khác cho phép khả năng sử dụng lại mã lớn hơn. Mã hướng đối tượng cho phép bạn viết mã solid (đã kiểm tra) một lần, và sau đó sử dụng lại nhiều lần. Nếu bạn cần điều chỉnh một cái gì đó cho trường hợp sử dụng cụ thể của mình, bạn có thể kế thừa từ một lớp hiện có và ghi đè lên hành vi hiện có. Nếu bạn cần thay đổi một cái gì đó, bạn có thể thay đổi tất cả trong khi duy trì chữ ký phương thức công khai hiện có, và không ai là người khôn ngoan hơn (hy vọng).

Một lần nữa, có một số lý do không sử dụng OOP và bạn không cần phải làm vậy. Nhưng may mắn thay với một ngôn ngữ như Python, bạn có thể sử dụng chỉ một chút hoặc rất nhiều, tùy bạn.

Một ví dụ về trường hợp sử dụng của sinh viên (không đảm bảo về chất lượng mã, chỉ là một ví dụ):

Hướng đối tượng

class Student(object):
    def __init__(self, name, age, gender, level, grades=None):
        self.name = name
        self.age = age
        self.gender = gender
        self.level = level
        self.grades = grades or {}

    def setGrade(self, course, grade):
        self.grades[course] = grade

    def getGrade(self, course):
        return self.grades[course]

    def getGPA(self):
        return sum(self.grades.values())/len(self.grades)

# Define some students
john = Student("John", 12, "male", 6, {"math":3.3})
jane = Student("Jane", 12, "female", 6, {"math":3.5})

# Now we can get to the grades easily
print(john.getGPA())
print(jane.getGPA())

Tiêu chuẩn Dict

def calculateGPA(gradeDict):
    return sum(gradeDict.values())/len(gradeDict)

students = {}
# We can set the keys to variables so we might minimize typos
name, age, gender, level, grades = "name", "age", "gender", "level", "grades"
john, jane = "john", "jane"
math = "math"
students[john] = {}
students[john][age] = 12
students[john][gender] = "male"
students[john][level] = 6
students[john][grades] = {math:3.3}

students[jane] = {}
students[jane][age] = 12
students[jane][gender] = "female"
students[jane][level] = 6
students[jane][grades] = {math:3.5}

# At this point, we need to remember who the students are and where the grades are stored. Not a huge deal, but avoided by OOP.
print(calculateGPA(students[john][grades]))
print(calculateGPA(students[jane][grades]))

Bởi vì "năng suất" đóng gói Python thường sạch hơn với các trình tạo và trình quản lý bối cảnh so với các lớp.
Dmitry Rubanovich

4
@meter Tôi đã thêm một ví dụ. Tôi hy vọng nó sẽ giúp. Lưu ý ở đây là thay vì phải dựa vào các khóa của các dicts của bạn có tên chính xác, trình thông dịch Python tạo ra ràng buộc này cho bạn nếu bạn làm phiền và buộc bạn phải sử dụng các phương thức được xác định (mặc dù không phải là các trường được xác định (mặc dù Java và các trường khác Các ngôn ngữ OOP không cho phép bạn xác định các trường bên ngoài các lớp như Python)).
dantiston

5
@meter cũng là một ví dụ về đóng gói: giả sử ngày hôm nay việc triển khai này là tốt vì tôi chỉ cần lấy GPA cho 50.000 sinh viên tại trường đại học của tôi một lần. Bây giờ ngày mai chúng tôi nhận được một khoản trợ cấp và cần cung cấp GPA hiện tại của mỗi học sinh mỗi giây (tất nhiên, không ai sẽ yêu cầu điều này, nhưng chỉ để làm cho nó có tính toán thách thức). Sau đó, chúng ta có thể "ghi nhớ" GPA và chỉ tính toán nó khi nó thay đổi (ví dụ, bằng cách đặt một biến trong phương thức setGrade), khác trả về một phiên bản được lưu trong bộ nhớ cache. Người dùng vẫn sử dụng getGPA () nhưng việc triển khai đã thay đổi.
dantiston

4
@dantiston, ví dụ này cần bộ sưu tập.namedtuple. Bạn có thể tạo một loại Sinh viên mới = bộ sưu tập.namedtuple ("Sinh viên", "tên, tuổi, giới tính, cấp độ, điểm số"). Và sau đó, bạn có thể tạo các trường hợp john = Student ("John", 12, "male", lớp = {'math': 3.5}, level = 6). Lưu ý rằng bạn sử dụng cả hai đối số vị trí và được đặt tên giống như khi tạo một lớp. Đây là kiểu dữ liệu đã được triển khai cho bạn trong Python. Sau đó, bạn có thể tham khảo john [0] hoặc john.name để lấy phần tử đầu tiên của bộ dữ liệu. Bây giờ bạn có thể lấy điểm của john là john.grades.values ​​(). Và nó đã được thực hiện cho bạn.
Dmitry Rubanovich

2
đối với tôi đóng gói là một lý do đủ tốt để luôn sử dụng OOP. Tôi đấu tranh để thấy giá trị KHÔNG sử dụng OOP cho bất kỳ dự án mã hóa có kích thước hợp lý nào. Tôi đoán tôi cần câu trả lời cho câu hỏi ngược :)
San Jay

23

Bất cứ khi nào bạn cần duy trì trạng thái của các chức năng của mình và nó không thể được thực hiện bằng các trình tạo (các hàm mang lại thay vì trả về). Máy phát điện duy trì trạng thái riêng của họ.

Nếu bạn muốn ghi đè bất kỳ toán tử tiêu chuẩn nào , bạn cần một lớp.

Bất cứ khi nào bạn sử dụng mẫu Khách truy cập, bạn sẽ cần các lớp. Mọi mẫu thiết kế khác có thể được thực hiện một cách hiệu quả và sạch sẽ hơn với các trình tạo, trình quản lý bối cảnh (cũng được triển khai tốt hơn như các trình tạo so với các lớp) và các loại POD (từ điển, danh sách và bộ dữ liệu, v.v.).

Nếu bạn muốn viết mã "pythonic", bạn nên ưu tiên trình quản lý bối cảnh và trình tạo trên các lớp. Nó sẽ sạch hơn.

Nếu bạn muốn mở rộng chức năng, hầu như bạn sẽ luôn có thể hoàn thành nó với sự ngăn chặn hơn là kế thừa.

Như mọi quy tắc, điều này có một ngoại lệ. Nếu bạn muốn đóng gói chức năng một cách nhanh chóng (nghĩa là viết mã kiểm tra thay vì mã có thể sử dụng lại ở cấp thư viện), bạn có thể gói gọn trạng thái trong một lớp. Nó sẽ đơn giản và sẽ không cần phải sử dụng lại.

Nếu bạn cần một hàm hủy kiểu C ++ (RIIA), bạn chắc chắn KHÔNG muốn sử dụng các lớp. Bạn muốn quản lý bối cảnh.


1
Việc đóng @DmitryRubanovich không được thực hiện thông qua các trình tạo trong Python.
Eli Korvigo

1
@DmitryRubanovich Tôi đã đề cập đến "việc đóng cửa được triển khai như các trình tạo trong Python", điều đó không đúng. Đóng cửa linh hoạt hơn nhiều. Các trình tạo được buộc phải trả về một Generatorthể hiện (một trình vòng lặp đặc biệt), trong khi các bao đóng có thể có bất kỳ chữ ký nào. Về cơ bản, bạn có thể tránh các lớp hầu hết thời gian bằng cách tạo các bao đóng. Và đóng cửa không chỉ đơn thuần là "các chức năng được xác định trong bối cảnh của các chức năng khác".
Eli Korvigo

3
@Eli Korvigo, trên thực tế, máy phát điện là một bước nhảy vọt đáng kể về mặt cú pháp. Chúng tạo ra một sự trừu tượng của một hàng đợi theo cùng cách mà các hàm là trừu tượng hóa của một ngăn xếp. Và hầu hết các luồng dữ liệu có thể được nối với nhau từ các nguyên hàm stack / queue.
Dmitry Rubanovich

1
@DmitryRubanovich chúng ta đang nói về táo và cam ở đây. Tôi đang nói rằng, các máy phát điện rất hữu ích trong một số trường hợp rất hạn chế và không có cách nào có thể được coi là sự thay thế cho các cuộc gọi trạng thái có mục đích chung. Bạn đang nói với tôi, họ tuyệt vời như thế nào, mà không mâu thuẫn với quan điểm của tôi.
Eli Korvigo

1
@Eli Korvigo, và tôi đang nói rằng các hàm gọi chỉ là khái quát của các hàm. Mà chính họ là cú pháp đường qua chế biến ngăn xếp. Trong khi máy phát điện là cú pháp đường qua xử lý hàng đợi. Nhưng chính sự cải tiến về cú pháp này cho phép các cấu trúc phức tạp hơn được xây dựng dễ dàng và với cú pháp rõ ràng hơn. '.next ()' gần như không bao giờ được sử dụng, btw.
Dmitry Rubanovich

11

Tôi nghĩ bạn làm đúng. Các lớp học là hợp lý khi bạn cần mô phỏng một số logic kinh doanh hoặc các quy trình thực tế khó khăn với các mối quan hệ khó khăn. Ví dụ như:

  • Một số chức năng với trạng thái chia sẻ
  • Nhiều bản sao của cùng một biến trạng thái
  • Để mở rộng hành vi của một chức năng hiện có

Tôi cũng đề nghị bạn xem video cổ điển này


3
Không cần sử dụng một lớp khi hàm gọi lại cần trạng thái liên tục trong Python. Sử dụng năng suất của Python thay vì trả về làm cho hàm được cấp lại.
Dmitry Rubanovich

4

Một lớp định nghĩa một thực thể thế giới thực. Nếu bạn đang làm việc trên một cái gì đó tồn tại riêng lẻ và có logic riêng tách biệt với những thứ khác, bạn nên tạo một lớp cho nó. Ví dụ, một lớp đóng gói kết nối cơ sở dữ liệu.

Nếu đây không phải là trường hợp, không cần phải tạo lớp


0

Nó phụ thuộc vào ý tưởng và thiết kế của bạn. nếu bạn là nhà thiết kế giỏi hơn OOP sẽ xuất hiện một cách tự nhiên dưới dạng các mẫu thiết kế khác nhau. Đối với một OOP, mức xử lý kịch bản đơn giản có thể là chi phí chung. Đơn giản hãy xem xét các lợi ích cơ bản của OOP như có thể tái sử dụng và có thể mở rộng và đảm bảo rằng chúng có cần thiết hay không. OOP làm cho những điều phức tạp trở nên đơn giản và những điều đơn giản trở nên phức tạp. Đơn giản chỉ cần giữ mọi thứ đơn giản theo cách sử dụng OOP hoặc không sử dụng OOP. mà bao giờ là đơn giản hơn sử dụng đó.

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.