Có cách nào pythonic để tách rời chức năng tùy chọn khỏi mục đích chính của chức năng không?


11

Bối cảnh

Giả sử tôi có mã Python sau:

def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        for _ in range(n_iters):
            number = halve(number)
        sum_all += number
    return sum_all


ns = [1, 3, 12]
print(example_function(ns, 3))

example_functionở đây chỉ đơn giản là đi qua từng yếu tố trong nsdanh sách và giảm một nửa chúng 3 lần, trong khi tích lũy kết quả. Đầu ra của việc chạy tập lệnh này chỉ đơn giản là:

2.0

Vì 1 / (2 ^ 3) * (1 + 3 + 12) = 2.

Bây giờ, giả sử rằng (vì bất kỳ lý do nào, có thể gỡ lỗi hoặc ghi nhật ký), tôi muốn hiển thị một số loại thông tin về các bước trung gian mà việc example_functionnày đang thực hiện. Có lẽ sau đó tôi sẽ viết lại hàm này thành một cái gì đó như thế này:

def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        print('Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            print(number)
        sum_all += number
        print('sum_all:', sum_all)
    return sum_all

mà bây giờ, khi được gọi với cùng các đối số như trước, sẽ đưa ra các kết quả sau:

Processing number 1
0.5
0.25
0.125
sum_all: 0.125
Processing number 3
1.5
0.75
0.375
sum_all: 0.5
Processing number 12
6.0
3.0
1.5
sum_all: 2.0

Điều này đạt được chính xác những gì tôi dự định. Tuy nhiên, điều này đi ngược lại một chút so với nguyên tắc rằng một hàm chỉ nên làm một việc, và bây giờ mã cho example_functiondài hơn và phức tạp hơn. Đối với một hàm đơn giản như vậy thì đây không phải là vấn đề, nhưng trong ngữ cảnh của tôi, tôi có các hàm khá phức tạp gọi nhau và các câu lệnh in thường bao gồm các bước phức tạp hơn được hiển thị ở đây, dẫn đến sự gia tăng đáng kể về độ phức tạp của mã của tôi (đối với một trong số các hàm của tôi có nhiều dòng mã liên quan đến ghi nhật ký hơn các dòng liên quan đến mục đích thực tế của nó!).

Hơn nữa, nếu sau này tôi quyết định rằng tôi không muốn bất kỳ câu lệnh in nào trong chức năng của mình nữa, tôi sẽ phải thực hiện example_functionvà xóa tất cả các printcâu lệnh theo cách thủ công, cùng với bất kỳ biến nào liên quan đến chức năng này, một quá trình vừa tẻ nhạt vừa có lỗi -dễ bị.

Tình hình thậm chí còn tồi tệ hơn nếu tôi luôn có khả năng in hoặc không in trong khi thực thi chức năng, dẫn đến tôi phải khai báo hai chức năng cực kỳ giống nhau (một có printcâu lệnh, một không có), rất tệ cho việc duy trì, hoặc để định nghĩa một cái gì đó như:

def example_function(numbers, n_iters, debug_mode=False):
    sum_all = 0
    for number in numbers:
        if debug_mode:
            print('Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            if debug_mode:
                print(number)
        sum_all += number
        if debug_mode:
            print('sum_all:', sum_all)
    return sum_all

dẫn đến một chức năng cồng kềnh và (hy vọng) phức tạp không cần thiết, ngay cả trong trường hợp đơn giản của chúng tôi example_function.


Câu hỏi

Có cách nào để "tách" chức năng in ra khỏi chức năng ban đầu example_functionkhông?

Tổng quát hơn, có một cách pythonic để tách rời chức năng tùy chọn khỏi mục đích chính của chức năng?


Những gì tôi đã cố gắng cho đến nay:

Giải pháp tôi đã tìm thấy tại thời điểm này là sử dụng các cuộc gọi lại cho việc tách rời. Chẳng hạn, người ta có thể viết lại example_functionnhư thế này:

def example_function(numbers, n_iters, callback=None):
    sum_all = 0
    for number in numbers:
        for i_iter in range(n_iters):
            number = number/2

            if callback is not None:
                callback(locals())
        sum_all += number
    return sum_all

và sau đó xác định chức năng gọi lại thực hiện bất kỳ chức năng in nào tôi muốn:

def print_callback(locals):
    print(locals['number'])

và gọi example_functionnhư thế này:

ns = [1, 3, 12]
example_function(ns, 3, callback=print_callback)

mà sau đó đầu ra:

0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0

Điều này tách thành công chức năng in từ chức năng cơ bản của example_function. Tuy nhiên, vấn đề chính của phương pháp này là chức năng gọi lại chỉ có thể được chạy ở một phần cụ thể của example_function(trong trường hợp này ngay sau khi giảm một nửa số hiện tại) và tất cả việc in phải diễn ra chính xác ở đó. Điều này đôi khi buộc thiết kế chức năng gọi lại khá phức tạp (và làm cho một số hành vi không thể đạt được).

Chẳng hạn, nếu người ta muốn đạt được chính xác kiểu in giống như tôi đã làm trong phần trước của câu hỏi (cho biết số nào đang được xử lý, cùng với các phân nửa tương ứng của nó) thì cuộc gọi lại kết quả sẽ là:

def complicated_callback(locals):
    i_iter = locals['i_iter']
    number = locals['number']
    if i_iter == 0:
        print('Processing number', number*2)
    print(number)
    if i_iter == locals['n_iters']-1:
        print('sum_all:', locals['sum_all']+number)

dẫn đến kết quả chính xác như trước:

Processing number 1.0
0.5
0.25
0.125
sum_all: 0.125
Processing number 3.0
1.5
0.75
0.375
sum_all: 0.5
Processing number 12.0
6.0
3.0
1.5
sum_all: 2.0

nhưng là một nỗi đau để viết, đọc và gỡ lỗi.


6
kiểm tra loggingmô-đun python
Chris_Rands

@Chris_Rands đúng .. sử dụng mô-đun ghi nhật ký .. theo cách đó bạn có thể bật và tắt đăng nhập .. sử dụng liên kết sau. stackoverflow.com/questions/2266646/ cường
Yatish Kadam

2
Tôi không thấy loggingmô-đun sẽ giúp gì ở đây. Mặc dù câu hỏi của tôi sử dụng các printcâu lệnh khi thiết lập bối cảnh, nhưng tôi thực sự đang tìm kiếm một giải pháp làm thế nào để tách rời bất kỳ loại chức năng tùy chọn nào khỏi mục đích chính của chức năng. Ví dụ, có thể tôi muốn một hàm để vẽ đồ thị khi nó chạy. Trong trường hợp đó tôi tin rằng loggingmô-đun thậm chí sẽ không được áp dụng.
JLagana

3
@Pythonic là một tính từ mô tả cú pháp / kiểu / cấu trúc / cách sử dụng python để duy trì triết lý của Python. Đây không phải là một quy tắc cú pháp hay thiết kế, mà là một cách tiếp cận cần được duy trì một cách có trách nhiệm để tạo ra một cơ sở mã trăn sạch và có thể bảo trì. Trong trường hợp của bạn, có một vài dòng dấu vết hoặc câu lệnh in sẽ thêm các giá trị cho khả năng duy trì thì sẽ có nó; đừng tự làm khó bản thân Xem xét bất kỳ phương pháp đã nói ở trên mà bạn nghĩ là lý tưởng.
Nair

1
Câu hỏi này quá rộng. Chúng tôi có thể có thể giải quyết các câu hỏi cụ thể (như các đề xuất để sử dụng loggingchứng minh), nhưng không thể phân tách mã tùy ý.
chepner

Câu trả lời:


4

Nếu bạn cần chức năng bên ngoài chức năng để sử dụng dữ liệu từ bên trong chức năng, thì cần phải có một số hệ thống nhắn tin bên trong chức năng để hỗ trợ việc này. Không có cách nào xung quanh điều này. Các biến cục bộ trong các hàm được cách ly hoàn toàn với bên ngoài.

Mô-đun đăng nhập khá tốt trong việc thiết lập một hệ thống tin nhắn. Nó không chỉ bị hạn chế in ra các thông điệp tường trình - sử dụng trình xử lý tùy chỉnh, bạn có thể làm bất cứ điều gì.

Thêm hệ thống tin nhắn tương tự như ví dụ gọi lại của bạn, ngoại trừ những nơi xử lý 'cuộc gọi lại' (trình xử lý ghi nhật ký) có thể được chỉ định ở bất cứ đâu trong example_function (bằng cách gửi tin nhắn đến bộ ghi chép). Bất kỳ biến nào được các trình xử lý ghi nhật ký cần có thể được chỉ định khi bạn gửi tin nhắn (bạn vẫn có thể sử dụng locals(), nhưng tốt nhất là khai báo rõ ràng các biến bạn cần).

Một cái mới example_functioncó thể trông giống như:

import logging

# Helper function
def send_message(logger, level=logging.DEBUG, **kwargs):
  logger.log(level, "", extra=kwargs)

# Your example function with logging information
def example_function(numbers, n_iters):
    logger = logging.getLogger("example_function")
    # If you have a logging system set up, then we don't want the messages sent here to propagate to the root logger
    logger.propagate = False
    sum_all = 0
    for number in numbers:
        send_message(logger, action="processing", number=number)
        for i_iter in range(n_iters):
            number = number/2
            send_message(logger, action="division", i_iter=i_iter, number=number)
        sum_all += number
        send_message(logger, action="sum", sum=sum_all)
    return sum_all

Điều này xác định ba vị trí mà các tin nhắn có thể được xử lý. Về bản thân, điều này example_functionsẽ không làm gì khác ngoài chức năng của example_functionchính nó. Nó sẽ không in ra bất cứ điều gì, hoặc làm bất kỳ chức năng nào khác.

Để thêm chức năng bổ sung cho example_function, sau đó bạn sẽ cần thêm trình xử lý vào bộ ghi.

Ví dụ: nếu bạn muốn thực hiện một số in ra các biến đã gửi (tương tự như debuggingví dụ của bạn ), thì bạn xác định trình xử lý tùy chỉnh và thêm nó vào example_functionlogger:

class ExampleFunctionPrinter(logging.Handler):
    def emit(self, record):
        if record.action == "processing":
          print("Processing number {}".format(record.number))
        elif record.action == "division":
          print(record.number)
        elif record.action == "sum":
          print("sum_all: {}".format(record.sum))

example_function_logger = logging.getLogger("example_function")
example_function_logger.setLevel(logging.DEBUG)
example_function_logger.addHandler(ExampleFunctionPrinter())

Nếu bạn muốn vẽ kết quả trên biểu đồ, thì chỉ cần xác định một trình xử lý khác:

class ExampleFunctionDivisionGrapher(logging.Handler):
    def __init__(self, grapher):
      self.grapher = grapher

    def emit(self, record):
      if record.action == "division":
        self.grapher.plot_point(x=record.i_iter, y=record.number)

example_function_logger = logging.getLogger("example_function")
example_function_logger.setLevel(logging.DEBUG)
example_function_logger.addHandler(
    ExampleFunctionDivisionGrapher(MyFancyGrapherClass())
)

Bạn có thể xác định và thêm vào bất cứ trình xử lý nào bạn muốn. Chúng sẽ hoàn toàn tách biệt với chức năng của example_functionvà chỉ có thể sử dụng các biến mà example_functionchúng mang lại cho chúng.

Mặc dù ghi nhật ký có thể được sử dụng như một hệ thống nhắn tin, nhưng tốt hơn là nên chuyển sang một hệ thống nhắn tin đầy đủ, chẳng hạn như PyPubSub , để nó không can thiệp vào bất kỳ hoạt động ghi nhật ký thực tế nào bạn có thể làm:

from pubsub import pub

# Your example function
def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        pub.sendMessage("example_function.processing", number=number)
        for i_iter in range(n_iters):
            number = number/2
            pub.sendMessage("example_function.division", i_iter=i_iter, number=number)
        sum_all += number
        pub.sendMessage("example_function.sum", sum=sum_all)
    return sum_all

# If you need extra functionality added in, then subscribe to the messages.
# Otherwise nothing will happen, other than the normal example_function functionality.
def handle_example_function_processing(number):
    print("Processing number {}".format(number))

def handle_example_function_division(i_iter, number):
    print(number)

def handle_example_function_sum(sum):
    print("sum_all: {}".format(sum))

pub.subscribe(
    "example_function.processing",
    handle_example_function_processing
)
pub.subscribe(
    "example_function.division",
    handle_example_function_division
)
pub.subscribe(
    "example_function.sum",
    handle_example_function_sum
)

Cảm ơn câu trả lời, RPalmer. Mã bạn cung cấp bằng cách sử dụng loggingmô-đun thực sự có tổ chức và duy trì hơn so với những gì tôi đề xuất sử dụng printifbáo cáo. Tuy nhiên, nó không tách rời chức năng in từ chức năng chính của example_functionchức năng. Đó là, vấn đề chính của việc example_functionlàm hai việc cùng một lúc vẫn còn, làm cho mã của nó phức tạp hơn những gì tôi muốn.
JLagana

So sánh điều này với ví dụ gợi ý gọi lại của tôi. Sử dụng các cuộc gọi lại, example_functiongiờ đây chỉ có một chức năng và công cụ in (hoặc bất kỳ chức năng nào khác mà chúng tôi muốn có) xảy ra bên ngoài chức năng đó.
JLagana

Xin chào @JLagana. My example_functionđược tách rời khỏi chức năng in - chức năng duy nhất được thêm vào chức năng là gửi tin nhắn. Nó tương tự như ví dụ gọi lại của bạn, ngoại trừ việc nó chỉ gửi các biến cụ thể mà bạn muốn, chứ không phải tất cả locals(). Tùy thuộc vào trình xử lý nhật ký (mà bạn đính kèm với trình ghi nhật ký ở một nơi khác) để thực hiện các chức năng bổ sung (in, vẽ biểu đồ, v.v.). Bạn không cần phải đính kèm bất kỳ trình xử lý nào, trong trường hợp đó sẽ không có gì xảy ra khi các tin nhắn được gửi. Tôi đã cập nhật bài viết của mình để làm cho điều này rõ ràng hơn.
RPalmer

Tôi đứng chính xác, ví dụ của bạn đã tách rời chức năng in từ chức năng chính của example_function. Cảm ơn đã làm cho nó thêm rõ ràng bây giờ! Tôi thực sự thích câu trả lời này, cái giá duy nhất được trả là sự phức tạp thêm của việc gửi tin nhắn, như bạn đã đề cập, dường như là không thể tránh khỏi. Cũng xin cảm ơn vì đã tham khảo PyPubSub, điều này khiến tôi đọc được mô hình quan sát viên .
JLagana

1

Nếu bạn muốn chỉ sử dụng các câu lệnh in, bạn có thể sử dụng một trình trang trí có thêm một đối số bật / tắt in để điều khiển.

Dưới đây là một trình trang trí thêm đối số chỉ từ khóa và giá trị mặc định của verbose=Falsebất kỳ chức năng nào, cập nhật chuỗi và chữ ký. Gọi hàm as-là trả về đầu ra dự kiến. Gọi hàm với verbose=Truesẽ bật các câu lệnh in và trả về đầu ra dự kiến. Điều này có thêm lợi ích của việc không phải mở đầu mỗi bản in bằng một if debug:khối.

from functools import wraps
from inspect import cleandoc, signature, Parameter
import sys
import os

def verbosify(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        def toggle(*args, verbose=False, **kwargs):
            if verbose:
                _stdout = sys.stdout
            else:
                _stdout = open(os.devnull, 'w')
            with redirect_stdout(_stdout):
                return func(*args, **kwargs)
        return toggle(*args, **kwargs)
    # update the docstring
    doc = '\n\nOption:\n-------\nverbose : bool\n    '
    doc += 'Turns on/off print lines in the function.\n '
    wrapper.__doc__ = cleandoc(wrapper.__doc__ or '\n') + doc
    # update the function signature to include the verbose keyword
    sig = signature(func)
    param_verbose = Parameter('verbose', Parameter.KEYWORD_ONLY, default=False)
    sig_params = tuple(sig.parameters.values()) + (param_verbose,)
    sig = sig.replace(parameters=sig_params)
    wrapper.__signature__ = sig
    return wrapper

Đóng gói chức năng của bạn bây giờ cho phép bạn bật / tắt chức năng in bằng cách sử dụng verbose.

@verbosify
def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        print('Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            print(number)
        sum_all += number
        print('sum_all:', sum_all)
    return sum_all

Ví dụ:

example_function([1,3,12], 3)
# returns:
2.0

example_function([1,3,12], 3, verbose=True)
# returns/prints:
Processing number 1
0.5
0.25
0.125
sum_all: 0.125
Processing number 3
1.5
0.75
0.375
sum_all: 0.5
Processing number 12
6.0
3.0
1.5
sum_all: 2.0
2.0

Khi bạn kiểm tra example_function, bạn sẽ thấy các tài liệu cập nhật là tốt. Vì chức năng của bạn không có chuỗi doc, nó chỉ là những gì có trong trang trí.

help(example_function)
# prints:
Help on function example_function in module __main__:

example_function(numbers, n_iters, *, verbose=False)
    Option:
    -------
    verbose : bool
        Turns on/off print lines in the function.

Về mặt triết lý mã hóa. Có chức năng mà không phải là tác dụng phụ là một mô hình lập trình chức năng. Python có thể là một ngôn ngữ chức năng, nhưng nó không được thiết kế để độc quyền theo cách đó. Tôi luôn luôn thiết kế mã của mình với người dùng.

Nếu việc thêm tùy chọn để in các bước tính toán là một lợi ích cho người dùng, thì KHÔNG có gì sai khi làm điều đó. Từ quan điểm thiết kế, bạn sẽ bị mắc kẹt với việc thêm vào các lệnh in / ghi nhật ký ở đâu đó.


Cảm ơn câu trả lời, James. Mã được cung cấp thực sự có tổ chức và duy trì hơn so với mã tôi đề xuất, đó là cách sử dụng printifcâu lệnh. Hơn nữa, nó quản lý để thực sự tách rời một phần chức năng in từ example_functionchức năng chính của nó, điều này rất hay (tôi cũng thích rằng trình trang trí tự động gắn vào chuỗi doc, cảm ứng đẹp). Tuy nhiên, nó không tách rời hoàn toàn chức năng in từ chức năng chính của example_function: bạn vẫn phải thêm các printcâu lệnh và bất kỳ logic đi kèm nào vào thân của hàm.
JLagana

So sánh điều này với ví dụ gợi ý gọi lại của tôi. Sử dụng các cuộc gọi lại, example_feft hiện chỉ có một chức năng và công cụ in (hoặc bất kỳ chức năng nào khác mà chúng tôi muốn có) xảy ra bên ngoài chức năng đó.
JLagana

Cuối cùng, chúng tôi đồng ý rằng nếu in các bước tính toán là một lợi ích cho người dùng, thì tôi sẽ bị mắc kẹt với việc thêm các lệnh in ở đâu đó. Tuy nhiên tôi muốn chúng ở bên ngoài example_functioncơ thể, vì vậy sự phức tạp của nó chỉ liên quan đến sự phức tạp của chức năng chính của nó. Trong ứng dụng thực tế của tôi về tất cả những điều này, tôi có một chức năng chính vốn đã phức tạp đáng kể. Thêm các báo cáo in / vẽ / ghi nhật ký vào cơ thể của nó làm cho nó trở thành một con thú khá khó khăn để duy trì và gỡ lỗi.
JLagana

1

Bạn có thể định nghĩa một hàm đóng gói debug_modeđiều kiện và truyền hàm tùy chọn mong muốn và các đối số của nó cho hàm đó (như được đề xuất ở đây ):

def DEBUG(function, *args):
    if debug_mode:
        function(*args)

def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        DEBUG(print, 'Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            DEBUG(print, number)
        sum_all += number
        DEBUG(print, 'sum_all:', sum_all)
    return sum_all

ns = [1, 3, 12]
debug_mode = True
print(example_function(ns, 3))

Lưu ý rằng debug_moderõ ràng phải được gán một giá trị trước khi gọi DEBUG.

Tất nhiên là có thể gọi các hàm khác hơn print.

Bạn cũng có thể mở rộng khái niệm này đến một số mức gỡ lỗi bằng cách sử dụng giá trị số cho debug_mode.


Cảm ơn câu trả lời, Gerd. Thật vậy, giải pháp của bạn đã thoát khỏi sự cần thiết của các ifbáo cáo ở khắp mọi nơi, và cũng làm cho việc bật và tắt in dễ dàng hơn. Tuy nhiên, nó không tách rời chức năng in từ chức năng chính của example_function. So sánh điều này với ví dụ gợi ý gọi lại của tôi. Sử dụng các cuộc gọi lại, example_feft hiện chỉ có một chức năng và công cụ in (hoặc bất kỳ chức năng nào khác mà chúng tôi muốn có) xảy ra bên ngoài chức năng đó.
JLagana

1

Tôi đã cập nhật câu trả lời của mình với một sự đơn giản hóa: chức năng example_functionđược thông qua một cuộc gọi lại hoặc hook với một giá trị mặc định sao cho example_functionkhông còn cần phải kiểm tra để xem liệu nó có được thông qua hay không:

hook=lambda *args, **kwargs: None

Trên đây là biểu thức lambda trả về Noneexample_functioncó thể gọi giá trị mặc định này hookvới bất kỳ sự kết hợp nào của các tham số vị trí và từ khóa tại các vị trí khác nhau trong hàm.

Trong ví dụ dưới đây, tôi chỉ quan tâm đến "end_iteration""result"các sự kiện.

def example_function(numbers, n_iters, hook=lambda *args, **kwargs: None):
    hook("init")
    sum_all = 0
    for number in numbers:
        for i_iter in range(n_iters):
            hook("start_iteration", number)
            number = number/2
            hook("end_iteration", number)
        sum_all += number
    hook("result", sum_all)
    return sum_all

if __name__ == '__main__':
    def my_hook(event_type, *args):
        if event_type in ["end_iteration", "result"]:
            print(args[0])

    print('sum = ', example_function([1, 3, 12], 3))
    print('sum = ', example_function([1, 3, 12], 3, my_hook))

Bản in:

sum =  2.0
0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
sum =  2.0

Hàm hook có thể đơn giản hoặc phức tạp như bạn muốn. Ở đây nó đang kiểm tra loại sự kiện và thực hiện một bản in đơn giản. Nhưng nó có thể có được một loggerthể hiện và ghi lại thông điệp. Bạn có thể có tất cả sự phong phú của việc đăng nhập nếu bạn cần nhưng đơn giản nếu bạn không.


Cảm ơn câu trả lời, Ronald. Ý tưởng mở rộng ý tưởng gọi lại để thực hiện các cuộc gọi lại trong các phần khác nhau của hàm (và chuyển một biến bối cảnh cho chúng) có vẻ như là cách tốt nhất để đi, thực sự. Nó làm cho nó dễ dàng hơn nhiều để viết các cuộc gọi lại và với một mức giá hợp lý trong sự phức tạp thêm vào example_function.
JLagana

Liên lạc tốt đẹp với giá trị mặc định; đó là một cách đơn giản để loại bỏ rất nhiều ifcâu lệnh :)
JLagana
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.