Em Python não há o costume de se usar getters e setters, por que tem que se pensar de uma forma diferente. Quando você fala em "atributos privados" - eles são privados para "quem"?
A ideia do encapsulamento em OO é que pessoas usando sua classe e métodos púbico não precisem se preocupar com os estados privados, nem devam tentar mexer neles diretamente. Isso facilita que na hora de desenvolver a classe você não precise se preocupar com "e se alguém deixar esse atributo inconsistente" entre duas chamadas de métodos, e para quem está usando não precisa se preocupar com "se eu alterar esse valor, será que quebro alguma coisa no funcionamento do objeto"?
O objetivo de atributos privados NÃO É, por exemplo, evitar que alguém que use sua classe possa ler algum valor que você considera privado por questões de segurança para se proteger de algo como um "programador mal intencionado" que esteja fazendo uso de sua classe.
Como linguagens como Java e C++ são definidas, dá a impressão de que atributos e métodos privados possam oferecer segurança contra um programador mal intencionado que esteja usando sua classe. Essa segurança não é real - em ambas as linguagens é possível se acessar esses atributos privados - as vezes dando muitas voltas.
Em Python e outras linguagens dinâmicas, o uso de introspecção torna bem fácil achar e usar qualquer atributo marcado como privado (mesmo os prefixados com __)
Em suma: se alguém está escrevendo código que vai rodar no mesmo processo que sua classe com atributos privados - ele pode e deve poder acessar os dados. É diferente do caso de um programdor em um sistema diferente do seu que vai acessar seus dados por uma API. Nesse caso, os atributos privados simplesmente não são expostos na API. Em Python, em vez de tentar forçar atributos privados inacessíveis, é costume dizer que a linguagem é usada por "adultos que consentem" .
Então, a prática é prefixar atributos e métodos privados com um único _ : dessa forma quem for usar a classe sabe que não precisa mexer com esses atributos.
Agora, Python tem um mecanismo muito poderoso para acesso a atributos, que chamamos de "descriptor protocol" - isso é o que é usado internamente pelo property para permitir criar métodos - ele vai muito além do que o proeprt permite (basicamente você pode definir atributos numa classe que automaticamente tem getters e setters customizados - basta criar uma classe especial para esses atributos com os métodos get, set ou del - o property faz isso).
Dito isso, o property é um facilitador para você, ao querer ler ou escrever um atributo, executar algum código customizado que pode transformar ou validar o dado daquele atributo, quando ele for acessado (por exemplo, leia o valor de um banco de dados, faça uma formatação, validar o tipo do atributo, etc...).
Usar o property (ou getters e setters) para simplesmente guardar o valor como ele veio e devolve-lo como veio, não faz sentido - a não ser, por exemplo, que você queira que a classe seja thread-safe e use esse código para usar locks e semaphores na alteração dos atributos. E é por "não fazer sentido" que a internet recomenda que não se faça - simplesmente por que o seu programa vai ser exatamente o mesmo com ou sem os getters e setters -
Sem o property:
class Teste:
def __init__(self, valor):
self.valor = valor
Com o property:
class Teste:
def __init__(self, nome):
self.nome = nome
Com o property:
@property
def nome(self):
# Este código é executado quando alguém for
# ler o valor de self.nome
return self._nome
@nome.setter
def nome(self, value):
# este código é executado sempre que alguém fizer
# self.nome = value
self._nome = value
E pronto - quem vai usar seu código, não vai usar uma chamada ao "setter" - a mágica do Python é que se você precisar colocar lógica customizada no getter e no setter, isso é completamente transparente para quem usa sua classe e seus atributos. Se nenhuma lógica extra é necessária para um dado atributo, não tem por que criar um property pra ele.